Files
windmill/backend/windmill-trigger-postgres/src/converter.rs
T
Diego Imbert 8e7ba9b33d feat: Data table as pg resource / trigger (#8088)
* Enable running pg scripts with datatable database input

* Postgres triggers for data tables

* REPLICATION attribute on custom_instance_user

* disable edit for datatables

* Update backend/windmill-trigger-postgres/src/replication_message.rs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-25 15:06:12 +00:00

490 lines
16 KiB
Rust

use core::str;
use std::{
num::{ParseFloatError, ParseIntError},
str::FromStr,
};
use super::{
bool::{parse_bool, ParseBoolError},
hex::{from_bytea_hex, ByteaHexParseError},
};
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use rust_decimal::Decimal;
use rust_postgres::types::Type;
use serde_json::{to_value, Number, Value};
use thiserror::Error;
use uuid::Uuid;
/**
* This implementation is inspired by Postgres replication functionality
* from https://github.com/supabase/pg_replicate
*
* Original implementation:
* - https://github.com/supabase/pg_replicate/blob/main/pg_replicate/src/conversions/text.rs
*
*/
#[derive(Debug, Error)]
pub enum ConverterError {
#[error("invalid bool value")]
InvalidBool(#[from] ParseBoolError),
#[error("invalid int value")]
InvalidInt(#[from] ParseIntError),
#[error("invalid float value")]
InvalidFloat(#[from] ParseFloatError),
#[error("invalid numeric: {0}")]
InvalidNumeric(#[from] rust_decimal::Error),
#[error("invalid bytea: {0}")]
InvalidBytea(#[from] ByteaHexParseError),
#[error("invalid uuid: {0}")]
InvalidUuid(#[from] uuid::Error),
#[error("invalid json: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("invalid timestamp: {0} ")]
InvalidTimestamp(#[from] chrono::ParseError),
#[error("invalid array: {0}")]
InvalidArray(#[from] ArrayParseError),
#[error("{0}")]
Custom(String),
}
fn convert_into<T>(number: T) -> Number
where
T: Sized,
serde_json::Number: From<T>,
{
serde_json::Number::from(number)
}
pub struct Converter;
#[derive(Debug, Error)]
pub enum ArrayParseError {
#[error("input too short")]
InputTooShort,
#[error("missing braces")]
MissingBraces,
}
fn f64_to_json_number(raw_val: f64) -> Result<Value, ConverterError> {
let temp = serde_json::Number::from_f64(raw_val.into())
.ok_or(ConverterError::Custom("invalid json-float".to_string()))?;
Ok(Value::Number(temp))
}
impl Converter {
pub fn try_from_str(typ: Option<Type>, str: &str) -> Result<Value, ConverterError> {
let value = match typ.unwrap_or(Type::TEXT) {
Type::BOOL => Value::Bool(parse_bool(str)?),
Type::BOOL_ARRAY => {
Converter::parse_array(str, |str| Ok(Value::Bool(parse_bool(str)?)))?
}
Type::CHAR | Type::BPCHAR | Type::VARCHAR | Type::NAME | Type::TEXT => {
Value::String(str.to_string())
}
Type::CHAR_ARRAY
| Type::BPCHAR_ARRAY
| Type::VARCHAR_ARRAY
| Type::NAME_ARRAY
| Type::TEXT_ARRAY => {
Converter::parse_array(str, |str| Ok(Value::String(str.to_string())))?
}
Type::INT2 => Value::Number(convert_into(str.parse::<i16>()?)),
Type::INT2_ARRAY => Converter::parse_array(str, |str| {
Ok(Value::Number(convert_into(str.parse::<i16>()?)))
})?,
Type::INT4 => Value::Number(convert_into(str.parse::<i32>()?)),
Type::INT4_ARRAY => Converter::parse_array(str, |str| {
Ok(Value::Number(convert_into(str.parse::<i32>()?)))
})?,
Type::INT8 => Value::Number(convert_into(str.parse::<i64>()?)),
Type::INT8_ARRAY => Converter::parse_array(str, |str| {
Ok(Value::Number(convert_into(str.parse::<i64>()?)))
})?,
Type::FLOAT4 => f64_to_json_number(str.parse::<f64>()?)?,
Type::FLOAT4_ARRAY => {
Converter::parse_array(str, |str| f64_to_json_number(str.parse::<f64>()?))?
}
Type::FLOAT8 => f64_to_json_number(str.parse::<f64>()?)?,
Type::FLOAT8_ARRAY => {
Converter::parse_array(str, |str| f64_to_json_number(str.parse::<f64>()?))?
}
Type::NUMERIC => serde_json::json!(Decimal::from_str(str)?),
Type::NUMERIC_ARRAY => {
Converter::parse_array(str, |str| Ok(serde_json::json!(Decimal::from_str(str)?)))?
}
Type::BYTEA => to_value(from_bytea_hex(str)?).unwrap(),
Type::BYTEA_ARRAY => {
Converter::parse_array(str, |str| Ok(to_value(from_bytea_hex(str)?).unwrap()))?
}
Type::DATE => {
let date = NaiveDate::parse_from_str(str, "%Y-%m-%d")?;
Value::String(date.to_string())
}
Type::DATE_ARRAY => Converter::parse_array(str, |str| {
let date = NaiveDate::parse_from_str(str, "%Y-%m-%d")?;
Ok(Value::String(date.to_string()))
})?,
Type::TIME => {
let time = NaiveTime::parse_from_str(str, "%H:%M:%S%.f")?;
Value::String(time.to_string())
}
Type::TIME_ARRAY => Converter::parse_array(str, |str| {
let time = NaiveTime::parse_from_str(str, "%H:%M:%S%.f")?;
Ok(Value::String(time.to_string()))
})?,
Type::TIMESTAMP => {
let timestamp = NaiveDateTime::parse_from_str(str, "%Y-%m-%d %H:%M:%S%.f")?;
Value::String(timestamp.to_string())
}
Type::TIMESTAMP_ARRAY => Converter::parse_array(str, |str| {
let timestamp = NaiveDateTime::parse_from_str(str, "%Y-%m-%d %H:%M:%S%.f")?;
Ok(Value::String(timestamp.to_string()))
})?,
Type::TIMESTAMPTZ => {
let val =
match DateTime::<FixedOffset>::parse_from_str(str, "%Y-%m-%d %H:%M:%S%.f%#z") {
Ok(val) => val,
Err(_) => {
DateTime::<FixedOffset>::parse_from_str(str, "%Y-%m-%d %H:%M:%S%.f%:z")?
}
};
let utc: DateTime<Utc> = val.into();
Value::String(utc.to_string())
}
Type::TIMESTAMPTZ_ARRAY => {
match Converter::parse_array(str, |str| {
let utc: DateTime<Utc> =
DateTime::<FixedOffset>::parse_from_str(str, "%Y-%m-%d %H:%M:%S%.f%#z")?
.into();
Ok(Value::String(utc.to_string()))
}) {
Ok(val) => val,
Err(_) => Converter::parse_array(str, |str| {
let utc: DateTime<Utc> = DateTime::<FixedOffset>::parse_from_str(
str,
"%Y-%m-%d %H:%M:%S%.f%#z",
)?
.into();
Ok(Value::String(utc.to_string()))
})?,
}
}
Type::UUID => Value::String(Uuid::parse_str(str)?.to_string()),
Type::UUID_ARRAY => Converter::parse_array(str, |str| {
Ok(Value::String(Uuid::parse_str(str)?.to_string()))
})?,
Type::JSON | Type::JSONB => serde_json::from_str::<serde_json::Value>(str)?,
Type::JSON_ARRAY | Type::JSONB_ARRAY => Converter::parse_array(str, |str| {
Ok(serde_json::from_str::<serde_json::Value>(str)?)
})?,
Type::OID => Value::Number(convert_into(str.parse::<u32>()?)),
Type::OID_ARRAY => Converter::parse_array(str, |str| {
Ok(Value::Number(convert_into(str.parse::<u32>()?)))
})?,
_ => Value::String(str.to_string()),
};
Ok(value)
}
fn parse_array<P>(str: &str, mut parse: P) -> Result<Value, ConverterError>
where
P: FnMut(&str) -> Result<Value, ConverterError>,
{
if str.len() < 2 {
return Err(ArrayParseError::InputTooShort.into());
}
if !str.starts_with('{') || !str.ends_with('}') {
return Err(ArrayParseError::MissingBraces.into());
}
let mut res = vec![];
let str = &str[1..(str.len() - 1)];
let mut val_str = String::with_capacity(10);
let mut in_quotes = false;
let mut in_escape = false;
let mut chars = str.chars();
let mut done = str.is_empty();
while !done {
loop {
match chars.next() {
Some(c) => match c {
c if in_escape => {
val_str.push(c);
in_escape = false;
}
'"' => in_quotes = !in_quotes,
'\\' => in_escape = true,
',' if !in_quotes => {
break;
}
c => {
val_str.push(c);
}
},
None => {
done = true;
break;
}
}
}
let val = if val_str.to_lowercase() == "null" {
Value::Null
} else {
parse(&val_str)?
};
res.push(val);
val_str.clear();
}
let arr = Value::Array(res);
Ok(arr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
// --- Scalar type conversions ---
#[test]
fn test_bool_true() {
let result = Converter::try_from_str(Some(Type::BOOL), "t").unwrap();
assert_eq!(result, Value::Bool(true));
}
#[test]
fn test_bool_false() {
let result = Converter::try_from_str(Some(Type::BOOL), "f").unwrap();
assert_eq!(result, Value::Bool(false));
}
#[test]
fn test_text() {
let result = Converter::try_from_str(Some(Type::TEXT), "hello world").unwrap();
assert_eq!(result, Value::String("hello world".to_string()));
}
#[test]
fn test_varchar() {
let result = Converter::try_from_str(Some(Type::VARCHAR), "test").unwrap();
assert_eq!(result, Value::String("test".to_string()));
}
#[test]
fn test_int2() {
let result = Converter::try_from_str(Some(Type::INT2), "42").unwrap();
assert_eq!(result, json!(42));
}
#[test]
fn test_int4() {
let result = Converter::try_from_str(Some(Type::INT4), "-100").unwrap();
assert_eq!(result, json!(-100));
}
#[test]
fn test_int8() {
let result = Converter::try_from_str(Some(Type::INT8), "9999999999").unwrap();
assert_eq!(result, json!(9999999999i64));
}
#[test]
fn test_float4() {
let result = Converter::try_from_str(Some(Type::FLOAT4), "3.14").unwrap();
assert!(result.as_f64().unwrap() - 3.14 < 0.001);
}
#[test]
fn test_float8() {
let result = Converter::try_from_str(Some(Type::FLOAT8), "2.718281828").unwrap();
assert!(result.as_f64().unwrap() - 2.718281828 < 0.0001);
}
#[test]
fn test_numeric() {
let result = Converter::try_from_str(Some(Type::NUMERIC), "123.456").unwrap();
assert_eq!(result.to_string(), "123.456");
}
#[test]
fn test_uuid() {
let result =
Converter::try_from_str(Some(Type::UUID), "550e8400-e29b-41d4-a716-446655440000")
.unwrap();
assert_eq!(
result,
Value::String("550e8400-e29b-41d4-a716-446655440000".to_string())
);
}
#[test]
fn test_json() {
let result =
Converter::try_from_str(Some(Type::JSON), r#"{"key": "value", "n": 1}"#).unwrap();
assert_eq!(result, json!({"key": "value", "n": 1}));
}
#[test]
fn test_jsonb() {
let result = Converter::try_from_str(Some(Type::JSONB), r#"[1,2,3]"#).unwrap();
assert_eq!(result, json!([1, 2, 3]));
}
#[test]
fn test_date() {
let result = Converter::try_from_str(Some(Type::DATE), "2024-01-15").unwrap();
assert_eq!(result, Value::String("2024-01-15".to_string()));
}
#[test]
fn test_time() {
let result = Converter::try_from_str(Some(Type::TIME), "14:30:00.0").unwrap();
assert_eq!(result, Value::String("14:30:00".to_string()));
}
#[test]
fn test_timestamp() {
let result =
Converter::try_from_str(Some(Type::TIMESTAMP), "2024-01-15 14:30:00.0").unwrap();
assert_eq!(result, Value::String("2024-01-15 14:30:00".to_string()));
}
#[test]
fn test_timestamptz() {
let result =
Converter::try_from_str(Some(Type::TIMESTAMPTZ), "2024-01-15 14:30:00.0+00").unwrap();
assert!(result.as_str().unwrap().contains("2024-01-15"));
}
#[test]
fn test_bytea() {
let result = Converter::try_from_str(Some(Type::BYTEA), "\\x48656c6c6f").unwrap();
assert_eq!(result, json!([72, 101, 108, 108, 111]));
}
#[test]
fn test_oid() {
let result = Converter::try_from_str(Some(Type::OID), "12345").unwrap();
assert_eq!(result, json!(12345u32));
}
#[test]
fn test_none_type_defaults_to_text() {
let result = Converter::try_from_str(None, "anything").unwrap();
assert_eq!(result, Value::String("anything".to_string()));
}
// --- Array type conversions ---
#[test]
fn test_int4_array() {
let result = Converter::try_from_str(Some(Type::INT4_ARRAY), "{1,2,3}").unwrap();
assert_eq!(result, json!([1, 2, 3]));
}
#[test]
fn test_text_array() {
let result = Converter::try_from_str(Some(Type::TEXT_ARRAY), "{hello,world}").unwrap();
assert_eq!(result, json!(["hello", "world"]));
}
#[test]
fn test_bool_array() {
let result = Converter::try_from_str(Some(Type::BOOL_ARRAY), "{t,f,t}").unwrap();
assert_eq!(result, json!([true, false, true]));
}
#[test]
fn test_array_with_null() {
let result = Converter::try_from_str(Some(Type::INT4_ARRAY), "{1,NULL,3}").unwrap();
assert_eq!(result, json!([1, null, 3]));
}
#[test]
fn test_array_with_quoted_strings() {
let result =
Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"hello, world","test"}"#).unwrap();
assert_eq!(result, json!(["hello, world", "test"]));
}
#[test]
fn test_empty_array() {
let result = Converter::try_from_str(Some(Type::INT4_ARRAY), "{}").unwrap();
assert_eq!(result, json!([]));
}
#[test]
fn test_uuid_array() {
let result = Converter::try_from_str(
Some(Type::UUID_ARRAY),
"{550e8400-e29b-41d4-a716-446655440000,6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
)
.unwrap();
assert_eq!(
result,
json!([
"550e8400-e29b-41d4-a716-446655440000",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8"
])
);
}
// --- Error cases ---
#[test]
fn test_invalid_int() {
assert!(Converter::try_from_str(Some(Type::INT4), "not_a_number").is_err());
}
#[test]
fn test_invalid_bool() {
assert!(Converter::try_from_str(Some(Type::BOOL), "yes").is_err());
}
#[test]
fn test_invalid_uuid() {
assert!(Converter::try_from_str(Some(Type::UUID), "not-a-uuid").is_err());
}
#[test]
fn test_invalid_json() {
assert!(Converter::try_from_str(Some(Type::JSON), "not json").is_err());
}
#[test]
fn test_array_missing_braces() {
assert!(Converter::try_from_str(Some(Type::INT4_ARRAY), "1,2,3").is_err());
}
#[test]
fn test_array_too_short() {
assert!(Converter::try_from_str(Some(Type::INT4_ARRAY), "{").is_err());
}
#[test]
fn test_array_with_escaped_backslash() {
let result = Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"a\\b","c"}"#).unwrap();
assert_eq!(result, json!(["a\\b", "c"]));
}
#[test]
fn test_float_nan_rejected() {
assert!(Converter::try_from_str(Some(Type::FLOAT4), "NaN").is_err());
}
}