From 7c85251e75ead0c48708e441741a059354d289d2 Mon Sep 17 00:00:00 2001 From: itowlson Date: Tue, 24 Jun 2025 13:34:32 +1200 Subject: [PATCH 1/9] PostgreSQL UUID and JSONB support Signed-off-by: itowlson --- Cargo.lock | 5 + crates/factor-outbound-pg/Cargo.toml | 4 +- crates/factor-outbound-pg/src/client.rs | 51 +- crates/factor-outbound-pg/src/host.rs | 93 +++- crates/factor-outbound-pg/src/lib.rs | 5 +- .../factor-outbound-pg/tests/factor_test.rs | 8 +- crates/world/src/conversions.rs | 456 ++++++++++++++---- crates/world/src/lib.rs | 4 +- examples/spin-timer/Cargo.lock | 5 + .../components/outbound-postgres/src/lib.rs | 104 +++- tests/test-components/helper/src/lib.rs | 2 +- wit/deps/spin-postgres@4.0.0/postgres.wit | 106 ++++ wit/deps/spin@3.2.0/world.wit | 16 + wit/world.wit | 3 +- 14 files changed, 729 insertions(+), 133 deletions(-) create mode 100644 wit/deps/spin-postgres@4.0.0/postgres.wit create mode 100644 wit/deps/spin@3.2.0/world.wit diff --git a/Cargo.lock b/Cargo.lock index f979c8d7b1..ea72fbf3eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6214,6 +6214,9 @@ dependencies = [ "chrono", "fallible-iterator 0.2.0", "postgres-protocol", + "serde", + "serde_json", + "uuid", ] [[package]] @@ -8250,6 +8253,7 @@ dependencies = [ "chrono", "native-tls", "postgres-native-tls", + "serde_json", "spin-core", "spin-factor-outbound-networking", "spin-factor-variables", @@ -8260,6 +8264,7 @@ dependencies = [ "tokio", "tokio-postgres", "tracing", + "uuid", ] [[package]] diff --git a/crates/factor-outbound-pg/Cargo.toml b/crates/factor-outbound-pg/Cargo.toml index da7bf6ed14..778206f96a 100644 --- a/crates/factor-outbound-pg/Cargo.toml +++ b/crates/factor-outbound-pg/Cargo.toml @@ -9,14 +9,16 @@ anyhow = { workspace = true } chrono = { workspace = true } native-tls = "0.2" postgres-native-tls = "0.5" +serde_json = { workspace = true } spin-core = { path = "../core" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } spin-world = { path = "../world" } tokio = { workspace = true, features = ["rt-multi-thread"] } -tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] } +tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } tracing = { workspace = true } +uuid = "1" [dev-dependencies] spin-factor-variables = { path = "../factor-variables" } diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index 9af7ab2aa3..f739270683 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -1,9 +1,9 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use native_tls::TlsConnector; use postgres_native_tls::MakeTlsConnector; use spin_world::async_trait; -use spin_world::spin::postgres::postgres::{ - self as v3, Column, DbDataType, DbValue, ParameterValue, RowSet, +use spin_world::spin::postgres4_0_0::postgres::{ + self as v4, Column, DbDataType, DbValue, ParameterValue, RowSet, }; use tokio_postgres::types::Type; use tokio_postgres::{config::SslMode, types::ToSql, Row}; @@ -19,13 +19,13 @@ pub trait Client { &self, statement: String, params: Vec, - ) -> Result; + ) -> Result; async fn query( &self, statement: String, params: Vec, - ) -> Result; + ) -> Result; } #[async_trait] @@ -55,12 +55,12 @@ impl Client for TokioClient { &self, statement: String, params: Vec, - ) -> Result { + ) -> Result { let params = params .iter() .map(to_sql_parameter) .collect::>>() - .map_err(|e| v3::Error::ValueConversionFailed(format!("{e:?}")))?; + .map_err(|e| v4::Error::ValueConversionFailed(format!("{e:?}")))?; let params_refs: Vec<&(dyn ToSql + Sync)> = params .iter() @@ -69,19 +69,19 @@ impl Client for TokioClient { self.execute(&statement, params_refs.as_slice()) .await - .map_err(|e| v3::Error::QueryFailed(format!("{e:?}"))) + .map_err(|e| v4::Error::QueryFailed(format!("{e:?}"))) } async fn query( &self, statement: String, params: Vec, - ) -> Result { + ) -> Result { let params = params .iter() .map(to_sql_parameter) .collect::>>() - .map_err(|e| v3::Error::BadParameter(format!("{e:?}")))?; + .map_err(|e| v4::Error::BadParameter(format!("{e:?}")))?; let params_refs: Vec<&(dyn ToSql + Sync)> = params .iter() @@ -91,7 +91,7 @@ impl Client for TokioClient { let results = self .query(&statement, params_refs.as_slice()) .await - .map_err(|e| v3::Error::QueryFailed(format!("{e:?}")))?; + .map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?; if results.is_empty() { return Ok(RowSet { @@ -105,7 +105,7 @@ impl Client for TokioClient { .iter() .map(convert_row) .collect::, _>>() - .map_err(|e| v3::Error::QueryFailed(format!("{e:?}")))?; + .map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?; Ok(RowSet { columns, rows }) } @@ -158,6 +158,15 @@ fn to_sql_parameter(value: &ParameterValue) -> Result { + let u = uuid::Uuid::parse_str(v).with_context(|| format!("invalid UUID {v}"))?; + Ok(Box::new(u)) + } + ParameterValue::Jsonb(v) => { + let j: serde_json::Value = serde_json::from_slice(v) + .with_context(|| format!("invalid JSON {}", String::from_utf8_lossy(v)))?; + Ok(Box::new(j)) + } ParameterValue::DbNull => Ok(Box::new(PgNull)), } } @@ -190,6 +199,8 @@ fn convert_data_type(pg_type: &Type) -> DbDataType { Type::TIMESTAMP | Type::TIMESTAMPTZ => DbDataType::Timestamp, Type::DATE => DbDataType::Date, Type::TIME => DbDataType::Time, + Type::UUID => DbDataType::Uuid, + Type::JSONB => DbDataType::Jsonb, _ => { tracing::debug!("Couldn't convert Postgres type {} to WIT", pg_type.name(),); DbDataType::Other @@ -285,6 +296,22 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { None => DbValue::DbNull, } } + &Type::UUID => { + let value: Option = row.try_get(index)?; + match value { + Some(v) => DbValue::Uuid(v.to_string()), + None => DbValue::DbNull, + } + } + &Type::JSONB => { + let value: Option = row.try_get(index)?; + match value { + Some(v) => { + DbValue::Jsonb(serde_json::to_vec(&v).context("invalid JSON from database")?) + } + None => DbValue::DbNull, + } + } t => { tracing::debug!( "Couldn't convert Postgres type {} in column {}", diff --git a/crates/factor-outbound-pg/src/host.rs b/crates/factor-outbound-pg/src/host.rs index d0b4b52a9b..04eb49e8ba 100644 --- a/crates/factor-outbound-pg/src/host.rs +++ b/crates/factor-outbound-pg/src/host.rs @@ -1,6 +1,7 @@ use anyhow::Result; use spin_core::wasmtime::component::Resource; -use spin_world::spin::postgres::postgres::{self as v3}; +use spin_world::spin::postgres3_0_0::postgres::{self as v3}; +use spin_world::spin::postgres4_0_0::postgres::{self as v4}; use spin_world::v1::postgres as v1; use spin_world::v1::rdbms_types as v1_types; use spin_world::v2::postgres::{self as v2}; @@ -16,24 +17,24 @@ impl InstanceState { async fn open_connection( &mut self, address: &str, - ) -> Result, v3::Error> { + ) -> Result, v4::Error> { self.connections .push( C::build_client(address) .await - .map_err(|e| v3::Error::ConnectionFailed(format!("{e:?}")))?, + .map_err(|e| v4::Error::ConnectionFailed(format!("{e:?}")))?, ) - .map_err(|_| v3::Error::ConnectionFailed("too many connections".into())) + .map_err(|_| v4::Error::ConnectionFailed("too many connections".into())) .map(Resource::new_own) } async fn get_client( &mut self, connection: Resource, - ) -> Result<&C, v3::Error> { + ) -> Result<&C, v4::Error> { self.connections .get(connection.rep()) - .ok_or_else(|| v3::Error::ConnectionFailed("no connection found".into())) + .ok_or_else(|| v4::Error::ConnectionFailed("no connection found".into())) } async fn is_address_allowed(&self, address: &str) -> Result { @@ -67,11 +68,15 @@ impl InstanceState { fn v2_params_to_v3( params: Vec, -) -> Result, v2::Error> { +) -> Result, v2::Error> { params.into_iter().map(|p| p.try_into()).collect() } -impl spin_world::spin::postgres::postgres::HostConnection +fn v3_params_to_v4(params: Vec) -> Vec { + params.into_iter().map(|p| p.into()).collect() +} + +impl spin_world::spin::postgres3_0_0::postgres::HostConnection for InstanceState { #[instrument(name = "spin_outbound_pg.open", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", db.address = Empty, server.port = Empty, db.namespace = Empty))] @@ -87,7 +92,7 @@ impl spin_world::spin::postgres::postgres::HostConnecti "address {address} is not permitted" ))); } - self.open_connection(&address).await + Ok(self.open_connection(&address).await?) } #[instrument(name = "spin_outbound_pg.execute", skip(self, connection, params), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", otel.name = statement))] @@ -97,10 +102,11 @@ impl spin_world::spin::postgres::postgres::HostConnecti statement: String, params: Vec, ) -> Result { - self.get_client(connection) + Ok(self + .get_client(connection) .await? - .execute(statement, params) - .await + .execute(statement, v3_params_to_v4(params)) + .await?) } #[instrument(name = "spin_outbound_pg.query", skip(self, connection, params), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", otel.name = statement))] @@ -110,13 +116,64 @@ impl spin_world::spin::postgres::postgres::HostConnecti statement: String, params: Vec, ) -> Result { + Ok(self + .get_client(connection) + .await? + .query(statement, v3_params_to_v4(params)) + .await? + .into()) + } + + async fn drop(&mut self, connection: Resource) -> anyhow::Result<()> { + self.connections.remove(connection.rep()); + Ok(()) + } +} + +impl v4::HostConnection for InstanceState { + #[instrument(name = "spin_outbound_pg.open", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", db.address = Empty, server.port = Empty, db.namespace = Empty))] + async fn open(&mut self, address: String) -> Result, v4::Error> { + spin_factor_outbound_networking::record_address_fields(&address); + + if !self + .is_address_allowed(&address) + .await + .map_err(|e| v4::Error::Other(e.to_string()))? + { + return Err(v4::Error::ConnectionFailed(format!( + "address {address} is not permitted" + ))); + } + self.open_connection(&address).await + } + + #[instrument(name = "spin_outbound_pg.execute", skip(self, connection, params), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", otel.name = statement))] + async fn execute( + &mut self, + connection: Resource, + statement: String, + params: Vec, + ) -> Result { + self.get_client(connection) + .await? + .execute(statement, params) + .await + } + + #[instrument(name = "spin_outbound_pg.query", skip(self, connection, params), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", otel.name = statement))] + async fn query( + &mut self, + connection: Resource, + statement: String, + params: Vec, + ) -> Result { self.get_client(connection) .await? .query(statement, params) .await } - async fn drop(&mut self, connection: Resource) -> anyhow::Result<()> { + async fn drop(&mut self, connection: Resource) -> anyhow::Result<()> { self.connections.remove(connection.rep()); Ok(()) } @@ -134,10 +191,16 @@ impl v3::Host for InstanceState { } } +impl v4::Host for InstanceState { + fn convert_error(&mut self, error: v4::Error) -> Result { + Ok(error) + } +} + /// Delegate a function call to the v3::HostConnection implementation macro_rules! delegate { ($self:ident.$name:ident($address:expr, $($arg:expr),*)) => {{ - if !$self.is_address_allowed(&$address).await.map_err(|e| v3::Error::Other(e.to_string()))? { + if !$self.is_address_allowed(&$address).await.map_err(|e| v4::Error::Other(e.to_string()))? { return Err(v1::PgError::ConnectionFailed(format!( "address {} is not permitted", $address ))); @@ -146,7 +209,7 @@ macro_rules! delegate { Ok(c) => c, Err(e) => return Err(e.into()), }; - ::$name($self, connection, $($arg),*) + ::$name($self, connection, $($arg),*) .await .map_err(|e| e.into()) }}; diff --git a/crates/factor-outbound-pg/src/lib.rs b/crates/factor-outbound-pg/src/lib.rs index 4b38ded051..0026ef9d31 100644 --- a/crates/factor-outbound-pg/src/lib.rs +++ b/crates/factor-outbound-pg/src/lib.rs @@ -24,7 +24,10 @@ impl Factor for OutboundPgFactor { ctx.link_bindings(spin_world::v1::postgres::add_to_linker::<_, FactorData>)?; ctx.link_bindings(spin_world::v2::postgres::add_to_linker::<_, FactorData>)?; ctx.link_bindings( - spin_world::spin::postgres::postgres::add_to_linker::<_, FactorData>, + spin_world::spin::postgres3_0_0::postgres::add_to_linker::<_, FactorData>, + )?; + ctx.link_bindings( + spin_world::spin::postgres4_0_0::postgres::add_to_linker::<_, FactorData>, )?; Ok(()) } diff --git a/crates/factor-outbound-pg/tests/factor_test.rs b/crates/factor-outbound-pg/tests/factor_test.rs index ae0ab28767..c4df559408 100644 --- a/crates/factor-outbound-pg/tests/factor_test.rs +++ b/crates/factor-outbound-pg/tests/factor_test.rs @@ -6,10 +6,10 @@ use spin_factor_variables::VariablesFactor; use spin_factors::{anyhow, RuntimeFactors}; use spin_factors_test::{toml, TestEnvironment}; use spin_world::async_trait; -use spin_world::spin::postgres::postgres::Error as PgError; -use spin_world::spin::postgres::postgres::HostConnection; -use spin_world::spin::postgres::postgres::{self as v2}; -use spin_world::spin::postgres::postgres::{ParameterValue, RowSet}; +use spin_world::spin::postgres4_0_0::postgres::Error as PgError; +use spin_world::spin::postgres4_0_0::postgres::HostConnection; +use spin_world::spin::postgres4_0_0::postgres::{self as v2}; +use spin_world::spin::postgres4_0_0::postgres::{ParameterValue, RowSet}; #[derive(RuntimeFactors)] struct TestFactors { diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index cd75d78a2d..37040473d4 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -12,8 +12,8 @@ mod rdbms_types { } } - impl From for v1::rdbms_types::Column { - fn from(value: spin::postgres::postgres::Column) -> Self { + impl From for v1::rdbms_types::Column { + fn from(value: spin::postgres4_0_0::postgres::Column) -> Self { v1::rdbms_types::Column { name: value.name, data_type: value.data_type.into(), @@ -21,8 +21,8 @@ mod rdbms_types { } } - impl From for v2::rdbms_types::Column { - fn from(value: spin::postgres::postgres::Column) -> Self { + impl From for v2::rdbms_types::Column { + fn from(value: spin::postgres4_0_0::postgres::Column) -> Self { v2::rdbms_types::Column { name: value.name, data_type: value.data_type.into(), @@ -30,6 +30,15 @@ mod rdbms_types { } } + impl From for spin::postgres3_0_0::postgres::Column { + fn from(value: spin::postgres4_0_0::postgres::Column) -> Self { + spin::postgres3_0_0::postgres::Column { + name: value.name, + data_type: value.data_type.into(), + } + } + } + impl From for v1::rdbms_types::DbValue { fn from(value: v2::rdbms_types::DbValue) -> v1::rdbms_types::DbValue { match value { @@ -52,26 +61,36 @@ mod rdbms_types { } } - impl From for v1::rdbms_types::DbValue { - fn from(value: spin::postgres::postgres::DbValue) -> v1::rdbms_types::DbValue { + impl From for v1::rdbms_types::DbValue { + fn from(value: spin::postgres4_0_0::postgres::DbValue) -> v1::rdbms_types::DbValue { match value { - spin::postgres::postgres::DbValue::Boolean(b) => { + spin::postgres4_0_0::postgres::DbValue::Boolean(b) => { v1::rdbms_types::DbValue::Boolean(b) } - spin::postgres::postgres::DbValue::Int8(i) => v1::rdbms_types::DbValue::Int8(i), - spin::postgres::postgres::DbValue::Int16(i) => v1::rdbms_types::DbValue::Int16(i), - spin::postgres::postgres::DbValue::Int32(i) => v1::rdbms_types::DbValue::Int32(i), - spin::postgres::postgres::DbValue::Int64(i) => v1::rdbms_types::DbValue::Int64(i), - spin::postgres::postgres::DbValue::Floating32(r) => { + spin::postgres4_0_0::postgres::DbValue::Int8(i) => { + v1::rdbms_types::DbValue::Int8(i) + } + spin::postgres4_0_0::postgres::DbValue::Int16(i) => { + v1::rdbms_types::DbValue::Int16(i) + } + spin::postgres4_0_0::postgres::DbValue::Int32(i) => { + v1::rdbms_types::DbValue::Int32(i) + } + spin::postgres4_0_0::postgres::DbValue::Int64(i) => { + v1::rdbms_types::DbValue::Int64(i) + } + spin::postgres4_0_0::postgres::DbValue::Floating32(r) => { v1::rdbms_types::DbValue::Floating32(r) } - spin::postgres::postgres::DbValue::Floating64(r) => { + spin::postgres4_0_0::postgres::DbValue::Floating64(r) => { v1::rdbms_types::DbValue::Floating64(r) } - spin::postgres::postgres::DbValue::Str(s) => v1::rdbms_types::DbValue::Str(s), - spin::postgres::postgres::DbValue::Binary(b) => v1::rdbms_types::DbValue::Binary(b), - spin::postgres::postgres::DbValue::DbNull => v1::rdbms_types::DbValue::DbNull, - spin::postgres::postgres::DbValue::Unsupported => { + spin::postgres4_0_0::postgres::DbValue::Str(s) => v1::rdbms_types::DbValue::Str(s), + spin::postgres4_0_0::postgres::DbValue::Binary(b) => { + v1::rdbms_types::DbValue::Binary(b) + } + spin::postgres4_0_0::postgres::DbValue::DbNull => v1::rdbms_types::DbValue::DbNull, + spin::postgres4_0_0::postgres::DbValue::Unsupported => { v1::rdbms_types::DbValue::Unsupported } _ => v1::rdbms_types::DbValue::Unsupported, @@ -79,26 +98,36 @@ mod rdbms_types { } } - impl From for v2::rdbms_types::DbValue { - fn from(value: spin::postgres::postgres::DbValue) -> v2::rdbms_types::DbValue { + impl From for v2::rdbms_types::DbValue { + fn from(value: spin::postgres4_0_0::postgres::DbValue) -> v2::rdbms_types::DbValue { match value { - spin::postgres::postgres::DbValue::Boolean(b) => { + spin::postgres4_0_0::postgres::DbValue::Boolean(b) => { v2::rdbms_types::DbValue::Boolean(b) } - spin::postgres::postgres::DbValue::Int8(i) => v2::rdbms_types::DbValue::Int8(i), - spin::postgres::postgres::DbValue::Int16(i) => v2::rdbms_types::DbValue::Int16(i), - spin::postgres::postgres::DbValue::Int32(i) => v2::rdbms_types::DbValue::Int32(i), - spin::postgres::postgres::DbValue::Int64(i) => v2::rdbms_types::DbValue::Int64(i), - spin::postgres::postgres::DbValue::Floating32(r) => { + spin::postgres4_0_0::postgres::DbValue::Int8(i) => { + v2::rdbms_types::DbValue::Int8(i) + } + spin::postgres4_0_0::postgres::DbValue::Int16(i) => { + v2::rdbms_types::DbValue::Int16(i) + } + spin::postgres4_0_0::postgres::DbValue::Int32(i) => { + v2::rdbms_types::DbValue::Int32(i) + } + spin::postgres4_0_0::postgres::DbValue::Int64(i) => { + v2::rdbms_types::DbValue::Int64(i) + } + spin::postgres4_0_0::postgres::DbValue::Floating32(r) => { v2::rdbms_types::DbValue::Floating32(r) } - spin::postgres::postgres::DbValue::Floating64(r) => { + spin::postgres4_0_0::postgres::DbValue::Floating64(r) => { v2::rdbms_types::DbValue::Floating64(r) } - spin::postgres::postgres::DbValue::Str(s) => v2::rdbms_types::DbValue::Str(s), - spin::postgres::postgres::DbValue::Binary(b) => v2::rdbms_types::DbValue::Binary(b), - spin::postgres::postgres::DbValue::DbNull => v2::rdbms_types::DbValue::DbNull, - spin::postgres::postgres::DbValue::Unsupported => { + spin::postgres4_0_0::postgres::DbValue::Str(s) => v2::rdbms_types::DbValue::Str(s), + spin::postgres4_0_0::postgres::DbValue::Binary(b) => { + v2::rdbms_types::DbValue::Binary(b) + } + spin::postgres4_0_0::postgres::DbValue::DbNull => v2::rdbms_types::DbValue::DbNull, + spin::postgres4_0_0::postgres::DbValue::Unsupported => { v2::rdbms_types::DbValue::Unsupported } _ => v2::rdbms_types::DbValue::Unsupported, @@ -106,54 +135,195 @@ mod rdbms_types { } } - impl From for v1::rdbms_types::DbDataType { - fn from(value: spin::postgres::postgres::DbDataType) -> v1::rdbms_types::DbDataType { + impl From for spin::postgres3_0_0::postgres::DbValue { + fn from( + value: spin::postgres4_0_0::postgres::DbValue, + ) -> spin::postgres3_0_0::postgres::DbValue { + match value { + spin::postgres4_0_0::postgres::DbValue::Boolean(b) => { + spin::postgres3_0_0::postgres::DbValue::Boolean(b) + } + spin::postgres4_0_0::postgres::DbValue::Int8(i) => { + spin::postgres3_0_0::postgres::DbValue::Int8(i) + } + spin::postgres4_0_0::postgres::DbValue::Int16(i) => { + spin::postgres3_0_0::postgres::DbValue::Int16(i) + } + spin::postgres4_0_0::postgres::DbValue::Int32(i) => { + spin::postgres3_0_0::postgres::DbValue::Int32(i) + } + spin::postgres4_0_0::postgres::DbValue::Int64(i) => { + spin::postgres3_0_0::postgres::DbValue::Int64(i) + } + spin::postgres4_0_0::postgres::DbValue::Floating32(r) => { + spin::postgres3_0_0::postgres::DbValue::Floating32(r) + } + spin::postgres4_0_0::postgres::DbValue::Floating64(r) => { + spin::postgres3_0_0::postgres::DbValue::Floating64(r) + } + spin::postgres4_0_0::postgres::DbValue::Str(s) => { + spin::postgres3_0_0::postgres::DbValue::Str(s) + } + spin::postgres4_0_0::postgres::DbValue::Binary(b) => { + spin::postgres3_0_0::postgres::DbValue::Binary(b) + } + spin::postgres4_0_0::postgres::DbValue::Date(d) => { + spin::postgres3_0_0::postgres::DbValue::Date(d) + } + spin::postgres4_0_0::postgres::DbValue::Datetime(dt) => { + spin::postgres3_0_0::postgres::DbValue::Datetime(dt) + } + spin::postgres4_0_0::postgres::DbValue::Time(t) => { + spin::postgres3_0_0::postgres::DbValue::Time(t) + } + spin::postgres4_0_0::postgres::DbValue::Timestamp(t) => { + spin::postgres3_0_0::postgres::DbValue::Timestamp(t) + } + spin::postgres4_0_0::postgres::DbValue::Uuid(_) => { + spin::postgres3_0_0::postgres::DbValue::Unsupported + } + spin::postgres4_0_0::postgres::DbValue::Jsonb(_) => { + spin::postgres3_0_0::postgres::DbValue::Unsupported + } + spin::postgres4_0_0::postgres::DbValue::DbNull => { + spin::postgres3_0_0::postgres::DbValue::DbNull + } + spin::postgres4_0_0::postgres::DbValue::Unsupported => { + spin::postgres3_0_0::postgres::DbValue::Unsupported + } + } + } + } + + impl From for v1::rdbms_types::DbDataType { + fn from(value: spin::postgres4_0_0::postgres::DbDataType) -> v1::rdbms_types::DbDataType { match value { - spin::postgres::postgres::DbDataType::Boolean => { + spin::postgres4_0_0::postgres::DbDataType::Boolean => { v1::rdbms_types::DbDataType::Boolean } - spin::postgres::postgres::DbDataType::Int8 => v1::rdbms_types::DbDataType::Int8, - spin::postgres::postgres::DbDataType::Int16 => v1::rdbms_types::DbDataType::Int16, - spin::postgres::postgres::DbDataType::Int32 => v1::rdbms_types::DbDataType::Int32, - spin::postgres::postgres::DbDataType::Int64 => v1::rdbms_types::DbDataType::Int64, - spin::postgres::postgres::DbDataType::Floating32 => { + spin::postgres4_0_0::postgres::DbDataType::Int8 => { + v1::rdbms_types::DbDataType::Int8 + } + spin::postgres4_0_0::postgres::DbDataType::Int16 => { + v1::rdbms_types::DbDataType::Int16 + } + spin::postgres4_0_0::postgres::DbDataType::Int32 => { + v1::rdbms_types::DbDataType::Int32 + } + spin::postgres4_0_0::postgres::DbDataType::Int64 => { + v1::rdbms_types::DbDataType::Int64 + } + spin::postgres4_0_0::postgres::DbDataType::Floating32 => { v1::rdbms_types::DbDataType::Floating32 } - spin::postgres::postgres::DbDataType::Floating64 => { + spin::postgres4_0_0::postgres::DbDataType::Floating64 => { v1::rdbms_types::DbDataType::Floating64 } - spin::postgres::postgres::DbDataType::Str => v1::rdbms_types::DbDataType::Str, - spin::postgres::postgres::DbDataType::Binary => v1::rdbms_types::DbDataType::Binary, - spin::postgres::postgres::DbDataType::Other => v1::rdbms_types::DbDataType::Other, + spin::postgres4_0_0::postgres::DbDataType::Str => v1::rdbms_types::DbDataType::Str, + spin::postgres4_0_0::postgres::DbDataType::Binary => { + v1::rdbms_types::DbDataType::Binary + } + spin::postgres4_0_0::postgres::DbDataType::Other => { + v1::rdbms_types::DbDataType::Other + } _ => v1::rdbms_types::DbDataType::Other, } } } - impl From for v2::rdbms_types::DbDataType { - fn from(value: spin::postgres::postgres::DbDataType) -> v2::rdbms_types::DbDataType { + impl From for v2::rdbms_types::DbDataType { + fn from(value: spin::postgres4_0_0::postgres::DbDataType) -> v2::rdbms_types::DbDataType { match value { - spin::postgres::postgres::DbDataType::Boolean => { + spin::postgres4_0_0::postgres::DbDataType::Boolean => { v2::rdbms_types::DbDataType::Boolean } - spin::postgres::postgres::DbDataType::Int8 => v2::rdbms_types::DbDataType::Int8, - spin::postgres::postgres::DbDataType::Int16 => v2::rdbms_types::DbDataType::Int16, - spin::postgres::postgres::DbDataType::Int32 => v2::rdbms_types::DbDataType::Int32, - spin::postgres::postgres::DbDataType::Int64 => v2::rdbms_types::DbDataType::Int64, - spin::postgres::postgres::DbDataType::Floating32 => { + spin::postgres4_0_0::postgres::DbDataType::Int8 => { + v2::rdbms_types::DbDataType::Int8 + } + spin::postgres4_0_0::postgres::DbDataType::Int16 => { + v2::rdbms_types::DbDataType::Int16 + } + spin::postgres4_0_0::postgres::DbDataType::Int32 => { + v2::rdbms_types::DbDataType::Int32 + } + spin::postgres4_0_0::postgres::DbDataType::Int64 => { + v2::rdbms_types::DbDataType::Int64 + } + spin::postgres4_0_0::postgres::DbDataType::Floating32 => { v2::rdbms_types::DbDataType::Floating32 } - spin::postgres::postgres::DbDataType::Floating64 => { + spin::postgres4_0_0::postgres::DbDataType::Floating64 => { v2::rdbms_types::DbDataType::Floating64 } - spin::postgres::postgres::DbDataType::Str => v2::rdbms_types::DbDataType::Str, - spin::postgres::postgres::DbDataType::Binary => v2::rdbms_types::DbDataType::Binary, - spin::postgres::postgres::DbDataType::Other => v2::rdbms_types::DbDataType::Other, + spin::postgres4_0_0::postgres::DbDataType::Str => v2::rdbms_types::DbDataType::Str, + spin::postgres4_0_0::postgres::DbDataType::Binary => { + v2::rdbms_types::DbDataType::Binary + } + spin::postgres4_0_0::postgres::DbDataType::Other => { + v2::rdbms_types::DbDataType::Other + } _ => v2::rdbms_types::DbDataType::Other, } } } + impl From for spin::postgres3_0_0::postgres::DbDataType { + fn from( + value: spin::postgres4_0_0::postgres::DbDataType, + ) -> spin::postgres3_0_0::postgres::DbDataType { + match value { + spin::postgres4_0_0::postgres::DbDataType::Boolean => { + spin::postgres3_0_0::postgres::DbDataType::Boolean + } + spin::postgres4_0_0::postgres::DbDataType::Int8 => { + spin::postgres3_0_0::postgres::DbDataType::Int8 + } + spin::postgres4_0_0::postgres::DbDataType::Int16 => { + spin::postgres3_0_0::postgres::DbDataType::Int16 + } + spin::postgres4_0_0::postgres::DbDataType::Int32 => { + spin::postgres3_0_0::postgres::DbDataType::Int32 + } + spin::postgres4_0_0::postgres::DbDataType::Int64 => { + spin::postgres3_0_0::postgres::DbDataType::Int64 + } + spin::postgres4_0_0::postgres::DbDataType::Floating32 => { + spin::postgres3_0_0::postgres::DbDataType::Floating32 + } + spin::postgres4_0_0::postgres::DbDataType::Floating64 => { + spin::postgres3_0_0::postgres::DbDataType::Floating64 + } + spin::postgres4_0_0::postgres::DbDataType::Str => { + spin::postgres3_0_0::postgres::DbDataType::Str + } + spin::postgres4_0_0::postgres::DbDataType::Binary => { + spin::postgres3_0_0::postgres::DbDataType::Binary + } + spin::postgres4_0_0::postgres::DbDataType::Date => { + spin::postgres3_0_0::postgres::DbDataType::Date + } + spin::postgres4_0_0::postgres::DbDataType::Datetime => { + spin::postgres3_0_0::postgres::DbDataType::Datetime + } + spin::postgres4_0_0::postgres::DbDataType::Time => { + spin::postgres3_0_0::postgres::DbDataType::Time + } + spin::postgres4_0_0::postgres::DbDataType::Timestamp => { + spin::postgres3_0_0::postgres::DbDataType::Timestamp + } + spin::postgres4_0_0::postgres::DbDataType::Uuid => { + spin::postgres3_0_0::postgres::DbDataType::Other + } + spin::postgres4_0_0::postgres::DbDataType::Jsonb => { + spin::postgres3_0_0::postgres::DbDataType::Other + } + spin::postgres4_0_0::postgres::DbDataType::Other => { + spin::postgres3_0_0::postgres::DbDataType::Other + } + } + } + } + impl From for v1::rdbms_types::DbDataType { fn from(value: v2::rdbms_types::DbDataType) -> v1::rdbms_types::DbDataType { match value { @@ -220,27 +390,27 @@ mod rdbms_types { } } - impl TryFrom for spin::postgres::postgres::ParameterValue { + impl TryFrom for spin::postgres4_0_0::postgres::ParameterValue { type Error = v1::postgres::PgError; fn try_from( value: v1::rdbms_types::ParameterValue, - ) -> Result { + ) -> Result { let converted = match value { v1::rdbms_types::ParameterValue::Boolean(b) => { - spin::postgres::postgres::ParameterValue::Boolean(b) + spin::postgres4_0_0::postgres::ParameterValue::Boolean(b) } v1::rdbms_types::ParameterValue::Int8(i) => { - spin::postgres::postgres::ParameterValue::Int8(i) + spin::postgres4_0_0::postgres::ParameterValue::Int8(i) } v1::rdbms_types::ParameterValue::Int16(i) => { - spin::postgres::postgres::ParameterValue::Int16(i) + spin::postgres4_0_0::postgres::ParameterValue::Int16(i) } v1::rdbms_types::ParameterValue::Int32(i) => { - spin::postgres::postgres::ParameterValue::Int32(i) + spin::postgres4_0_0::postgres::ParameterValue::Int32(i) } v1::rdbms_types::ParameterValue::Int64(i) => { - spin::postgres::postgres::ParameterValue::Int64(i) + spin::postgres4_0_0::postgres::ParameterValue::Int64(i) } v1::rdbms_types::ParameterValue::Uint8(_) | v1::rdbms_types::ParameterValue::Uint16(_) @@ -251,46 +421,46 @@ mod rdbms_types { )); } v1::rdbms_types::ParameterValue::Floating32(r) => { - spin::postgres::postgres::ParameterValue::Floating32(r) + spin::postgres4_0_0::postgres::ParameterValue::Floating32(r) } v1::rdbms_types::ParameterValue::Floating64(r) => { - spin::postgres::postgres::ParameterValue::Floating64(r) + spin::postgres4_0_0::postgres::ParameterValue::Floating64(r) } v1::rdbms_types::ParameterValue::Str(s) => { - spin::postgres::postgres::ParameterValue::Str(s) + spin::postgres4_0_0::postgres::ParameterValue::Str(s) } v1::rdbms_types::ParameterValue::Binary(b) => { - spin::postgres::postgres::ParameterValue::Binary(b) + spin::postgres4_0_0::postgres::ParameterValue::Binary(b) } v1::rdbms_types::ParameterValue::DbNull => { - spin::postgres::postgres::ParameterValue::DbNull + spin::postgres4_0_0::postgres::ParameterValue::DbNull } }; Ok(converted) } } - impl TryFrom for spin::postgres::postgres::ParameterValue { + impl TryFrom for spin::postgres4_0_0::postgres::ParameterValue { type Error = v2::rdbms_types::Error; fn try_from( value: v2::rdbms_types::ParameterValue, - ) -> Result { + ) -> Result { let converted = match value { v2::rdbms_types::ParameterValue::Boolean(b) => { - spin::postgres::postgres::ParameterValue::Boolean(b) + spin::postgres4_0_0::postgres::ParameterValue::Boolean(b) } v2::rdbms_types::ParameterValue::Int8(i) => { - spin::postgres::postgres::ParameterValue::Int8(i) + spin::postgres4_0_0::postgres::ParameterValue::Int8(i) } v2::rdbms_types::ParameterValue::Int16(i) => { - spin::postgres::postgres::ParameterValue::Int16(i) + spin::postgres4_0_0::postgres::ParameterValue::Int16(i) } v2::rdbms_types::ParameterValue::Int32(i) => { - spin::postgres::postgres::ParameterValue::Int32(i) + spin::postgres4_0_0::postgres::ParameterValue::Int32(i) } v2::rdbms_types::ParameterValue::Int64(i) => { - spin::postgres::postgres::ParameterValue::Int64(i) + spin::postgres4_0_0::postgres::ParameterValue::Int64(i) } v2::rdbms_types::ParameterValue::Uint8(_) | v2::rdbms_types::ParameterValue::Uint16(_) @@ -301,25 +471,78 @@ mod rdbms_types { )); } v2::rdbms_types::ParameterValue::Floating32(r) => { - spin::postgres::postgres::ParameterValue::Floating32(r) + spin::postgres4_0_0::postgres::ParameterValue::Floating32(r) } v2::rdbms_types::ParameterValue::Floating64(r) => { - spin::postgres::postgres::ParameterValue::Floating64(r) + spin::postgres4_0_0::postgres::ParameterValue::Floating64(r) } v2::rdbms_types::ParameterValue::Str(s) => { - spin::postgres::postgres::ParameterValue::Str(s) + spin::postgres4_0_0::postgres::ParameterValue::Str(s) } v2::rdbms_types::ParameterValue::Binary(b) => { - spin::postgres::postgres::ParameterValue::Binary(b) + spin::postgres4_0_0::postgres::ParameterValue::Binary(b) } v2::rdbms_types::ParameterValue::DbNull => { - spin::postgres::postgres::ParameterValue::DbNull + spin::postgres4_0_0::postgres::ParameterValue::DbNull } }; Ok(converted) } } + impl From + for spin::postgres4_0_0::postgres::ParameterValue + { + fn from( + value: spin::postgres3_0_0::postgres::ParameterValue, + ) -> spin::postgres4_0_0::postgres::ParameterValue { + match value { + spin::postgres3_0_0::postgres::ParameterValue::Boolean(b) => { + spin::postgres4_0_0::postgres::ParameterValue::Boolean(b) + } + spin::postgres3_0_0::postgres::ParameterValue::Int8(i) => { + spin::postgres4_0_0::postgres::ParameterValue::Int8(i) + } + spin::postgres3_0_0::postgres::ParameterValue::Int16(i) => { + spin::postgres4_0_0::postgres::ParameterValue::Int16(i) + } + spin::postgres3_0_0::postgres::ParameterValue::Int32(i) => { + spin::postgres4_0_0::postgres::ParameterValue::Int32(i) + } + spin::postgres3_0_0::postgres::ParameterValue::Int64(i) => { + spin::postgres4_0_0::postgres::ParameterValue::Int64(i) + } + spin::postgres3_0_0::postgres::ParameterValue::Floating32(r) => { + spin::postgres4_0_0::postgres::ParameterValue::Floating32(r) + } + spin::postgres3_0_0::postgres::ParameterValue::Floating64(r) => { + spin::postgres4_0_0::postgres::ParameterValue::Floating64(r) + } + spin::postgres3_0_0::postgres::ParameterValue::Str(s) => { + spin::postgres4_0_0::postgres::ParameterValue::Str(s) + } + spin::postgres3_0_0::postgres::ParameterValue::Binary(b) => { + spin::postgres4_0_0::postgres::ParameterValue::Binary(b) + } + spin::postgres3_0_0::postgres::ParameterValue::Date(d) => { + spin::postgres4_0_0::postgres::ParameterValue::Date(d) + } + spin::postgres3_0_0::postgres::ParameterValue::Datetime(dt) => { + spin::postgres4_0_0::postgres::ParameterValue::Datetime(dt) + } + spin::postgres3_0_0::postgres::ParameterValue::Time(t) => { + spin::postgres4_0_0::postgres::ParameterValue::Time(t) + } + spin::postgres3_0_0::postgres::ParameterValue::Timestamp(t) => { + spin::postgres4_0_0::postgres::ParameterValue::Timestamp(t) + } + spin::postgres3_0_0::postgres::ParameterValue::DbNull => { + spin::postgres4_0_0::postgres::ParameterValue::DbNull + } + } + } + } + impl From for v1::mysql::MysqlError { fn from(error: v2::rdbms_types::Error) -> v1::mysql::MysqlError { match error { @@ -334,42 +557,68 @@ mod rdbms_types { } } - impl From for v1::postgres::PgError { - fn from(error: spin::postgres::postgres::Error) -> v1::postgres::PgError { + impl From for v1::postgres::PgError { + fn from(error: spin::postgres4_0_0::postgres::Error) -> v1::postgres::PgError { match error { - spin::postgres::postgres::Error::ConnectionFailed(e) => { + spin::postgres4_0_0::postgres::Error::ConnectionFailed(e) => { v1::postgres::PgError::ConnectionFailed(e) } - spin::postgres::postgres::Error::BadParameter(e) => { + spin::postgres4_0_0::postgres::Error::BadParameter(e) => { v1::postgres::PgError::BadParameter(e) } - spin::postgres::postgres::Error::QueryFailed(e) => { + spin::postgres4_0_0::postgres::Error::QueryFailed(e) => { v1::postgres::PgError::QueryFailed(e) } - spin::postgres::postgres::Error::ValueConversionFailed(e) => { + spin::postgres4_0_0::postgres::Error::ValueConversionFailed(e) => { v1::postgres::PgError::ValueConversionFailed(e) } - spin::postgres::postgres::Error::Other(e) => v1::postgres::PgError::OtherError(e), + spin::postgres4_0_0::postgres::Error::Other(e) => { + v1::postgres::PgError::OtherError(e) + } } } } - impl From for v2::rdbms_types::Error { - fn from(error: spin::postgres::postgres::Error) -> v2::rdbms_types::Error { + impl From for v2::rdbms_types::Error { + fn from(error: spin::postgres4_0_0::postgres::Error) -> v2::rdbms_types::Error { match error { - spin::postgres::postgres::Error::ConnectionFailed(e) => { + spin::postgres4_0_0::postgres::Error::ConnectionFailed(e) => { v2::rdbms_types::Error::ConnectionFailed(e) } - spin::postgres::postgres::Error::BadParameter(e) => { + spin::postgres4_0_0::postgres::Error::BadParameter(e) => { v2::rdbms_types::Error::BadParameter(e) } - spin::postgres::postgres::Error::QueryFailed(e) => { + spin::postgres4_0_0::postgres::Error::QueryFailed(e) => { v2::rdbms_types::Error::QueryFailed(e) } - spin::postgres::postgres::Error::ValueConversionFailed(e) => { + spin::postgres4_0_0::postgres::Error::ValueConversionFailed(e) => { v2::rdbms_types::Error::ValueConversionFailed(e) } - spin::postgres::postgres::Error::Other(e) => v2::rdbms_types::Error::Other(e), + spin::postgres4_0_0::postgres::Error::Other(e) => v2::rdbms_types::Error::Other(e), + } + } + } + + impl From for spin::postgres3_0_0::postgres::Error { + fn from( + error: spin::postgres4_0_0::postgres::Error, + ) -> spin::postgres3_0_0::postgres::Error { + match error { + spin::postgres4_0_0::postgres::Error::ConnectionFailed(e) => { + spin::postgres3_0_0::postgres::Error::ConnectionFailed(e) + } + spin::postgres4_0_0::postgres::Error::BadParameter(e) => { + spin::postgres3_0_0::postgres::Error::BadParameter(e) + } + spin::postgres4_0_0::postgres::Error::QueryFailed(e) => { + spin::postgres3_0_0::postgres::Error::QueryFailed(e) + } + spin::postgres4_0_0::postgres::Error::ValueConversionFailed(e) => { + spin::postgres3_0_0::postgres::Error::ValueConversionFailed(e) + } + spin::postgres4_0_0::postgres::Error::Other(e) => { + spin::postgres3_0_0::postgres::Error::Other(e) + } } } } @@ -378,8 +627,8 @@ mod rdbms_types { mod postgres { use super::*; - impl From for v1::postgres::RowSet { - fn from(value: spin::postgres::postgres::RowSet) -> v1::postgres::RowSet { + impl From for v1::postgres::RowSet { + fn from(value: spin::postgres4_0_0::postgres::RowSet) -> v1::postgres::RowSet { v1::mysql::RowSet { columns: value.columns.into_iter().map(Into::into).collect(), rows: value @@ -391,8 +640,8 @@ mod postgres { } } - impl From for v2::rdbms_types::RowSet { - fn from(value: spin::postgres::postgres::RowSet) -> v2::rdbms_types::RowSet { + impl From for v2::rdbms_types::RowSet { + fn from(value: spin::postgres4_0_0::postgres::RowSet) -> v2::rdbms_types::RowSet { v2::rdbms_types::RowSet { columns: value.columns.into_iter().map(Into::into).collect(), rows: value @@ -403,6 +652,21 @@ mod postgres { } } } + + impl From for spin::postgres3_0_0::postgres::RowSet { + fn from( + value: spin::postgres4_0_0::postgres::RowSet, + ) -> spin::postgres3_0_0::postgres::RowSet { + spin::postgres3_0_0::postgres::RowSet { + columns: value.columns.into_iter().map(Into::into).collect(), + rows: value + .rows + .into_iter() + .map(|r| r.into_iter().map(Into::into).collect()) + .collect(), + } + } + } } mod mysql { diff --git a/crates/world/src/lib.rs b/crates/world/src/lib.rs index a9c01c6a94..cbc2283aba 100644 --- a/crates/world/src/lib.rs +++ b/crates/world/src/lib.rs @@ -11,6 +11,7 @@ wasmtime::component::bindgen!({ include fermyon:spin/platform@2.0.0; include fermyon:spin/platform@3.0.0; include spin:up/platform@3.2.0; + include spin:up/platform@3.4.0; include wasi:keyvalue/imports@0.2.0-draft2; } "#, @@ -31,7 +32,8 @@ wasmtime::component::bindgen!({ "fermyon:spin/sqlite@2.0.0/error" => v2::sqlite::Error, "fermyon:spin/sqlite/error" => v1::sqlite::Error, "fermyon:spin/variables@2.0.0/error" => v2::variables::Error, - "spin:postgres/postgres/error" => spin::postgres::postgres::Error, + "spin:postgres/postgres@3.0.0/error" => spin::postgres3_0_0::postgres::Error, + "spin:postgres/postgres@4.0.0/error" => spin::postgres4_0_0::postgres::Error, "spin:sqlite/sqlite/error" => spin::sqlite::sqlite::Error, "wasi:config/store@0.2.0-draft-2024-09-27/error" => wasi::config::store::Error, "wasi:keyvalue/store/error" => wasi::keyvalue::store::Error, diff --git a/examples/spin-timer/Cargo.lock b/examples/spin-timer/Cargo.lock index 88cce711f8..e58b3e05d6 100644 --- a/examples/spin-timer/Cargo.lock +++ b/examples/spin-timer/Cargo.lock @@ -3350,6 +3350,9 @@ dependencies = [ "chrono", "fallible-iterator 0.2.0", "postgres-protocol", + "serde", + "serde_json", + "uuid", ] [[package]] @@ -4630,6 +4633,7 @@ dependencies = [ "chrono", "native-tls", "postgres-native-tls", + "serde_json", "spin-core", "spin-factor-outbound-networking", "spin-factors", @@ -4638,6 +4642,7 @@ dependencies = [ "tokio", "tokio-postgres", "tracing", + "uuid", ] [[package]] diff --git a/tests/test-components/components/outbound-postgres/src/lib.rs b/tests/test-components/components/outbound-postgres/src/lib.rs index f5481c4153..5528bd6199 100644 --- a/tests/test-components/components/outbound-postgres/src/lib.rs +++ b/tests/test-components/components/outbound-postgres/src/lib.rs @@ -1,4 +1,4 @@ -use helper::http_trigger_bindings::spin::postgres::postgres; +use helper::http_trigger_bindings::spin::postgres4_0_0::postgres; use helper::{ensure, ensure_eq, ensure_matches, ensure_ok}; helper::define_component!(Component); @@ -35,6 +35,16 @@ impl Component { ensure_matches!(rowset.rows[1][2], postgres::DbValue::Time((h, m, s, ns)) if h == 14 && m == 15 && s == 16 && ns == 17); ensure_matches!(rowset.rows[1][3], postgres::DbValue::Datetime((y, _, _, h, _, _, ns)) if y == 1989 && h == 1 && ns == 4); + let rowset = ensure_ok!(json_types(&conn)); + ensure!(rowset.rows.iter().all(|r| r.len() == 2)); + ensure_matches!(&rowset.rows[0][1], postgres::DbValue::Jsonb(v) if String::from_utf8_lossy(v) == r#"{"s":"hello","n":123,"b":true,"x":null}"#); + ensure_matches!(&rowset.rows[1][1], postgres::DbValue::Jsonb(v) if String::from_utf8_lossy(v) == r#"{"s":"world","n":234,"b":false,"x":null}"#); + + let rowset = ensure_ok!(uuid_type(&conn)); + ensure!(rowset.rows.iter().all(|r| r.len() == 2)); + ensure_matches!(&rowset.rows[0][1], postgres::DbValue::Uuid(v) if v == "12345678-1234-1234-1234-123456789abc"); + ensure_matches!(&rowset.rows[1][1], postgres::DbValue::Uuid(v) if v == "fedcba98-fedc-fedc-fedc-fedcba987654"); + let rowset = ensure_ok!(nullable(&conn)); ensure!(rowset.rows.iter().all(|r| r.len() == 1)); ensure!(matches!(rowset.rows[0][0], postgres::DbValue::DbNull)); @@ -177,6 +187,98 @@ fn date_time_types(conn: &postgres::Connection) -> Result Result { + let create_table_sql = r#" + CREATE TEMPORARY TABLE test_json_types ( + index int2, + j jsonb NOT NULL + ); + "#; + + conn.execute(create_table_sql, &[])?; + + // We will use this to test that we correctly decode "known good" + // Postgres database values. (This validates our decoding logic + // independently of our encoding logic.) + let insert_sql_pg_literals = r#" + INSERT INTO test_json_types + (index, j) + VALUES + (1, jsonb('{ "s": "hello", "n": 123, "b": true, "x": null }')); + "#; + + conn.execute(insert_sql_pg_literals, &[])?; + + // We will use this to test that we correctly encode Spin ParameterValue + // objects. (In conjunction with knowing that our decode logic is good, + // this validates our encode logic.) + let insert_sql_spin_parameters = r#" + INSERT INTO test_json_types + (index, j) + VALUES + (2, $1); + "#; + + let jsonb_pv = postgres::ParameterValue::Jsonb(r#"{ "s": "world", "n": 234, "b": false, "x": null }"#.as_bytes().to_vec()); + conn.execute(insert_sql_spin_parameters, &[jsonb_pv])?; + + let sql = r#" + SELECT + index, + j + FROM test_json_types + ORDER BY index; + "#; + + conn.query(sql, &[]) +} + +fn uuid_type(conn: &postgres::Connection) -> Result { + let create_table_sql = r#" + CREATE TEMPORARY TABLE test_uuid_type ( + index int2, + u uuid NOT NULL + ); + "#; + + conn.execute(create_table_sql, &[])?; + + // We will use this to test that we correctly decode "known good" + // Postgres database values. (This validates our decoding logic + // independently of our encoding logic.) + let insert_sql_pg_literals = r#" + INSERT INTO test_uuid_type + (index, u) + VALUES + (1, uuid('12345678-1234-1234-1234-123456789abc')); + "#; + + conn.execute(insert_sql_pg_literals, &[])?; + + // We will use this to test that we correctly encode Spin ParameterValue + // objects. (In conjunction with knowing that our decode logic is good, + // this validates our encode logic.) + let insert_sql_spin_parameters = r#" + INSERT INTO test_uuid_type + (index, u) + VALUES + (2, $1); + "#; + + let uuid_pv = postgres::ParameterValue::Uuid("fedcba98-fedc-fedc-fedc-fedcba987654".to_owned()); + conn.execute(insert_sql_spin_parameters, &[uuid_pv])?; + + let sql = r#" + SELECT + index, + u + FROM test_uuid + ORDER BY index; + "#; + + conn.query(sql, &[]) +} + fn nullable(conn: &postgres::Connection) -> Result { let create_table_sql = r#" CREATE TEMPORARY TABLE test_nullable ( diff --git a/tests/test-components/helper/src/lib.rs b/tests/test-components/helper/src/lib.rs index 62f7be93f8..20d5209dcb 100644 --- a/tests/test-components/helper/src/lib.rs +++ b/tests/test-components/helper/src/lib.rs @@ -17,7 +17,7 @@ use bindings::wasi::io0_2_0::streams::OutputStream; #[cfg(feature = "define-component")] pub mod http_trigger_bindings { wit_bindgen::generate!({ - world: "spin:up/http-trigger@3.2.0", + world: "spin:up/http-trigger@3.4.0", path: "../../../wit", generate_all, pub_export_macro: true, diff --git a/wit/deps/spin-postgres@4.0.0/postgres.wit b/wit/deps/spin-postgres@4.0.0/postgres.wit new file mode 100644 index 0000000000..1522dbfb9e --- /dev/null +++ b/wit/deps/spin-postgres@4.0.0/postgres.wit @@ -0,0 +1,106 @@ +package spin:postgres@4.0.0; + +interface postgres { + /// Errors related to interacting with a database. + variant error { + connection-failed(string), + bad-parameter(string), + query-failed(string), + value-conversion-failed(string), + other(string) + } + + /// Data types for a database column + enum db-data-type { + boolean, + int8, + int16, + int32, + int64, + floating32, + floating64, + str, + binary, + date, + time, + datetime, + timestamp, + uuid, + jsonb, + other, + } + + /// Database values + variant db-value { + boolean(bool), + int8(s8), + int16(s16), + int32(s32), + int64(s64), + floating32(f32), + floating64(f64), + str(string), + binary(list), + date(tuple), // (year, month, day) + time(tuple), // (hour, minute, second, nanosecond) + /// Date-time types are always treated as UTC (without timezone info). + /// The instant is represented as a (year, month, day, hour, minute, second, nanosecond) tuple. + datetime(tuple), + /// Unix timestamp (seconds since epoch) + timestamp(s64), + uuid(string), + jsonb(list), + db-null, + unsupported, + } + + /// Values used in parameterized queries + variant parameter-value { + boolean(bool), + int8(s8), + int16(s16), + int32(s32), + int64(s64), + floating32(f32), + floating64(f64), + str(string), + binary(list), + date(tuple), // (year, month, day) + time(tuple), // (hour, minute, second, nanosecond) + /// Date-time types are always treated as UTC (without timezone info). + /// The instant is represented as a (year, month, day, hour, minute, second, nanosecond) tuple. + datetime(tuple), + /// Unix timestamp (seconds since epoch) + timestamp(s64), + uuid(string), + jsonb(list), + db-null, + } + + /// A database column + record column { + name: string, + data-type: db-data-type, + } + + /// A database row + type row = list; + + /// A set of database rows + record row-set { + columns: list, + rows: list, + } + + /// A connection to a postgres database. + resource connection { + /// Open a connection to the Postgres instance at `address`. + open: static func(address: string) -> result; + + /// Query the database. + query: func(statement: string, params: list) -> result; + + /// Execute command to the database. + execute: func(statement: string, params: list) -> result; + } +} diff --git a/wit/deps/spin@3.2.0/world.wit b/wit/deps/spin@3.2.0/world.wit new file mode 100644 index 0000000000..4d63b146bc --- /dev/null +++ b/wit/deps/spin@3.2.0/world.wit @@ -0,0 +1,16 @@ +package spin:up@3.2.0; + +/// The full world of a guest targeting an http-trigger +world http-trigger { + include platform; + export wasi:http/incoming-handler@0.2.0; +} + +/// The imports needed for a guest to run on a Spin host +world platform { + include fermyon:spin/platform@2.0.0; + include wasi:keyvalue/imports@0.2.0-draft2; + import spin:postgres/postgres@3.0.0; + import spin:sqlite/sqlite@3.0.0; + import wasi:config/store@0.2.0-draft-2024-09-27; +} diff --git a/wit/world.wit b/wit/world.wit index 4d63b146bc..b5d66b3b2f 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -1,4 +1,4 @@ -package spin:up@3.2.0; +package spin:up@3.4.0; /// The full world of a guest targeting an http-trigger world http-trigger { @@ -11,6 +11,7 @@ world platform { include fermyon:spin/platform@2.0.0; include wasi:keyvalue/imports@0.2.0-draft2; import spin:postgres/postgres@3.0.0; + import spin:postgres/postgres@4.0.0; import spin:sqlite/sqlite@3.0.0; import wasi:config/store@0.2.0-draft-2024-09-27; } From 292e7058b62aa8033ce0b90fd7d602101c82108b Mon Sep 17 00:00:00 2001 From: itowlson Date: Wed, 25 Jun 2025 11:12:30 +1200 Subject: [PATCH 2/9] PostgreSQL range and decimal types Signed-off-by: itowlson --- Cargo.lock | 205 +++++++- crates/factor-outbound-pg/Cargo.toml | 2 + crates/factor-outbound-pg/src/client.rs | 77 +++ crates/world/src/conversions.rs | 566 +++++++--------------- wit/deps/spin-postgres@4.0.0/postgres.wit | 15 + 5 files changed, 469 insertions(+), 396 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea72fbf3eb..6284a90a58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,17 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -167,6 +178,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -1086,6 +1103,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -1132,6 +1161,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "bstr" version = "1.10.0" @@ -1161,6 +1213,28 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.19.0" @@ -2925,6 +2999,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -3696,6 +3776,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -3703,7 +3786,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -6219,6 +6302,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "postgres_range" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6dce28dc5ba143d8eb157b62aac01ae5a1c585c40792158b720e86a87642101" +dependencies = [ + "postgres-protocol", + "postgres-types", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -6462,6 +6555,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ptree" version = "0.5.2" @@ -6590,6 +6703,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -6917,6 +7036,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -7037,6 +7165,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rle-decode-fast" version = "1.0.3" @@ -7141,6 +7298,23 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "postgres-types", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -7454,6 +7628,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -7796,6 +7976,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.6.0" @@ -8253,6 +8439,8 @@ dependencies = [ "chrono", "native-tls", "postgres-native-tls", + "postgres_range", + "rust_decimal", "serde_json", "spin-core", "spin-factor-outbound-networking", @@ -9147,6 +9335,12 @@ dependencies = [ "winx", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.43" @@ -11683,6 +11877,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "0.2.3" diff --git a/crates/factor-outbound-pg/Cargo.toml b/crates/factor-outbound-pg/Cargo.toml index 778206f96a..e47c70e084 100644 --- a/crates/factor-outbound-pg/Cargo.toml +++ b/crates/factor-outbound-pg/Cargo.toml @@ -9,6 +9,8 @@ anyhow = { workspace = true } chrono = { workspace = true } native-tls = "0.2" postgres-native-tls = "0.5" +postgres_range = "0.11" +rust_decimal = { version = "1.37", features = ["db-tokio-postgres"] } serde_json = { workspace = true } spin-core = { path = "../core" } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index f739270683..4ea509dd70 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -167,10 +167,42 @@ fn to_sql_parameter(value: &ParameterValue) -> Result { + let dec = rust_decimal::Decimal::from_str_exact(v) + .with_context(|| format!("invalid decimal {v}"))?; + Ok(Box::new(dec)) + } + ParameterValue::Range32((lower, upper)) => { + let lbound = lower.map(|(value, kind)| { + postgres_range::RangeBound::new(value, range_bound_kind(kind)) + }); + let ubound = upper.map(|(value, kind)| { + postgres_range::RangeBound::new(value, range_bound_kind(kind)) + }); + let r = postgres_range::Range::new(lbound, ubound); + Ok(Box::new(r)) + } + ParameterValue::Range64((lower, upper)) => { + let lbound = lower.map(|(value, kind)| { + postgres_range::RangeBound::new(value, range_bound_kind(kind)) + }); + let ubound = upper.map(|(value, kind)| { + postgres_range::RangeBound::new(value, range_bound_kind(kind)) + }); + let r = postgres_range::Range::new(lbound, ubound); + Ok(Box::new(r)) + } ParameterValue::DbNull => Ok(Box::new(PgNull)), } } +fn range_bound_kind(wit_kind: v4::RangeBoundKind) -> postgres_range::BoundType { + match wit_kind { + v4::RangeBoundKind::Inclusive => postgres_range::BoundType::Inclusive, + v4::RangeBoundKind::Exclusive => postgres_range::BoundType::Exclusive, + } +} + fn infer_columns(row: &Row) -> Vec { let mut result = Vec::with_capacity(row.len()); for index in 0..row.len() { @@ -201,6 +233,9 @@ fn convert_data_type(pg_type: &Type) -> DbDataType { Type::TIME => DbDataType::Time, Type::UUID => DbDataType::Uuid, Type::JSONB => DbDataType::Jsonb, + Type::NUMERIC => DbDataType::Decimal, + Type::INT4_RANGE => DbDataType::Range32, + Type::INT8_RANGE => DbDataType::Range64, _ => { tracing::debug!("Couldn't convert Postgres type {} to WIT", pg_type.name(),); DbDataType::Other @@ -312,6 +347,35 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { None => DbValue::DbNull, } } + &Type::NUMERIC => { + let value: Option = row.try_get(index)?; + match value { + Some(v) => DbValue::Decimal(v.to_string()), + None => DbValue::DbNull, + } + } + &Type::INT4_RANGE => { + let value: Option> = row.try_get(index)?; + match value { + Some(v) => { + let lower = v.lower().map(tuplify_range_bound); + let upper = v.lower().map(tuplify_range_bound); + DbValue::Range32((lower, upper)) + } + None => DbValue::DbNull, + } + } + &Type::INT8_RANGE => { + let value: Option> = row.try_get(index)?; + match value { + Some(v) => { + let lower = v.lower().map(tuplify_range_bound); + let upper = v.lower().map(tuplify_range_bound); + DbValue::Range64((lower, upper)) + } + None => DbValue::DbNull, + } + } t => { tracing::debug!( "Couldn't convert Postgres type {} in column {}", @@ -324,6 +388,19 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { Ok(value) } +fn tuplify_range_bound( + value: &postgres_range::RangeBound, +) -> (T, v4::RangeBoundKind) { + (value.value, wit_bound_kind(value.type_)) +} + +fn wit_bound_kind(bound_type: postgres_range::BoundType) -> v4::RangeBoundKind { + match bound_type { + postgres_range::BoundType::Inclusive => v4::RangeBoundKind::Inclusive, + postgres_range::BoundType::Exclusive => v4::RangeBoundKind::Exclusive, + } +} + // Functions to convert from the chrono types to the WIT interface tuples fn tuplify_date_time( value: chrono::NaiveDateTime, diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 37040473d4..75ecf86a2c 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -2,6 +2,8 @@ use super::*; mod rdbms_types { use super::*; + use spin::postgres3_0_0::postgres as pg3; + use spin::postgres4_0_0::postgres as pg4; impl From for v1::rdbms_types::Column { fn from(value: v2::rdbms_types::Column) -> Self { @@ -12,7 +14,7 @@ mod rdbms_types { } } - impl From for v1::rdbms_types::Column { + impl From for v1::rdbms_types::Column { fn from(value: spin::postgres4_0_0::postgres::Column) -> Self { v1::rdbms_types::Column { name: value.name, @@ -21,8 +23,8 @@ mod rdbms_types { } } - impl From for v2::rdbms_types::Column { - fn from(value: spin::postgres4_0_0::postgres::Column) -> Self { + impl From for v2::rdbms_types::Column { + fn from(value: pg4::Column) -> Self { v2::rdbms_types::Column { name: value.name, data_type: value.data_type.into(), @@ -30,9 +32,9 @@ mod rdbms_types { } } - impl From for spin::postgres3_0_0::postgres::Column { - fn from(value: spin::postgres4_0_0::postgres::Column) -> Self { - spin::postgres3_0_0::postgres::Column { + impl From for pg3::Column { + fn from(value: pg4::Column) -> Self { + pg3::Column { name: value.name, data_type: value.data_type.into(), } @@ -61,265 +63,129 @@ mod rdbms_types { } } - impl From for v1::rdbms_types::DbValue { - fn from(value: spin::postgres4_0_0::postgres::DbValue) -> v1::rdbms_types::DbValue { + impl From for v1::rdbms_types::DbValue { + fn from(value: pg4::DbValue) -> v1::rdbms_types::DbValue { match value { - spin::postgres4_0_0::postgres::DbValue::Boolean(b) => { - v1::rdbms_types::DbValue::Boolean(b) - } - spin::postgres4_0_0::postgres::DbValue::Int8(i) => { - v1::rdbms_types::DbValue::Int8(i) - } - spin::postgres4_0_0::postgres::DbValue::Int16(i) => { - v1::rdbms_types::DbValue::Int16(i) - } - spin::postgres4_0_0::postgres::DbValue::Int32(i) => { - v1::rdbms_types::DbValue::Int32(i) - } - spin::postgres4_0_0::postgres::DbValue::Int64(i) => { - v1::rdbms_types::DbValue::Int64(i) - } - spin::postgres4_0_0::postgres::DbValue::Floating32(r) => { - v1::rdbms_types::DbValue::Floating32(r) - } - spin::postgres4_0_0::postgres::DbValue::Floating64(r) => { - v1::rdbms_types::DbValue::Floating64(r) - } - spin::postgres4_0_0::postgres::DbValue::Str(s) => v1::rdbms_types::DbValue::Str(s), - spin::postgres4_0_0::postgres::DbValue::Binary(b) => { - v1::rdbms_types::DbValue::Binary(b) - } - spin::postgres4_0_0::postgres::DbValue::DbNull => v1::rdbms_types::DbValue::DbNull, - spin::postgres4_0_0::postgres::DbValue::Unsupported => { - v1::rdbms_types::DbValue::Unsupported - } + pg4::DbValue::Boolean(b) => v1::rdbms_types::DbValue::Boolean(b), + pg4::DbValue::Int8(i) => v1::rdbms_types::DbValue::Int8(i), + pg4::DbValue::Int16(i) => v1::rdbms_types::DbValue::Int16(i), + pg4::DbValue::Int32(i) => v1::rdbms_types::DbValue::Int32(i), + pg4::DbValue::Int64(i) => v1::rdbms_types::DbValue::Int64(i), + pg4::DbValue::Floating32(r) => v1::rdbms_types::DbValue::Floating32(r), + pg4::DbValue::Floating64(r) => v1::rdbms_types::DbValue::Floating64(r), + pg4::DbValue::Str(s) => v1::rdbms_types::DbValue::Str(s), + pg4::DbValue::Binary(b) => v1::rdbms_types::DbValue::Binary(b), + pg4::DbValue::DbNull => v1::rdbms_types::DbValue::DbNull, + pg4::DbValue::Unsupported => v1::rdbms_types::DbValue::Unsupported, _ => v1::rdbms_types::DbValue::Unsupported, } } } - impl From for v2::rdbms_types::DbValue { - fn from(value: spin::postgres4_0_0::postgres::DbValue) -> v2::rdbms_types::DbValue { + impl From for v2::rdbms_types::DbValue { + fn from(value: pg4::DbValue) -> v2::rdbms_types::DbValue { match value { - spin::postgres4_0_0::postgres::DbValue::Boolean(b) => { - v2::rdbms_types::DbValue::Boolean(b) - } - spin::postgres4_0_0::postgres::DbValue::Int8(i) => { - v2::rdbms_types::DbValue::Int8(i) - } - spin::postgres4_0_0::postgres::DbValue::Int16(i) => { - v2::rdbms_types::DbValue::Int16(i) - } - spin::postgres4_0_0::postgres::DbValue::Int32(i) => { - v2::rdbms_types::DbValue::Int32(i) - } - spin::postgres4_0_0::postgres::DbValue::Int64(i) => { - v2::rdbms_types::DbValue::Int64(i) - } - spin::postgres4_0_0::postgres::DbValue::Floating32(r) => { - v2::rdbms_types::DbValue::Floating32(r) - } - spin::postgres4_0_0::postgres::DbValue::Floating64(r) => { - v2::rdbms_types::DbValue::Floating64(r) - } - spin::postgres4_0_0::postgres::DbValue::Str(s) => v2::rdbms_types::DbValue::Str(s), - spin::postgres4_0_0::postgres::DbValue::Binary(b) => { - v2::rdbms_types::DbValue::Binary(b) - } - spin::postgres4_0_0::postgres::DbValue::DbNull => v2::rdbms_types::DbValue::DbNull, - spin::postgres4_0_0::postgres::DbValue::Unsupported => { - v2::rdbms_types::DbValue::Unsupported - } + pg4::DbValue::Boolean(b) => v2::rdbms_types::DbValue::Boolean(b), + pg4::DbValue::Int8(i) => v2::rdbms_types::DbValue::Int8(i), + pg4::DbValue::Int16(i) => v2::rdbms_types::DbValue::Int16(i), + pg4::DbValue::Int32(i) => v2::rdbms_types::DbValue::Int32(i), + pg4::DbValue::Int64(i) => v2::rdbms_types::DbValue::Int64(i), + pg4::DbValue::Floating32(r) => v2::rdbms_types::DbValue::Floating32(r), + pg4::DbValue::Floating64(r) => v2::rdbms_types::DbValue::Floating64(r), + pg4::DbValue::Str(s) => v2::rdbms_types::DbValue::Str(s), + pg4::DbValue::Binary(b) => v2::rdbms_types::DbValue::Binary(b), + pg4::DbValue::DbNull => v2::rdbms_types::DbValue::DbNull, + pg4::DbValue::Unsupported => v2::rdbms_types::DbValue::Unsupported, _ => v2::rdbms_types::DbValue::Unsupported, } } } - impl From for spin::postgres3_0_0::postgres::DbValue { - fn from( - value: spin::postgres4_0_0::postgres::DbValue, - ) -> spin::postgres3_0_0::postgres::DbValue { + impl From for pg3::DbValue { + fn from(value: pg4::DbValue) -> pg3::DbValue { match value { - spin::postgres4_0_0::postgres::DbValue::Boolean(b) => { - spin::postgres3_0_0::postgres::DbValue::Boolean(b) - } - spin::postgres4_0_0::postgres::DbValue::Int8(i) => { - spin::postgres3_0_0::postgres::DbValue::Int8(i) - } - spin::postgres4_0_0::postgres::DbValue::Int16(i) => { - spin::postgres3_0_0::postgres::DbValue::Int16(i) - } - spin::postgres4_0_0::postgres::DbValue::Int32(i) => { - spin::postgres3_0_0::postgres::DbValue::Int32(i) - } - spin::postgres4_0_0::postgres::DbValue::Int64(i) => { - spin::postgres3_0_0::postgres::DbValue::Int64(i) - } - spin::postgres4_0_0::postgres::DbValue::Floating32(r) => { - spin::postgres3_0_0::postgres::DbValue::Floating32(r) - } - spin::postgres4_0_0::postgres::DbValue::Floating64(r) => { - spin::postgres3_0_0::postgres::DbValue::Floating64(r) - } - spin::postgres4_0_0::postgres::DbValue::Str(s) => { - spin::postgres3_0_0::postgres::DbValue::Str(s) - } - spin::postgres4_0_0::postgres::DbValue::Binary(b) => { - spin::postgres3_0_0::postgres::DbValue::Binary(b) - } - spin::postgres4_0_0::postgres::DbValue::Date(d) => { - spin::postgres3_0_0::postgres::DbValue::Date(d) - } - spin::postgres4_0_0::postgres::DbValue::Datetime(dt) => { - spin::postgres3_0_0::postgres::DbValue::Datetime(dt) - } - spin::postgres4_0_0::postgres::DbValue::Time(t) => { - spin::postgres3_0_0::postgres::DbValue::Time(t) - } - spin::postgres4_0_0::postgres::DbValue::Timestamp(t) => { - spin::postgres3_0_0::postgres::DbValue::Timestamp(t) - } - spin::postgres4_0_0::postgres::DbValue::Uuid(_) => { - spin::postgres3_0_0::postgres::DbValue::Unsupported - } - spin::postgres4_0_0::postgres::DbValue::Jsonb(_) => { - spin::postgres3_0_0::postgres::DbValue::Unsupported - } - spin::postgres4_0_0::postgres::DbValue::DbNull => { - spin::postgres3_0_0::postgres::DbValue::DbNull - } - spin::postgres4_0_0::postgres::DbValue::Unsupported => { - spin::postgres3_0_0::postgres::DbValue::Unsupported - } + pg4::DbValue::Boolean(b) => pg3::DbValue::Boolean(b), + pg4::DbValue::Int8(i) => pg3::DbValue::Int8(i), + pg4::DbValue::Int16(i) => pg3::DbValue::Int16(i), + pg4::DbValue::Int32(i) => pg3::DbValue::Int32(i), + pg4::DbValue::Int64(i) => pg3::DbValue::Int64(i), + pg4::DbValue::Floating32(r) => pg3::DbValue::Floating32(r), + pg4::DbValue::Floating64(r) => pg3::DbValue::Floating64(r), + pg4::DbValue::Str(s) => pg3::DbValue::Str(s), + pg4::DbValue::Binary(b) => pg3::DbValue::Binary(b), + pg4::DbValue::Date(d) => pg3::DbValue::Date(d), + pg4::DbValue::Datetime(dt) => pg3::DbValue::Datetime(dt), + pg4::DbValue::Time(t) => pg3::DbValue::Time(t), + pg4::DbValue::Timestamp(t) => pg3::DbValue::Timestamp(t), + pg4::DbValue::Uuid(_) => pg3::DbValue::Unsupported, + pg4::DbValue::Jsonb(_) => pg3::DbValue::Unsupported, + pg4::DbValue::Decimal(_) => pg3::DbValue::Unsupported, + pg4::DbValue::Range32(_) => pg3::DbValue::Unsupported, + pg4::DbValue::Range64(_) => pg3::DbValue::Unsupported, + pg4::DbValue::DbNull => pg3::DbValue::DbNull, + pg4::DbValue::Unsupported => pg3::DbValue::Unsupported, } } } - impl From for v1::rdbms_types::DbDataType { - fn from(value: spin::postgres4_0_0::postgres::DbDataType) -> v1::rdbms_types::DbDataType { + impl From for v1::rdbms_types::DbDataType { + fn from(value: pg4::DbDataType) -> v1::rdbms_types::DbDataType { match value { - spin::postgres4_0_0::postgres::DbDataType::Boolean => { - v1::rdbms_types::DbDataType::Boolean - } - spin::postgres4_0_0::postgres::DbDataType::Int8 => { - v1::rdbms_types::DbDataType::Int8 - } - spin::postgres4_0_0::postgres::DbDataType::Int16 => { - v1::rdbms_types::DbDataType::Int16 - } - spin::postgres4_0_0::postgres::DbDataType::Int32 => { - v1::rdbms_types::DbDataType::Int32 - } - spin::postgres4_0_0::postgres::DbDataType::Int64 => { - v1::rdbms_types::DbDataType::Int64 - } - spin::postgres4_0_0::postgres::DbDataType::Floating32 => { - v1::rdbms_types::DbDataType::Floating32 - } - spin::postgres4_0_0::postgres::DbDataType::Floating64 => { - v1::rdbms_types::DbDataType::Floating64 - } - spin::postgres4_0_0::postgres::DbDataType::Str => v1::rdbms_types::DbDataType::Str, - spin::postgres4_0_0::postgres::DbDataType::Binary => { - v1::rdbms_types::DbDataType::Binary - } - spin::postgres4_0_0::postgres::DbDataType::Other => { - v1::rdbms_types::DbDataType::Other - } + pg4::DbDataType::Boolean => v1::rdbms_types::DbDataType::Boolean, + pg4::DbDataType::Int8 => v1::rdbms_types::DbDataType::Int8, + pg4::DbDataType::Int16 => v1::rdbms_types::DbDataType::Int16, + pg4::DbDataType::Int32 => v1::rdbms_types::DbDataType::Int32, + pg4::DbDataType::Int64 => v1::rdbms_types::DbDataType::Int64, + pg4::DbDataType::Floating32 => v1::rdbms_types::DbDataType::Floating32, + pg4::DbDataType::Floating64 => v1::rdbms_types::DbDataType::Floating64, + pg4::DbDataType::Str => v1::rdbms_types::DbDataType::Str, + pg4::DbDataType::Binary => v1::rdbms_types::DbDataType::Binary, + pg4::DbDataType::Other => v1::rdbms_types::DbDataType::Other, _ => v1::rdbms_types::DbDataType::Other, } } } - impl From for v2::rdbms_types::DbDataType { - fn from(value: spin::postgres4_0_0::postgres::DbDataType) -> v2::rdbms_types::DbDataType { + impl From for v2::rdbms_types::DbDataType { + fn from(value: pg4::DbDataType) -> v2::rdbms_types::DbDataType { match value { - spin::postgres4_0_0::postgres::DbDataType::Boolean => { - v2::rdbms_types::DbDataType::Boolean - } - spin::postgres4_0_0::postgres::DbDataType::Int8 => { - v2::rdbms_types::DbDataType::Int8 - } - spin::postgres4_0_0::postgres::DbDataType::Int16 => { - v2::rdbms_types::DbDataType::Int16 - } - spin::postgres4_0_0::postgres::DbDataType::Int32 => { - v2::rdbms_types::DbDataType::Int32 - } - spin::postgres4_0_0::postgres::DbDataType::Int64 => { - v2::rdbms_types::DbDataType::Int64 - } - spin::postgres4_0_0::postgres::DbDataType::Floating32 => { - v2::rdbms_types::DbDataType::Floating32 - } - spin::postgres4_0_0::postgres::DbDataType::Floating64 => { - v2::rdbms_types::DbDataType::Floating64 - } - spin::postgres4_0_0::postgres::DbDataType::Str => v2::rdbms_types::DbDataType::Str, - spin::postgres4_0_0::postgres::DbDataType::Binary => { - v2::rdbms_types::DbDataType::Binary - } - spin::postgres4_0_0::postgres::DbDataType::Other => { - v2::rdbms_types::DbDataType::Other - } + pg4::DbDataType::Boolean => v2::rdbms_types::DbDataType::Boolean, + pg4::DbDataType::Int8 => v2::rdbms_types::DbDataType::Int8, + pg4::DbDataType::Int16 => v2::rdbms_types::DbDataType::Int16, + pg4::DbDataType::Int32 => v2::rdbms_types::DbDataType::Int32, + pg4::DbDataType::Int64 => v2::rdbms_types::DbDataType::Int64, + pg4::DbDataType::Floating32 => v2::rdbms_types::DbDataType::Floating32, + pg4::DbDataType::Floating64 => v2::rdbms_types::DbDataType::Floating64, + pg4::DbDataType::Str => v2::rdbms_types::DbDataType::Str, + pg4::DbDataType::Binary => v2::rdbms_types::DbDataType::Binary, + pg4::DbDataType::Other => v2::rdbms_types::DbDataType::Other, _ => v2::rdbms_types::DbDataType::Other, } } } - impl From for spin::postgres3_0_0::postgres::DbDataType { - fn from( - value: spin::postgres4_0_0::postgres::DbDataType, - ) -> spin::postgres3_0_0::postgres::DbDataType { + impl From for pg3::DbDataType { + fn from(value: pg4::DbDataType) -> pg3::DbDataType { match value { - spin::postgres4_0_0::postgres::DbDataType::Boolean => { - spin::postgres3_0_0::postgres::DbDataType::Boolean - } - spin::postgres4_0_0::postgres::DbDataType::Int8 => { - spin::postgres3_0_0::postgres::DbDataType::Int8 - } - spin::postgres4_0_0::postgres::DbDataType::Int16 => { - spin::postgres3_0_0::postgres::DbDataType::Int16 - } - spin::postgres4_0_0::postgres::DbDataType::Int32 => { - spin::postgres3_0_0::postgres::DbDataType::Int32 - } - spin::postgres4_0_0::postgres::DbDataType::Int64 => { - spin::postgres3_0_0::postgres::DbDataType::Int64 - } - spin::postgres4_0_0::postgres::DbDataType::Floating32 => { - spin::postgres3_0_0::postgres::DbDataType::Floating32 - } - spin::postgres4_0_0::postgres::DbDataType::Floating64 => { - spin::postgres3_0_0::postgres::DbDataType::Floating64 - } - spin::postgres4_0_0::postgres::DbDataType::Str => { - spin::postgres3_0_0::postgres::DbDataType::Str - } - spin::postgres4_0_0::postgres::DbDataType::Binary => { - spin::postgres3_0_0::postgres::DbDataType::Binary - } - spin::postgres4_0_0::postgres::DbDataType::Date => { - spin::postgres3_0_0::postgres::DbDataType::Date - } - spin::postgres4_0_0::postgres::DbDataType::Datetime => { - spin::postgres3_0_0::postgres::DbDataType::Datetime - } - spin::postgres4_0_0::postgres::DbDataType::Time => { - spin::postgres3_0_0::postgres::DbDataType::Time - } - spin::postgres4_0_0::postgres::DbDataType::Timestamp => { - spin::postgres3_0_0::postgres::DbDataType::Timestamp - } - spin::postgres4_0_0::postgres::DbDataType::Uuid => { - spin::postgres3_0_0::postgres::DbDataType::Other - } - spin::postgres4_0_0::postgres::DbDataType::Jsonb => { - spin::postgres3_0_0::postgres::DbDataType::Other - } - spin::postgres4_0_0::postgres::DbDataType::Other => { - spin::postgres3_0_0::postgres::DbDataType::Other - } + pg4::DbDataType::Boolean => pg3::DbDataType::Boolean, + pg4::DbDataType::Int8 => pg3::DbDataType::Int8, + pg4::DbDataType::Int16 => pg3::DbDataType::Int16, + pg4::DbDataType::Int32 => pg3::DbDataType::Int32, + pg4::DbDataType::Int64 => pg3::DbDataType::Int64, + pg4::DbDataType::Floating32 => pg3::DbDataType::Floating32, + pg4::DbDataType::Floating64 => pg3::DbDataType::Floating64, + pg4::DbDataType::Str => pg3::DbDataType::Str, + pg4::DbDataType::Binary => pg3::DbDataType::Binary, + pg4::DbDataType::Date => pg3::DbDataType::Date, + pg4::DbDataType::Datetime => pg3::DbDataType::Datetime, + pg4::DbDataType::Time => pg3::DbDataType::Time, + pg4::DbDataType::Timestamp => pg3::DbDataType::Timestamp, + pg4::DbDataType::Uuid => pg3::DbDataType::Other, + pg4::DbDataType::Jsonb => pg3::DbDataType::Other, + pg4::DbDataType::Decimal => pg3::DbDataType::Other, + pg4::DbDataType::Range32 => pg3::DbDataType::Other, + pg4::DbDataType::Range64 => pg3::DbDataType::Other, + pg4::DbDataType::Other => pg3::DbDataType::Other, } } } @@ -390,28 +256,18 @@ mod rdbms_types { } } - impl TryFrom for spin::postgres4_0_0::postgres::ParameterValue { + impl TryFrom for pg4::ParameterValue { type Error = v1::postgres::PgError; fn try_from( value: v1::rdbms_types::ParameterValue, - ) -> Result { + ) -> Result { let converted = match value { - v1::rdbms_types::ParameterValue::Boolean(b) => { - spin::postgres4_0_0::postgres::ParameterValue::Boolean(b) - } - v1::rdbms_types::ParameterValue::Int8(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int8(i) - } - v1::rdbms_types::ParameterValue::Int16(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int16(i) - } - v1::rdbms_types::ParameterValue::Int32(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int32(i) - } - v1::rdbms_types::ParameterValue::Int64(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int64(i) - } + v1::rdbms_types::ParameterValue::Boolean(b) => pg4::ParameterValue::Boolean(b), + v1::rdbms_types::ParameterValue::Int8(i) => pg4::ParameterValue::Int8(i), + v1::rdbms_types::ParameterValue::Int16(i) => pg4::ParameterValue::Int16(i), + v1::rdbms_types::ParameterValue::Int32(i) => pg4::ParameterValue::Int32(i), + v1::rdbms_types::ParameterValue::Int64(i) => pg4::ParameterValue::Int64(i), v1::rdbms_types::ParameterValue::Uint8(_) | v1::rdbms_types::ParameterValue::Uint16(_) | v1::rdbms_types::ParameterValue::Uint32(_) @@ -421,47 +277,31 @@ mod rdbms_types { )); } v1::rdbms_types::ParameterValue::Floating32(r) => { - spin::postgres4_0_0::postgres::ParameterValue::Floating32(r) + pg4::ParameterValue::Floating32(r) } v1::rdbms_types::ParameterValue::Floating64(r) => { - spin::postgres4_0_0::postgres::ParameterValue::Floating64(r) - } - v1::rdbms_types::ParameterValue::Str(s) => { - spin::postgres4_0_0::postgres::ParameterValue::Str(s) - } - v1::rdbms_types::ParameterValue::Binary(b) => { - spin::postgres4_0_0::postgres::ParameterValue::Binary(b) - } - v1::rdbms_types::ParameterValue::DbNull => { - spin::postgres4_0_0::postgres::ParameterValue::DbNull + pg4::ParameterValue::Floating64(r) } + v1::rdbms_types::ParameterValue::Str(s) => pg4::ParameterValue::Str(s), + v1::rdbms_types::ParameterValue::Binary(b) => pg4::ParameterValue::Binary(b), + v1::rdbms_types::ParameterValue::DbNull => pg4::ParameterValue::DbNull, }; Ok(converted) } } - impl TryFrom for spin::postgres4_0_0::postgres::ParameterValue { + impl TryFrom for pg4::ParameterValue { type Error = v2::rdbms_types::Error; fn try_from( value: v2::rdbms_types::ParameterValue, - ) -> Result { + ) -> Result { let converted = match value { - v2::rdbms_types::ParameterValue::Boolean(b) => { - spin::postgres4_0_0::postgres::ParameterValue::Boolean(b) - } - v2::rdbms_types::ParameterValue::Int8(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int8(i) - } - v2::rdbms_types::ParameterValue::Int16(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int16(i) - } - v2::rdbms_types::ParameterValue::Int32(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int32(i) - } - v2::rdbms_types::ParameterValue::Int64(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int64(i) - } + v2::rdbms_types::ParameterValue::Boolean(b) => pg4::ParameterValue::Boolean(b), + v2::rdbms_types::ParameterValue::Int8(i) => pg4::ParameterValue::Int8(i), + v2::rdbms_types::ParameterValue::Int16(i) => pg4::ParameterValue::Int16(i), + v2::rdbms_types::ParameterValue::Int32(i) => pg4::ParameterValue::Int32(i), + v2::rdbms_types::ParameterValue::Int64(i) => pg4::ParameterValue::Int64(i), v2::rdbms_types::ParameterValue::Uint8(_) | v2::rdbms_types::ParameterValue::Uint16(_) | v2::rdbms_types::ParameterValue::Uint32(_) @@ -471,74 +311,36 @@ mod rdbms_types { )); } v2::rdbms_types::ParameterValue::Floating32(r) => { - spin::postgres4_0_0::postgres::ParameterValue::Floating32(r) + pg4::ParameterValue::Floating32(r) } v2::rdbms_types::ParameterValue::Floating64(r) => { - spin::postgres4_0_0::postgres::ParameterValue::Floating64(r) - } - v2::rdbms_types::ParameterValue::Str(s) => { - spin::postgres4_0_0::postgres::ParameterValue::Str(s) - } - v2::rdbms_types::ParameterValue::Binary(b) => { - spin::postgres4_0_0::postgres::ParameterValue::Binary(b) - } - v2::rdbms_types::ParameterValue::DbNull => { - spin::postgres4_0_0::postgres::ParameterValue::DbNull + pg4::ParameterValue::Floating64(r) } + v2::rdbms_types::ParameterValue::Str(s) => pg4::ParameterValue::Str(s), + v2::rdbms_types::ParameterValue::Binary(b) => pg4::ParameterValue::Binary(b), + v2::rdbms_types::ParameterValue::DbNull => pg4::ParameterValue::DbNull, }; Ok(converted) } } - impl From - for spin::postgres4_0_0::postgres::ParameterValue - { - fn from( - value: spin::postgres3_0_0::postgres::ParameterValue, - ) -> spin::postgres4_0_0::postgres::ParameterValue { + impl From for pg4::ParameterValue { + fn from(value: pg3::ParameterValue) -> pg4::ParameterValue { match value { - spin::postgres3_0_0::postgres::ParameterValue::Boolean(b) => { - spin::postgres4_0_0::postgres::ParameterValue::Boolean(b) - } - spin::postgres3_0_0::postgres::ParameterValue::Int8(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int8(i) - } - spin::postgres3_0_0::postgres::ParameterValue::Int16(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int16(i) - } - spin::postgres3_0_0::postgres::ParameterValue::Int32(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int32(i) - } - spin::postgres3_0_0::postgres::ParameterValue::Int64(i) => { - spin::postgres4_0_0::postgres::ParameterValue::Int64(i) - } - spin::postgres3_0_0::postgres::ParameterValue::Floating32(r) => { - spin::postgres4_0_0::postgres::ParameterValue::Floating32(r) - } - spin::postgres3_0_0::postgres::ParameterValue::Floating64(r) => { - spin::postgres4_0_0::postgres::ParameterValue::Floating64(r) - } - spin::postgres3_0_0::postgres::ParameterValue::Str(s) => { - spin::postgres4_0_0::postgres::ParameterValue::Str(s) - } - spin::postgres3_0_0::postgres::ParameterValue::Binary(b) => { - spin::postgres4_0_0::postgres::ParameterValue::Binary(b) - } - spin::postgres3_0_0::postgres::ParameterValue::Date(d) => { - spin::postgres4_0_0::postgres::ParameterValue::Date(d) - } - spin::postgres3_0_0::postgres::ParameterValue::Datetime(dt) => { - spin::postgres4_0_0::postgres::ParameterValue::Datetime(dt) - } - spin::postgres3_0_0::postgres::ParameterValue::Time(t) => { - spin::postgres4_0_0::postgres::ParameterValue::Time(t) - } - spin::postgres3_0_0::postgres::ParameterValue::Timestamp(t) => { - spin::postgres4_0_0::postgres::ParameterValue::Timestamp(t) - } - spin::postgres3_0_0::postgres::ParameterValue::DbNull => { - spin::postgres4_0_0::postgres::ParameterValue::DbNull - } + pg3::ParameterValue::Boolean(b) => pg4::ParameterValue::Boolean(b), + pg3::ParameterValue::Int8(i) => pg4::ParameterValue::Int8(i), + pg3::ParameterValue::Int16(i) => pg4::ParameterValue::Int16(i), + pg3::ParameterValue::Int32(i) => pg4::ParameterValue::Int32(i), + pg3::ParameterValue::Int64(i) => pg4::ParameterValue::Int64(i), + pg3::ParameterValue::Floating32(r) => pg4::ParameterValue::Floating32(r), + pg3::ParameterValue::Floating64(r) => pg4::ParameterValue::Floating64(r), + pg3::ParameterValue::Str(s) => pg4::ParameterValue::Str(s), + pg3::ParameterValue::Binary(b) => pg4::ParameterValue::Binary(b), + pg3::ParameterValue::Date(d) => pg4::ParameterValue::Date(d), + pg3::ParameterValue::Datetime(dt) => pg4::ParameterValue::Datetime(dt), + pg3::ParameterValue::Time(t) => pg4::ParameterValue::Time(t), + pg3::ParameterValue::Timestamp(t) => pg4::ParameterValue::Timestamp(t), + pg3::ParameterValue::DbNull => pg4::ParameterValue::DbNull, } } } @@ -557,68 +359,42 @@ mod rdbms_types { } } - impl From for v1::postgres::PgError { - fn from(error: spin::postgres4_0_0::postgres::Error) -> v1::postgres::PgError { + impl From for v1::postgres::PgError { + fn from(error: pg4::Error) -> v1::postgres::PgError { match error { - spin::postgres4_0_0::postgres::Error::ConnectionFailed(e) => { - v1::postgres::PgError::ConnectionFailed(e) - } - spin::postgres4_0_0::postgres::Error::BadParameter(e) => { - v1::postgres::PgError::BadParameter(e) - } - spin::postgres4_0_0::postgres::Error::QueryFailed(e) => { - v1::postgres::PgError::QueryFailed(e) - } - spin::postgres4_0_0::postgres::Error::ValueConversionFailed(e) => { + pg4::Error::ConnectionFailed(e) => v1::postgres::PgError::ConnectionFailed(e), + pg4::Error::BadParameter(e) => v1::postgres::PgError::BadParameter(e), + pg4::Error::QueryFailed(e) => v1::postgres::PgError::QueryFailed(e), + pg4::Error::ValueConversionFailed(e) => { v1::postgres::PgError::ValueConversionFailed(e) } - spin::postgres4_0_0::postgres::Error::Other(e) => { - v1::postgres::PgError::OtherError(e) - } + pg4::Error::Other(e) => v1::postgres::PgError::OtherError(e), } } } - impl From for v2::rdbms_types::Error { - fn from(error: spin::postgres4_0_0::postgres::Error) -> v2::rdbms_types::Error { + impl From for v2::rdbms_types::Error { + fn from(error: pg4::Error) -> v2::rdbms_types::Error { match error { - spin::postgres4_0_0::postgres::Error::ConnectionFailed(e) => { - v2::rdbms_types::Error::ConnectionFailed(e) - } - spin::postgres4_0_0::postgres::Error::BadParameter(e) => { - v2::rdbms_types::Error::BadParameter(e) - } - spin::postgres4_0_0::postgres::Error::QueryFailed(e) => { - v2::rdbms_types::Error::QueryFailed(e) - } - spin::postgres4_0_0::postgres::Error::ValueConversionFailed(e) => { + pg4::Error::ConnectionFailed(e) => v2::rdbms_types::Error::ConnectionFailed(e), + pg4::Error::BadParameter(e) => v2::rdbms_types::Error::BadParameter(e), + pg4::Error::QueryFailed(e) => v2::rdbms_types::Error::QueryFailed(e), + pg4::Error::ValueConversionFailed(e) => { v2::rdbms_types::Error::ValueConversionFailed(e) } - spin::postgres4_0_0::postgres::Error::Other(e) => v2::rdbms_types::Error::Other(e), + pg4::Error::Other(e) => v2::rdbms_types::Error::Other(e), } } } - impl From for spin::postgres3_0_0::postgres::Error { - fn from( - error: spin::postgres4_0_0::postgres::Error, - ) -> spin::postgres3_0_0::postgres::Error { + impl From for pg3::Error { + fn from(error: pg4::Error) -> pg3::Error { match error { - spin::postgres4_0_0::postgres::Error::ConnectionFailed(e) => { - spin::postgres3_0_0::postgres::Error::ConnectionFailed(e) - } - spin::postgres4_0_0::postgres::Error::BadParameter(e) => { - spin::postgres3_0_0::postgres::Error::BadParameter(e) - } - spin::postgres4_0_0::postgres::Error::QueryFailed(e) => { - spin::postgres3_0_0::postgres::Error::QueryFailed(e) - } - spin::postgres4_0_0::postgres::Error::ValueConversionFailed(e) => { - spin::postgres3_0_0::postgres::Error::ValueConversionFailed(e) - } - spin::postgres4_0_0::postgres::Error::Other(e) => { - spin::postgres3_0_0::postgres::Error::Other(e) - } + pg4::Error::ConnectionFailed(e) => pg3::Error::ConnectionFailed(e), + pg4::Error::BadParameter(e) => pg3::Error::BadParameter(e), + pg4::Error::QueryFailed(e) => pg3::Error::QueryFailed(e), + pg4::Error::ValueConversionFailed(e) => pg3::Error::ValueConversionFailed(e), + pg4::Error::Other(e) => pg3::Error::Other(e), } } } @@ -626,9 +402,11 @@ mod rdbms_types { mod postgres { use super::*; + use spin::postgres3_0_0::postgres as pg3; + use spin::postgres4_0_0::postgres as pg4; - impl From for v1::postgres::RowSet { - fn from(value: spin::postgres4_0_0::postgres::RowSet) -> v1::postgres::RowSet { + impl From for v1::postgres::RowSet { + fn from(value: pg4::RowSet) -> v1::postgres::RowSet { v1::mysql::RowSet { columns: value.columns.into_iter().map(Into::into).collect(), rows: value @@ -640,8 +418,8 @@ mod postgres { } } - impl From for v2::rdbms_types::RowSet { - fn from(value: spin::postgres4_0_0::postgres::RowSet) -> v2::rdbms_types::RowSet { + impl From for v2::rdbms_types::RowSet { + fn from(value: pg4::RowSet) -> v2::rdbms_types::RowSet { v2::rdbms_types::RowSet { columns: value.columns.into_iter().map(Into::into).collect(), rows: value @@ -653,11 +431,9 @@ mod postgres { } } - impl From for spin::postgres3_0_0::postgres::RowSet { - fn from( - value: spin::postgres4_0_0::postgres::RowSet, - ) -> spin::postgres3_0_0::postgres::RowSet { - spin::postgres3_0_0::postgres::RowSet { + impl From for pg3::RowSet { + fn from(value: pg4::RowSet) -> pg3::RowSet { + pg3::RowSet { columns: value.columns.into_iter().map(Into::into).collect(), rows: value .rows diff --git a/wit/deps/spin-postgres@4.0.0/postgres.wit b/wit/deps/spin-postgres@4.0.0/postgres.wit index 1522dbfb9e..a174fc3e7e 100644 --- a/wit/deps/spin-postgres@4.0.0/postgres.wit +++ b/wit/deps/spin-postgres@4.0.0/postgres.wit @@ -27,6 +27,9 @@ interface postgres { timestamp, uuid, jsonb, + decimal, + range32, + range64, other, } @@ -50,6 +53,9 @@ interface postgres { timestamp(s64), uuid(string), jsonb(list), + decimal(string), // I admit defeat. Base 10 + range32(tuple>, option>>), + range64(tuple>, option>>), db-null, unsupported, } @@ -74,6 +80,9 @@ interface postgres { timestamp(s64), uuid(string), jsonb(list), + decimal(string), // base 10 + range32(tuple>, option>>), + range64(tuple>, option>>), db-null, } @@ -92,6 +101,12 @@ interface postgres { rows: list, } + /// For range types, indicates if each bound is inclusive or exclusive + enum range-bound-kind { + inclusive, + exclusive, + } + /// A connection to a postgres database. resource connection { /// Open a connection to the Postgres instance at `address`. From 50bde9df7b12bca47bf52bdc1147cf7c4fc58e8f Mon Sep 17 00:00:00 2001 From: itowlson Date: Wed, 25 Jun 2025 15:02:22 +1200 Subject: [PATCH 3/9] Range and array types Signed-off-by: itowlson --- crates/factor-outbound-pg/src/client.rs | 31 +++++++++++++++++++++-- crates/world/src/conversions.rs | 6 +++++ wit/deps/spin-postgres@4.0.0/postgres.wit | 9 +++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index 4ea509dd70..076a813494 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -192,6 +192,9 @@ fn to_sql_parameter(value: &ParameterValue) -> Result Ok(Box::new(vs.to_owned())), + ParameterValue::ArrayInt64(vs) => Ok(Box::new(vs.to_owned())), + ParameterValue::ArrayStr(vs) => Ok(Box::new(vs.to_owned())), ParameterValue::DbNull => Ok(Box::new(PgNull)), } } @@ -236,6 +239,9 @@ fn convert_data_type(pg_type: &Type) -> DbDataType { Type::NUMERIC => DbDataType::Decimal, Type::INT4_RANGE => DbDataType::Range32, Type::INT8_RANGE => DbDataType::Range64, + Type::INT4_ARRAY => DbDataType::ArrayInt32, + Type::INT8_ARRAY => DbDataType::ArrayInt64, + Type::TEXT_ARRAY | Type::VARCHAR_ARRAY | Type::BPCHAR_ARRAY => DbDataType::ArrayStr, _ => { tracing::debug!("Couldn't convert Postgres type {} to WIT", pg_type.name(),); DbDataType::Other @@ -359,7 +365,7 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { match value { Some(v) => { let lower = v.lower().map(tuplify_range_bound); - let upper = v.lower().map(tuplify_range_bound); + let upper = v.upper().map(tuplify_range_bound); DbValue::Range32((lower, upper)) } None => DbValue::DbNull, @@ -370,12 +376,33 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { match value { Some(v) => { let lower = v.lower().map(tuplify_range_bound); - let upper = v.lower().map(tuplify_range_bound); + let upper = v.upper().map(tuplify_range_bound); DbValue::Range64((lower, upper)) } None => DbValue::DbNull, } } + &Type::INT4_ARRAY => { + let value: Option>> = row.try_get(index)?; + match value { + Some(v) => DbValue::ArrayInt32(v), + None => DbValue::DbNull, + } + } + &Type::INT8_ARRAY => { + let value: Option>> = row.try_get(index)?; + match value { + Some(v) => DbValue::ArrayInt64(v), + None => DbValue::DbNull, + } + } + &Type::TEXT_ARRAY | &Type::VARCHAR_ARRAY | &Type::BPCHAR_ARRAY => { + let value: Option>> = row.try_get(index)?; + match value { + Some(v) => DbValue::ArrayStr(v), + None => DbValue::DbNull, + } + } t => { tracing::debug!( "Couldn't convert Postgres type {} in column {}", diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 75ecf86a2c..f00bbf45be 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -122,6 +122,9 @@ mod rdbms_types { pg4::DbValue::Decimal(_) => pg3::DbValue::Unsupported, pg4::DbValue::Range32(_) => pg3::DbValue::Unsupported, pg4::DbValue::Range64(_) => pg3::DbValue::Unsupported, + pg4::DbValue::ArrayInt32(_) => pg3::DbValue::Unsupported, + pg4::DbValue::ArrayInt64(_) => pg3::DbValue::Unsupported, + pg4::DbValue::ArrayStr(_) => pg3::DbValue::Unsupported, pg4::DbValue::DbNull => pg3::DbValue::DbNull, pg4::DbValue::Unsupported => pg3::DbValue::Unsupported, } @@ -185,6 +188,9 @@ mod rdbms_types { pg4::DbDataType::Decimal => pg3::DbDataType::Other, pg4::DbDataType::Range32 => pg3::DbDataType::Other, pg4::DbDataType::Range64 => pg3::DbDataType::Other, + pg4::DbDataType::ArrayInt32 => pg3::DbDataType::Other, + pg4::DbDataType::ArrayInt64 => pg3::DbDataType::Other, + pg4::DbDataType::ArrayStr => pg3::DbDataType::Other, pg4::DbDataType::Other => pg3::DbDataType::Other, } } diff --git a/wit/deps/spin-postgres@4.0.0/postgres.wit b/wit/deps/spin-postgres@4.0.0/postgres.wit index a174fc3e7e..a44c52cd32 100644 --- a/wit/deps/spin-postgres@4.0.0/postgres.wit +++ b/wit/deps/spin-postgres@4.0.0/postgres.wit @@ -30,6 +30,9 @@ interface postgres { decimal, range32, range64, + array-int32, + array-int64, + array-str, other, } @@ -56,6 +59,9 @@ interface postgres { decimal(string), // I admit defeat. Base 10 range32(tuple>, option>>), range64(tuple>, option>>), + array-int32(list>), + array-int64(list>), + array-str(list>), db-null, unsupported, } @@ -83,6 +89,9 @@ interface postgres { decimal(string), // base 10 range32(tuple>, option>>), range64(tuple>, option>>), + array-int32(list>), + array-int64(list>), + array-str(list>), db-null, } From 47d3dc8aa9cb3cb5c902ac3fab0d1c31a7abfdd5 Mon Sep 17 00:00:00 2001 From: itowlson Date: Thu, 26 Jun 2025 11:08:23 +1200 Subject: [PATCH 4/9] PostgreSQL interval type Signed-off-by: itowlson --- Cargo.lock | 1 + crates/factor-outbound-pg/Cargo.toml | 1 + crates/factor-outbound-pg/src/client.rs | 87 ++++++++++++++++++++++- crates/world/src/conversions.rs | 2 + wit/deps/spin-postgres@4.0.0/postgres.wit | 9 +++ 5 files changed, 99 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6284a90a58..8cb66f82fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8436,6 +8436,7 @@ name = "spin-factor-outbound-pg" version = "3.4.0-pre0" dependencies = [ "anyhow", + "bytes", "chrono", "native-tls", "postgres-native-tls", diff --git a/crates/factor-outbound-pg/Cargo.toml b/crates/factor-outbound-pg/Cargo.toml index e47c70e084..f375383044 100644 --- a/crates/factor-outbound-pg/Cargo.toml +++ b/crates/factor-outbound-pg/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } [dependencies] anyhow = { workspace = true } +bytes = {workspace = true } chrono = { workspace = true } native-tls = "0.2" postgres-native-tls = "0.5" diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index 076a813494..2becd3fd09 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -5,7 +5,7 @@ use spin_world::async_trait; use spin_world::spin::postgres4_0_0::postgres::{ self as v4, Column, DbDataType, DbValue, ParameterValue, RowSet, }; -use tokio_postgres::types::Type; +use tokio_postgres::types::{FromSql, Type}; use tokio_postgres::{config::SslMode, types::ToSql, Row}; use tokio_postgres::{Client as TokioClient, NoTls, Socket}; @@ -195,6 +195,7 @@ fn to_sql_parameter(value: &ParameterValue) -> Result Ok(Box::new(vs.to_owned())), ParameterValue::ArrayInt64(vs) => Ok(Box::new(vs.to_owned())), ParameterValue::ArrayStr(vs) => Ok(Box::new(vs.to_owned())), + ParameterValue::Interval(v) => Ok(Box::new(Interval(*v))), ParameterValue::DbNull => Ok(Box::new(PgNull)), } } @@ -403,6 +404,13 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { None => DbValue::DbNull, } } + &Type::INTERVAL => { + let value: Option = row.try_get(index)?; + match value { + Some(v) => DbValue::Interval(v.0), + None => DbValue::DbNull, + } + } t => { tracing::debug!( "Couldn't convert Postgres type {} in column {}", @@ -504,3 +512,80 @@ impl std::fmt::Debug for PgNull { f.debug_struct("NULL").finish() } } + +#[derive(Debug)] +struct Interval(v4::Interval); + +impl ToSql for Interval { + tokio_postgres::types::to_sql_checked!(); + + fn to_sql( + &self, + _ty: &Type, + out: &mut tokio_postgres::types::private::BytesMut, + ) -> Result> + where + Self: Sized, + { + use bytes::BufMut; + + out.put_i64(self.0.micros); + out.put_i32(self.0.days); + out.put_i32(self.0.months); + + Ok(tokio_postgres::types::IsNull::No) + } + + fn accepts(ty: &Type) -> bool + where + Self: Sized, + { + matches!(ty, &Type::INTERVAL) + } +} + +impl FromSql<'_> for Interval { + fn from_sql( + _ty: &Type, + raw: &'_ [u8], + ) -> std::result::Result> { + const EXPECTED_LEN: usize = size_of::() + size_of::() + size_of::(); + + if raw.len() != EXPECTED_LEN { + return Err(Box::new(IntervalLengthError)); + } + + let (micro_bytes, rest) = raw.split_at(size_of::()); + let (day_bytes, rest) = rest.split_at(size_of::()); + let month_bytes = rest; + let months = i32::from_be_bytes(month_bytes.try_into().unwrap()); + let days = i32::from_be_bytes(day_bytes.try_into().unwrap()); + let micros = i64::from_be_bytes(micro_bytes.try_into().unwrap()); + + Ok(Self(v4::Interval { + micros, + days, + months, + })) + } + + fn accepts(ty: &Type) -> bool { + matches!(ty, &Type::INTERVAL) + } +} + +struct IntervalLengthError; + +impl std::error::Error for IntervalLengthError {} + +impl std::fmt::Display for IntervalLengthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("unexpected binary format for Postgres INTERVAL") + } +} + +impl std::fmt::Debug for IntervalLengthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index f00bbf45be..23c6daa36a 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -125,6 +125,7 @@ mod rdbms_types { pg4::DbValue::ArrayInt32(_) => pg3::DbValue::Unsupported, pg4::DbValue::ArrayInt64(_) => pg3::DbValue::Unsupported, pg4::DbValue::ArrayStr(_) => pg3::DbValue::Unsupported, + pg4::DbValue::Interval(_) => pg3::DbValue::Unsupported, pg4::DbValue::DbNull => pg3::DbValue::DbNull, pg4::DbValue::Unsupported => pg3::DbValue::Unsupported, } @@ -191,6 +192,7 @@ mod rdbms_types { pg4::DbDataType::ArrayInt32 => pg3::DbDataType::Other, pg4::DbDataType::ArrayInt64 => pg3::DbDataType::Other, pg4::DbDataType::ArrayStr => pg3::DbDataType::Other, + pg4::DbDataType::Interval => pg3::DbDataType::Other, pg4::DbDataType::Other => pg3::DbDataType::Other, } } diff --git a/wit/deps/spin-postgres@4.0.0/postgres.wit b/wit/deps/spin-postgres@4.0.0/postgres.wit index a44c52cd32..0f092425aa 100644 --- a/wit/deps/spin-postgres@4.0.0/postgres.wit +++ b/wit/deps/spin-postgres@4.0.0/postgres.wit @@ -33,6 +33,7 @@ interface postgres { array-int32, array-int64, array-str, + interval, other, } @@ -62,6 +63,7 @@ interface postgres { array-int32(list>), array-int64(list>), array-str(list>), + interval(interval), db-null, unsupported, } @@ -92,9 +94,16 @@ interface postgres { array-int32(list>), array-int64(list>), array-str(list>), + interval(interval), db-null, } + record interval { + micros: s64, + days: s32, + months: s32, + } + /// A database column record column { name: string, From b2b9303eec37cf74e32b6b3f4b5c96bcd446ef14 Mon Sep 17 00:00:00 2001 From: itowlson Date: Thu, 26 Jun 2025 13:34:55 +1200 Subject: [PATCH 5/9] Decimal arrays and ranges Signed-off-by: itowlson --- crates/factor-outbound-pg/src/client.rs | 137 +++++++++++++++++++++- crates/world/src/conversions.rs | 12 +- wit/deps/spin-postgres@4.0.0/postgres.wit | 18 ++- 3 files changed, 151 insertions(+), 16 deletions(-) diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index 2becd3fd09..e6cd714b17 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -172,7 +172,7 @@ fn to_sql_parameter(value: &ParameterValue) -> Result { + ParameterValue::RangeInt32((lower, upper)) => { let lbound = lower.map(|(value, kind)| { postgres_range::RangeBound::new(value, range_bound_kind(kind)) }); @@ -182,7 +182,7 @@ fn to_sql_parameter(value: &ParameterValue) -> Result { + ParameterValue::RangeInt64((lower, upper)) => { let lbound = lower.map(|(value, kind)| { postgres_range::RangeBound::new(value, range_bound_kind(kind)) }); @@ -192,8 +192,48 @@ fn to_sql_parameter(value: &ParameterValue) -> Result { + let lbound = match lower { + None => None, + Some((value, kind)) => { + let dec = rust_decimal::Decimal::from_str_exact(value) + .with_context(|| format!("invalid decimal {value}"))?; + let dec = RangeableDecimal(dec); + Some(postgres_range::RangeBound::new( + dec, + range_bound_kind(*kind), + )) + } + }; + let ubound = match upper { + None => None, + Some((value, kind)) => { + let dec = rust_decimal::Decimal::from_str_exact(value) + .with_context(|| format!("invalid decimal {value}"))?; + let dec = RangeableDecimal(dec); + Some(postgres_range::RangeBound::new( + dec, + range_bound_kind(*kind), + )) + } + }; + let r = postgres_range::Range::new(lbound, ubound); + Ok(Box::new(r)) + } ParameterValue::ArrayInt32(vs) => Ok(Box::new(vs.to_owned())), ParameterValue::ArrayInt64(vs) => Ok(Box::new(vs.to_owned())), + ParameterValue::ArrayDecimal(vs) => { + let decs = vs + .iter() + .map(|v| match v { + None => Ok(None), + Some(v) => rust_decimal::Decimal::from_str_exact(v) + .with_context(|| format!("invalid decimal {v}")) + .map(Some), + }) + .collect::>>()?; + Ok(Box::new(decs)) + } ParameterValue::ArrayStr(vs) => Ok(Box::new(vs.to_owned())), ParameterValue::Interval(v) => Ok(Box::new(Interval(*v))), ParameterValue::DbNull => Ok(Box::new(PgNull)), @@ -238,11 +278,14 @@ fn convert_data_type(pg_type: &Type) -> DbDataType { Type::UUID => DbDataType::Uuid, Type::JSONB => DbDataType::Jsonb, Type::NUMERIC => DbDataType::Decimal, - Type::INT4_RANGE => DbDataType::Range32, - Type::INT8_RANGE => DbDataType::Range64, + Type::INT4_RANGE => DbDataType::RangeInt32, + Type::INT8_RANGE => DbDataType::RangeInt64, + Type::NUM_RANGE => DbDataType::RangeDecimal, Type::INT4_ARRAY => DbDataType::ArrayInt32, Type::INT8_ARRAY => DbDataType::ArrayInt64, + Type::NUMERIC_ARRAY => DbDataType::ArrayDecimal, Type::TEXT_ARRAY | Type::VARCHAR_ARRAY | Type::BPCHAR_ARRAY => DbDataType::ArrayStr, + Type::INTERVAL => DbDataType::Interval, _ => { tracing::debug!("Couldn't convert Postgres type {} to WIT", pg_type.name(),); DbDataType::Other @@ -367,7 +410,7 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { Some(v) => { let lower = v.lower().map(tuplify_range_bound); let upper = v.upper().map(tuplify_range_bound); - DbValue::Range32((lower, upper)) + DbValue::RangeInt32((lower, upper)) } None => DbValue::DbNull, } @@ -378,7 +421,22 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { Some(v) => { let lower = v.lower().map(tuplify_range_bound); let upper = v.upper().map(tuplify_range_bound); - DbValue::Range64((lower, upper)) + DbValue::RangeInt64((lower, upper)) + } + None => DbValue::DbNull, + } + } + &Type::NUM_RANGE => { + let value: Option> = row.try_get(index)?; + match value { + Some(v) => { + let lower = v + .lower() + .map(|b| tuplify_range_bound_map(b, |d| d.0.to_string())); + let upper = v + .upper() + .map(|b| tuplify_range_bound_map(b, |d| d.0.to_string())); + DbValue::RangeDecimal((lower, upper)) } None => DbValue::DbNull, } @@ -397,6 +455,16 @@ fn convert_entry(row: &Row, index: usize) -> anyhow::Result { None => DbValue::DbNull, } } + &Type::NUMERIC_ARRAY => { + let value: Option>> = row.try_get(index)?; + match value { + Some(v) => { + let dstrs = v.iter().map(|opt| opt.map(|d| d.to_string())).collect(); + DbValue::ArrayDecimal(dstrs) + } + None => DbValue::DbNull, + } + } &Type::TEXT_ARRAY | &Type::VARCHAR_ARRAY | &Type::BPCHAR_ARRAY => { let value: Option>> = row.try_get(index)?; match value { @@ -429,6 +497,13 @@ fn tuplify_range_bound( (value.value, wit_bound_kind(value.type_)) } +fn tuplify_range_bound_map( + value: &postgres_range::RangeBound, + map_fn: impl Fn(&T) -> U, +) -> (U, v4::RangeBoundKind) { + (map_fn(&value.value), wit_bound_kind(value.type_)) +} + fn wit_bound_kind(bound_type: postgres_range::BoundType) -> v4::RangeBoundKind { match bound_type { postgres_range::BoundType::Inclusive => v4::RangeBoundKind::Inclusive, @@ -589,3 +664,53 @@ impl std::fmt::Debug for IntervalLengthError { std::fmt::Display::fmt(self, f) } } + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +struct RangeableDecimal(rust_decimal::Decimal); + +impl ToSql for RangeableDecimal { + tokio_postgres::types::to_sql_checked!(); + + fn to_sql( + &self, + ty: &Type, + out: &mut tokio_postgres::types::private::BytesMut, + ) -> Result> + where + Self: Sized, + { + self.0.to_sql(ty, out) + } + + fn accepts(ty: &Type) -> bool + where + Self: Sized, + { + ::accepts(ty) + } +} + +impl FromSql<'_> for RangeableDecimal { + fn from_sql( + ty: &Type, + raw: &'_ [u8], + ) -> std::result::Result> { + let d = ::from_sql(ty, raw)?; + Ok(Self(d)) + } + + fn accepts(ty: &Type) -> bool { + ::accepts(ty) + } +} + +impl postgres_range::Normalizable for RangeableDecimal { + fn normalize( + bound: postgres_range::RangeBound, + ) -> postgres_range::RangeBound + where + S: postgres_range::BoundSided, + { + bound + } +} diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 23c6daa36a..b5aba5473b 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -120,10 +120,12 @@ mod rdbms_types { pg4::DbValue::Uuid(_) => pg3::DbValue::Unsupported, pg4::DbValue::Jsonb(_) => pg3::DbValue::Unsupported, pg4::DbValue::Decimal(_) => pg3::DbValue::Unsupported, - pg4::DbValue::Range32(_) => pg3::DbValue::Unsupported, - pg4::DbValue::Range64(_) => pg3::DbValue::Unsupported, + pg4::DbValue::RangeInt32(_) => pg3::DbValue::Unsupported, + pg4::DbValue::RangeInt64(_) => pg3::DbValue::Unsupported, + pg4::DbValue::RangeDecimal(_) => pg3::DbValue::Unsupported, pg4::DbValue::ArrayInt32(_) => pg3::DbValue::Unsupported, pg4::DbValue::ArrayInt64(_) => pg3::DbValue::Unsupported, + pg4::DbValue::ArrayDecimal(_) => pg3::DbValue::Unsupported, pg4::DbValue::ArrayStr(_) => pg3::DbValue::Unsupported, pg4::DbValue::Interval(_) => pg3::DbValue::Unsupported, pg4::DbValue::DbNull => pg3::DbValue::DbNull, @@ -187,10 +189,12 @@ mod rdbms_types { pg4::DbDataType::Uuid => pg3::DbDataType::Other, pg4::DbDataType::Jsonb => pg3::DbDataType::Other, pg4::DbDataType::Decimal => pg3::DbDataType::Other, - pg4::DbDataType::Range32 => pg3::DbDataType::Other, - pg4::DbDataType::Range64 => pg3::DbDataType::Other, + pg4::DbDataType::RangeInt32 => pg3::DbDataType::Other, + pg4::DbDataType::RangeInt64 => pg3::DbDataType::Other, + pg4::DbDataType::RangeDecimal => pg3::DbDataType::Other, pg4::DbDataType::ArrayInt32 => pg3::DbDataType::Other, pg4::DbDataType::ArrayInt64 => pg3::DbDataType::Other, + pg4::DbDataType::ArrayDecimal => pg3::DbDataType::Other, pg4::DbDataType::ArrayStr => pg3::DbDataType::Other, pg4::DbDataType::Interval => pg3::DbDataType::Other, pg4::DbDataType::Other => pg3::DbDataType::Other, diff --git a/wit/deps/spin-postgres@4.0.0/postgres.wit b/wit/deps/spin-postgres@4.0.0/postgres.wit index 0f092425aa..3ebc744514 100644 --- a/wit/deps/spin-postgres@4.0.0/postgres.wit +++ b/wit/deps/spin-postgres@4.0.0/postgres.wit @@ -28,10 +28,12 @@ interface postgres { uuid, jsonb, decimal, - range32, - range64, + range-int32, + range-int64, + range-decimal, array-int32, array-int64, + array-decimal, array-str, interval, other, @@ -58,10 +60,12 @@ interface postgres { uuid(string), jsonb(list), decimal(string), // I admit defeat. Base 10 - range32(tuple>, option>>), - range64(tuple>, option>>), + range-int32(tuple>, option>>), + range-int64(tuple>, option>>), + range-decimal(tuple>, option>>), array-int32(list>), array-int64(list>), + array-decimal(list>), array-str(list>), interval(interval), db-null, @@ -89,10 +93,12 @@ interface postgres { uuid(string), jsonb(list), decimal(string), // base 10 - range32(tuple>, option>>), - range64(tuple>, option>>), + range-int32(tuple>, option>>), + range-int64(tuple>, option>>), + range-decimal(tuple>, option>>), array-int32(list>), array-int64(list>), + array-decimal(list>), array-str(list>), interval(interval), db-null, From c0031074711b46de13672231ec39ee20d46808a1 Mon Sep 17 00:00:00 2001 From: itowlson Date: Fri, 27 Jun 2025 12:21:48 +1200 Subject: [PATCH 6/9] Break type-related code out of Postgres `client` to more focused modules Signed-off-by: itowlson --- crates/factor-outbound-pg/src/client.rs | 1006 +++++++---------- crates/factor-outbound-pg/src/lib.rs | 1 + crates/factor-outbound-pg/src/types.rs | 162 +++ .../factor-outbound-pg/src/types/convert.rs | 318 ++++++ .../factor-outbound-pg/src/types/decimal.rs | 59 + .../factor-outbound-pg/src/types/interval.rs | 92 ++ .../factor-outbound-pg/src/types/pg_null.rs | 44 + 7 files changed, 1111 insertions(+), 571 deletions(-) create mode 100644 crates/factor-outbound-pg/src/types.rs create mode 100644 crates/factor-outbound-pg/src/types/convert.rs create mode 100644 crates/factor-outbound-pg/src/types/decimal.rs create mode 100644 crates/factor-outbound-pg/src/types/interval.rs create mode 100644 crates/factor-outbound-pg/src/types/pg_null.rs diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index e6cd714b17..ae4fb796da 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -1,14 +1,15 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use native_tls::TlsConnector; use postgres_native_tls::MakeTlsConnector; use spin_world::async_trait; use spin_world::spin::postgres4_0_0::postgres::{ - self as v4, Column, DbDataType, DbValue, ParameterValue, RowSet, + self as v4, Column, DbValue, ParameterValue, RowSet, }; -use tokio_postgres::types::{FromSql, Type}; use tokio_postgres::{config::SslMode, types::ToSql, Row}; use tokio_postgres::{Client as TokioClient, NoTls, Socket}; +use crate::types::{convert_data_type, convert_entry, to_sql_parameter}; + #[async_trait] pub trait Client { async fn build_client(address: &str) -> Result @@ -122,130 +123,130 @@ where }); } -fn to_sql_parameter(value: &ParameterValue) -> Result> { - match value { - ParameterValue::Boolean(v) => Ok(Box::new(*v)), - ParameterValue::Int32(v) => Ok(Box::new(*v)), - ParameterValue::Int64(v) => Ok(Box::new(*v)), - ParameterValue::Int8(v) => Ok(Box::new(*v)), - ParameterValue::Int16(v) => Ok(Box::new(*v)), - ParameterValue::Floating32(v) => Ok(Box::new(*v)), - ParameterValue::Floating64(v) => Ok(Box::new(*v)), - ParameterValue::Str(v) => Ok(Box::new(v.clone())), - ParameterValue::Binary(v) => Ok(Box::new(v.clone())), - ParameterValue::Date((y, mon, d)) => { - let naive_date = chrono::NaiveDate::from_ymd_opt(*y, (*mon).into(), (*d).into()) - .ok_or_else(|| anyhow!("invalid date y={y}, m={mon}, d={d}"))?; - Ok(Box::new(naive_date)) - } - ParameterValue::Time((h, min, s, ns)) => { - let naive_time = - chrono::NaiveTime::from_hms_nano_opt((*h).into(), (*min).into(), (*s).into(), *ns) - .ok_or_else(|| anyhow!("invalid time {h}:{min}:{s}:{ns}"))?; - Ok(Box::new(naive_time)) - } - ParameterValue::Datetime((y, mon, d, h, min, s, ns)) => { - let naive_date = chrono::NaiveDate::from_ymd_opt(*y, (*mon).into(), (*d).into()) - .ok_or_else(|| anyhow!("invalid date y={y}, m={mon}, d={d}"))?; - let naive_time = - chrono::NaiveTime::from_hms_nano_opt((*h).into(), (*min).into(), (*s).into(), *ns) - .ok_or_else(|| anyhow!("invalid time {h}:{min}:{s}:{ns}"))?; - let dt = chrono::NaiveDateTime::new(naive_date, naive_time); - Ok(Box::new(dt)) - } - ParameterValue::Timestamp(v) => { - let ts = chrono::DateTime::::from_timestamp(*v, 0) - .ok_or_else(|| anyhow!("invalid epoch timestamp {v}"))?; - Ok(Box::new(ts)) - } - ParameterValue::Uuid(v) => { - let u = uuid::Uuid::parse_str(v).with_context(|| format!("invalid UUID {v}"))?; - Ok(Box::new(u)) - } - ParameterValue::Jsonb(v) => { - let j: serde_json::Value = serde_json::from_slice(v) - .with_context(|| format!("invalid JSON {}", String::from_utf8_lossy(v)))?; - Ok(Box::new(j)) - } - ParameterValue::Decimal(v) => { - let dec = rust_decimal::Decimal::from_str_exact(v) - .with_context(|| format!("invalid decimal {v}"))?; - Ok(Box::new(dec)) - } - ParameterValue::RangeInt32((lower, upper)) => { - let lbound = lower.map(|(value, kind)| { - postgres_range::RangeBound::new(value, range_bound_kind(kind)) - }); - let ubound = upper.map(|(value, kind)| { - postgres_range::RangeBound::new(value, range_bound_kind(kind)) - }); - let r = postgres_range::Range::new(lbound, ubound); - Ok(Box::new(r)) - } - ParameterValue::RangeInt64((lower, upper)) => { - let lbound = lower.map(|(value, kind)| { - postgres_range::RangeBound::new(value, range_bound_kind(kind)) - }); - let ubound = upper.map(|(value, kind)| { - postgres_range::RangeBound::new(value, range_bound_kind(kind)) - }); - let r = postgres_range::Range::new(lbound, ubound); - Ok(Box::new(r)) - } - ParameterValue::RangeDecimal((lower, upper)) => { - let lbound = match lower { - None => None, - Some((value, kind)) => { - let dec = rust_decimal::Decimal::from_str_exact(value) - .with_context(|| format!("invalid decimal {value}"))?; - let dec = RangeableDecimal(dec); - Some(postgres_range::RangeBound::new( - dec, - range_bound_kind(*kind), - )) - } - }; - let ubound = match upper { - None => None, - Some((value, kind)) => { - let dec = rust_decimal::Decimal::from_str_exact(value) - .with_context(|| format!("invalid decimal {value}"))?; - let dec = RangeableDecimal(dec); - Some(postgres_range::RangeBound::new( - dec, - range_bound_kind(*kind), - )) - } - }; - let r = postgres_range::Range::new(lbound, ubound); - Ok(Box::new(r)) - } - ParameterValue::ArrayInt32(vs) => Ok(Box::new(vs.to_owned())), - ParameterValue::ArrayInt64(vs) => Ok(Box::new(vs.to_owned())), - ParameterValue::ArrayDecimal(vs) => { - let decs = vs - .iter() - .map(|v| match v { - None => Ok(None), - Some(v) => rust_decimal::Decimal::from_str_exact(v) - .with_context(|| format!("invalid decimal {v}")) - .map(Some), - }) - .collect::>>()?; - Ok(Box::new(decs)) - } - ParameterValue::ArrayStr(vs) => Ok(Box::new(vs.to_owned())), - ParameterValue::Interval(v) => Ok(Box::new(Interval(*v))), - ParameterValue::DbNull => Ok(Box::new(PgNull)), - } -} - -fn range_bound_kind(wit_kind: v4::RangeBoundKind) -> postgres_range::BoundType { - match wit_kind { - v4::RangeBoundKind::Inclusive => postgres_range::BoundType::Inclusive, - v4::RangeBoundKind::Exclusive => postgres_range::BoundType::Exclusive, - } -} +// fn to_sql_parameter(value: &ParameterValue) -> Result> { +// match value { +// ParameterValue::Boolean(v) => Ok(Box::new(*v)), +// ParameterValue::Int32(v) => Ok(Box::new(*v)), +// ParameterValue::Int64(v) => Ok(Box::new(*v)), +// ParameterValue::Int8(v) => Ok(Box::new(*v)), +// ParameterValue::Int16(v) => Ok(Box::new(*v)), +// ParameterValue::Floating32(v) => Ok(Box::new(*v)), +// ParameterValue::Floating64(v) => Ok(Box::new(*v)), +// ParameterValue::Str(v) => Ok(Box::new(v.clone())), +// ParameterValue::Binary(v) => Ok(Box::new(v.clone())), +// ParameterValue::Date((y, mon, d)) => { +// let naive_date = chrono::NaiveDate::from_ymd_opt(*y, (*mon).into(), (*d).into()) +// .ok_or_else(|| anyhow!("invalid date y={y}, m={mon}, d={d}"))?; +// Ok(Box::new(naive_date)) +// } +// ParameterValue::Time((h, min, s, ns)) => { +// let naive_time = +// chrono::NaiveTime::from_hms_nano_opt((*h).into(), (*min).into(), (*s).into(), *ns) +// .ok_or_else(|| anyhow!("invalid time {h}:{min}:{s}:{ns}"))?; +// Ok(Box::new(naive_time)) +// } +// ParameterValue::Datetime((y, mon, d, h, min, s, ns)) => { +// let naive_date = chrono::NaiveDate::from_ymd_opt(*y, (*mon).into(), (*d).into()) +// .ok_or_else(|| anyhow!("invalid date y={y}, m={mon}, d={d}"))?; +// let naive_time = +// chrono::NaiveTime::from_hms_nano_opt((*h).into(), (*min).into(), (*s).into(), *ns) +// .ok_or_else(|| anyhow!("invalid time {h}:{min}:{s}:{ns}"))?; +// let dt = chrono::NaiveDateTime::new(naive_date, naive_time); +// Ok(Box::new(dt)) +// } +// ParameterValue::Timestamp(v) => { +// let ts = chrono::DateTime::::from_timestamp(*v, 0) +// .ok_or_else(|| anyhow!("invalid epoch timestamp {v}"))?; +// Ok(Box::new(ts)) +// } +// ParameterValue::Uuid(v) => { +// let u = uuid::Uuid::parse_str(v).with_context(|| format!("invalid UUID {v}"))?; +// Ok(Box::new(u)) +// } +// ParameterValue::Jsonb(v) => { +// let j: serde_json::Value = serde_json::from_slice(v) +// .with_context(|| format!("invalid JSON {}", String::from_utf8_lossy(v)))?; +// Ok(Box::new(j)) +// } +// ParameterValue::Decimal(v) => { +// let dec = rust_decimal::Decimal::from_str_exact(v) +// .with_context(|| format!("invalid decimal {v}"))?; +// Ok(Box::new(dec)) +// } +// ParameterValue::RangeInt32((lower, upper)) => { +// let lbound = lower.map(|(value, kind)| { +// postgres_range::RangeBound::new(value, range_bound_kind(kind)) +// }); +// let ubound = upper.map(|(value, kind)| { +// postgres_range::RangeBound::new(value, range_bound_kind(kind)) +// }); +// let r = postgres_range::Range::new(lbound, ubound); +// Ok(Box::new(r)) +// } +// ParameterValue::RangeInt64((lower, upper)) => { +// let lbound = lower.map(|(value, kind)| { +// postgres_range::RangeBound::new(value, range_bound_kind(kind)) +// }); +// let ubound = upper.map(|(value, kind)| { +// postgres_range::RangeBound::new(value, range_bound_kind(kind)) +// }); +// let r = postgres_range::Range::new(lbound, ubound); +// Ok(Box::new(r)) +// } +// ParameterValue::RangeDecimal((lower, upper)) => { +// let lbound = match lower { +// None => None, +// Some((value, kind)) => { +// let dec = rust_decimal::Decimal::from_str_exact(value) +// .with_context(|| format!("invalid decimal {value}"))?; +// let dec = RangeableDecimal(dec); +// Some(postgres_range::RangeBound::new( +// dec, +// range_bound_kind(*kind), +// )) +// } +// }; +// let ubound = match upper { +// None => None, +// Some((value, kind)) => { +// let dec = rust_decimal::Decimal::from_str_exact(value) +// .with_context(|| format!("invalid decimal {value}"))?; +// let dec = RangeableDecimal(dec); +// Some(postgres_range::RangeBound::new( +// dec, +// range_bound_kind(*kind), +// )) +// } +// }; +// let r = postgres_range::Range::new(lbound, ubound); +// Ok(Box::new(r)) +// } +// ParameterValue::ArrayInt32(vs) => Ok(Box::new(vs.to_owned())), +// ParameterValue::ArrayInt64(vs) => Ok(Box::new(vs.to_owned())), +// ParameterValue::ArrayDecimal(vs) => { +// let decs = vs +// .iter() +// .map(|v| match v { +// None => Ok(None), +// Some(v) => rust_decimal::Decimal::from_str_exact(v) +// .with_context(|| format!("invalid decimal {v}")) +// .map(Some), +// }) +// .collect::>>()?; +// Ok(Box::new(decs)) +// } +// ParameterValue::ArrayStr(vs) => Ok(Box::new(vs.to_owned())), +// ParameterValue::Interval(v) => Ok(Box::new(Interval(*v))), +// ParameterValue::DbNull => Ok(Box::new(PgNull)), +// } +// } + +// fn range_bound_kind(wit_kind: v4::RangeBoundKind) -> postgres_range::BoundType { +// match wit_kind { +// v4::RangeBoundKind::Inclusive => postgres_range::BoundType::Inclusive, +// v4::RangeBoundKind::Exclusive => postgres_range::BoundType::Exclusive, +// } +// } fn infer_columns(row: &Row) -> Vec { let mut result = Vec::with_capacity(row.len()); @@ -262,37 +263,6 @@ fn infer_column(row: &Row, index: usize) -> Column { Column { name, data_type } } -fn convert_data_type(pg_type: &Type) -> DbDataType { - match *pg_type { - Type::BOOL => DbDataType::Boolean, - Type::BYTEA => DbDataType::Binary, - Type::FLOAT4 => DbDataType::Floating32, - Type::FLOAT8 => DbDataType::Floating64, - Type::INT2 => DbDataType::Int16, - Type::INT4 => DbDataType::Int32, - Type::INT8 => DbDataType::Int64, - Type::TEXT | Type::VARCHAR | Type::BPCHAR => DbDataType::Str, - Type::TIMESTAMP | Type::TIMESTAMPTZ => DbDataType::Timestamp, - Type::DATE => DbDataType::Date, - Type::TIME => DbDataType::Time, - Type::UUID => DbDataType::Uuid, - Type::JSONB => DbDataType::Jsonb, - Type::NUMERIC => DbDataType::Decimal, - Type::INT4_RANGE => DbDataType::RangeInt32, - Type::INT8_RANGE => DbDataType::RangeInt64, - Type::NUM_RANGE => DbDataType::RangeDecimal, - Type::INT4_ARRAY => DbDataType::ArrayInt32, - Type::INT8_ARRAY => DbDataType::ArrayInt64, - Type::NUMERIC_ARRAY => DbDataType::ArrayDecimal, - Type::TEXT_ARRAY | Type::VARCHAR_ARRAY | Type::BPCHAR_ARRAY => DbDataType::ArrayStr, - Type::INTERVAL => DbDataType::Interval, - _ => { - tracing::debug!("Couldn't convert Postgres type {} to WIT", pg_type.name(),); - DbDataType::Other - } - } -} - fn convert_row(row: &Row) -> anyhow::Result> { let mut result = Vec::with_capacity(row.len()); for index in 0..row.len() { @@ -301,416 +271,310 @@ fn convert_row(row: &Row) -> anyhow::Result> { Ok(result) } -fn convert_entry(row: &Row, index: usize) -> anyhow::Result { - let column = &row.columns()[index]; - let value = match column.type_() { - &Type::BOOL => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Boolean(v), - None => DbValue::DbNull, - } - } - &Type::BYTEA => { - let value: Option> = row.try_get(index)?; - match value { - Some(v) => DbValue::Binary(v), - None => DbValue::DbNull, - } - } - &Type::FLOAT4 => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Floating32(v), - None => DbValue::DbNull, - } - } - &Type::FLOAT8 => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Floating64(v), - None => DbValue::DbNull, - } - } - &Type::INT2 => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Int16(v), - None => DbValue::DbNull, - } - } - &Type::INT4 => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Int32(v), - None => DbValue::DbNull, - } - } - &Type::INT8 => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Int64(v), - None => DbValue::DbNull, - } - } - &Type::TEXT | &Type::VARCHAR | &Type::BPCHAR => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Str(v), - None => DbValue::DbNull, - } - } - &Type::TIMESTAMP | &Type::TIMESTAMPTZ => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Datetime(tuplify_date_time(v)?), - None => DbValue::DbNull, - } - } - &Type::DATE => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Date(tuplify_date(v)?), - None => DbValue::DbNull, - } - } - &Type::TIME => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Time(tuplify_time(v)?), - None => DbValue::DbNull, - } - } - &Type::UUID => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Uuid(v.to_string()), - None => DbValue::DbNull, - } - } - &Type::JSONB => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => { - DbValue::Jsonb(serde_json::to_vec(&v).context("invalid JSON from database")?) - } - None => DbValue::DbNull, - } - } - &Type::NUMERIC => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Decimal(v.to_string()), - None => DbValue::DbNull, - } - } - &Type::INT4_RANGE => { - let value: Option> = row.try_get(index)?; - match value { - Some(v) => { - let lower = v.lower().map(tuplify_range_bound); - let upper = v.upper().map(tuplify_range_bound); - DbValue::RangeInt32((lower, upper)) - } - None => DbValue::DbNull, - } - } - &Type::INT8_RANGE => { - let value: Option> = row.try_get(index)?; - match value { - Some(v) => { - let lower = v.lower().map(tuplify_range_bound); - let upper = v.upper().map(tuplify_range_bound); - DbValue::RangeInt64((lower, upper)) - } - None => DbValue::DbNull, - } - } - &Type::NUM_RANGE => { - let value: Option> = row.try_get(index)?; - match value { - Some(v) => { - let lower = v - .lower() - .map(|b| tuplify_range_bound_map(b, |d| d.0.to_string())); - let upper = v - .upper() - .map(|b| tuplify_range_bound_map(b, |d| d.0.to_string())); - DbValue::RangeDecimal((lower, upper)) - } - None => DbValue::DbNull, - } - } - &Type::INT4_ARRAY => { - let value: Option>> = row.try_get(index)?; - match value { - Some(v) => DbValue::ArrayInt32(v), - None => DbValue::DbNull, - } - } - &Type::INT8_ARRAY => { - let value: Option>> = row.try_get(index)?; - match value { - Some(v) => DbValue::ArrayInt64(v), - None => DbValue::DbNull, - } - } - &Type::NUMERIC_ARRAY => { - let value: Option>> = row.try_get(index)?; - match value { - Some(v) => { - let dstrs = v.iter().map(|opt| opt.map(|d| d.to_string())).collect(); - DbValue::ArrayDecimal(dstrs) - } - None => DbValue::DbNull, - } - } - &Type::TEXT_ARRAY | &Type::VARCHAR_ARRAY | &Type::BPCHAR_ARRAY => { - let value: Option>> = row.try_get(index)?; - match value { - Some(v) => DbValue::ArrayStr(v), - None => DbValue::DbNull, - } - } - &Type::INTERVAL => { - let value: Option = row.try_get(index)?; - match value { - Some(v) => DbValue::Interval(v.0), - None => DbValue::DbNull, - } - } - t => { - tracing::debug!( - "Couldn't convert Postgres type {} in column {}", - t.name(), - column.name() - ); - DbValue::Unsupported - } - }; - Ok(value) -} - -fn tuplify_range_bound( - value: &postgres_range::RangeBound, -) -> (T, v4::RangeBoundKind) { - (value.value, wit_bound_kind(value.type_)) -} - -fn tuplify_range_bound_map( - value: &postgres_range::RangeBound, - map_fn: impl Fn(&T) -> U, -) -> (U, v4::RangeBoundKind) { - (map_fn(&value.value), wit_bound_kind(value.type_)) -} - -fn wit_bound_kind(bound_type: postgres_range::BoundType) -> v4::RangeBoundKind { - match bound_type { - postgres_range::BoundType::Inclusive => v4::RangeBoundKind::Inclusive, - postgres_range::BoundType::Exclusive => v4::RangeBoundKind::Exclusive, - } -} - -// Functions to convert from the chrono types to the WIT interface tuples -fn tuplify_date_time( - value: chrono::NaiveDateTime, -) -> anyhow::Result<(i32, u8, u8, u8, u8, u8, u32)> { - use chrono::{Datelike, Timelike}; - Ok(( - value.year(), - value.month().try_into()?, - value.day().try_into()?, - value.hour().try_into()?, - value.minute().try_into()?, - value.second().try_into()?, - value.nanosecond(), - )) -} - -fn tuplify_date(value: chrono::NaiveDate) -> anyhow::Result<(i32, u8, u8)> { - use chrono::Datelike; - Ok(( - value.year(), - value.month().try_into()?, - value.day().try_into()?, - )) -} - -fn tuplify_time(value: chrono::NaiveTime) -> anyhow::Result<(u8, u8, u8, u32)> { - use chrono::Timelike; - Ok(( - value.hour().try_into()?, - value.minute().try_into()?, - value.second().try_into()?, - value.nanosecond(), - )) -} - -/// Although the Postgres crate converts Rust Option::None to Postgres NULL, -/// it enforces the type of the Option as it does so. (For example, trying to -/// pass an Option::::None to a VARCHAR column fails conversion.) As we -/// do not know expected column types, we instead use a "neutral" custom type -/// which allows conversion to any type but always tells the Postgres crate to -/// treat it as a SQL NULL. -struct PgNull; - -impl ToSql for PgNull { - fn to_sql( - &self, - _ty: &Type, - _out: &mut tokio_postgres::types::private::BytesMut, - ) -> Result> - where - Self: Sized, - { - Ok(tokio_postgres::types::IsNull::Yes) - } - - fn accepts(_ty: &Type) -> bool - where - Self: Sized, - { - true - } - - fn to_sql_checked( - &self, - _ty: &Type, - _out: &mut tokio_postgres::types::private::BytesMut, - ) -> Result> { - Ok(tokio_postgres::types::IsNull::Yes) - } -} - -impl std::fmt::Debug for PgNull { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NULL").finish() - } -} - -#[derive(Debug)] -struct Interval(v4::Interval); - -impl ToSql for Interval { - tokio_postgres::types::to_sql_checked!(); - - fn to_sql( - &self, - _ty: &Type, - out: &mut tokio_postgres::types::private::BytesMut, - ) -> Result> - where - Self: Sized, - { - use bytes::BufMut; - - out.put_i64(self.0.micros); - out.put_i32(self.0.days); - out.put_i32(self.0.months); - - Ok(tokio_postgres::types::IsNull::No) - } - - fn accepts(ty: &Type) -> bool - where - Self: Sized, - { - matches!(ty, &Type::INTERVAL) - } -} - -impl FromSql<'_> for Interval { - fn from_sql( - _ty: &Type, - raw: &'_ [u8], - ) -> std::result::Result> { - const EXPECTED_LEN: usize = size_of::() + size_of::() + size_of::(); - - if raw.len() != EXPECTED_LEN { - return Err(Box::new(IntervalLengthError)); - } - - let (micro_bytes, rest) = raw.split_at(size_of::()); - let (day_bytes, rest) = rest.split_at(size_of::()); - let month_bytes = rest; - let months = i32::from_be_bytes(month_bytes.try_into().unwrap()); - let days = i32::from_be_bytes(day_bytes.try_into().unwrap()); - let micros = i64::from_be_bytes(micro_bytes.try_into().unwrap()); - - Ok(Self(v4::Interval { - micros, - days, - months, - })) - } - - fn accepts(ty: &Type) -> bool { - matches!(ty, &Type::INTERVAL) - } -} - -struct IntervalLengthError; - -impl std::error::Error for IntervalLengthError {} - -impl std::fmt::Display for IntervalLengthError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("unexpected binary format for Postgres INTERVAL") - } -} - -impl std::fmt::Debug for IntervalLengthError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(self, f) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] -struct RangeableDecimal(rust_decimal::Decimal); - -impl ToSql for RangeableDecimal { - tokio_postgres::types::to_sql_checked!(); - - fn to_sql( - &self, - ty: &Type, - out: &mut tokio_postgres::types::private::BytesMut, - ) -> Result> - where - Self: Sized, - { - self.0.to_sql(ty, out) - } - - fn accepts(ty: &Type) -> bool - where - Self: Sized, - { - ::accepts(ty) - } -} - -impl FromSql<'_> for RangeableDecimal { - fn from_sql( - ty: &Type, - raw: &'_ [u8], - ) -> std::result::Result> { - let d = ::from_sql(ty, raw)?; - Ok(Self(d)) - } - - fn accepts(ty: &Type) -> bool { - ::accepts(ty) - } -} - -impl postgres_range::Normalizable for RangeableDecimal { - fn normalize( - bound: postgres_range::RangeBound, - ) -> postgres_range::RangeBound - where - S: postgres_range::BoundSided, - { - bound - } -} +// fn db_value<'a, T: FromSql<'a>>(row: &'a Row, index: usize, convert_fn: impl Fn(T) -> DbValue) -> anyhow::Result { +// let value: Option = row.try_get(index)?; +// Ok(match value { +// Some(v) => convert_fn(v), +// None => DbValue::DbNull, +// }) +// } + +// fn map_db_value<'a, T: FromSql<'a>, W>(row: &'a Row, index: usize, ctor: impl Fn(W) -> DbValue, convert_fn: impl Fn(T) -> W) -> anyhow::Result { +// let value: Option = row.try_get(index)?; +// Ok(match value { +// Some(v) => ctor(convert_fn(v)), +// None => DbValue::DbNull, +// }) +// } + +// fn try_map_db_value<'a, T: FromSql<'a>, W>(row: &'a Row, index: usize, ctor: impl Fn(W) -> DbValue, convert_fn: impl Fn(T) -> anyhow::Result) -> anyhow::Result { +// let value: Option = row.try_get(index)?; +// Ok(match value { +// Some(v) => ctor(convert_fn(v)?), +// None => DbValue::DbNull, +// }) +// } + +// fn json_db_value_to_vec(value: serde_json::Value) -> anyhow::Result> { +// serde_json::to_vec(&value).context("invalid JSON from database") +// } + +// fn range_db_value_to_range(value: postgres_range::Range) -> (Option<(T, v4::RangeBoundKind)>, Option<(T, v4::RangeBoundKind)>) { +// let lower = value.lower().map(tuplify_range_bound); +// let upper = value.upper().map(tuplify_range_bound); +// (lower, upper) +// } + +// fn decimal_range_db_value_to_range(value: postgres_range::Range) -> (Option<(String, v4::RangeBoundKind)>, Option<(String, v4::RangeBoundKind)>) { +// let lower = value +// .lower() +// .map(|b| tuplify_range_bound_map(b, |d| d.0.to_string())); +// let upper = value +// .upper() +// .map(|b| tuplify_range_bound_map(b, |d| d.0.to_string())); +// (lower, upper) +// } + +// fn decimal_array_db_value_to_wit(value: Vec>) -> Vec> { +// value.iter().map(|opt| opt.map(|d| d.to_string())).collect() +// } + +// fn convert_entry(row: &Row, index: usize) -> anyhow::Result { +// let column = &row.columns()[index]; +// match column.type_() { +// &Type::BOOL => db_value(row, index, DbValue::Boolean), +// &Type::BYTEA => db_value(row, index, DbValue::Binary), +// &Type::FLOAT4 => db_value(row, index, DbValue::Floating32), +// &Type::FLOAT8 => db_value(row, index, DbValue::Floating64), +// &Type::INT2 => db_value(row, index, DbValue::Int16), +// &Type::INT4 => db_value(row, index, DbValue::Int32), +// &Type::INT8 => db_value(row, index, DbValue::Int64), +// &Type::TEXT | &Type::VARCHAR | &Type::BPCHAR => db_value(row, index, DbValue::Str), +// &Type::TIMESTAMP | &Type::TIMESTAMPTZ => try_map_db_value(row, index, DbValue::Datetime, tuplify_date_time), +// &Type::DATE => try_map_db_value(row, index, DbValue::Date, tuplify_date), +// &Type::TIME => try_map_db_value(row, index, DbValue::Time, tuplify_time), +// &Type::UUID => map_db_value(row, index, DbValue::Uuid, |v: uuid::Uuid| v.to_string()), +// &Type::JSONB => try_map_db_value(row, index, DbValue::Jsonb, json_db_value_to_vec), +// &Type::NUMERIC => map_db_value(row, index, DbValue::Decimal, |v: rust_decimal::Decimal| v.to_string()), +// &Type::INT4_RANGE => map_db_value(row, index, DbValue::RangeInt32, range_db_value_to_range), +// &Type::INT8_RANGE => map_db_value(row, index, DbValue::RangeInt64, range_db_value_to_range), +// &Type::NUM_RANGE => map_db_value(row, index, DbValue::RangeDecimal, decimal_range_db_value_to_range), +// &Type::INT4_ARRAY => db_value(row, index, DbValue::ArrayInt32), +// &Type::INT8_ARRAY => db_value(row, index, DbValue::ArrayInt64), +// &Type::NUMERIC_ARRAY => map_db_value(row, index, DbValue::ArrayDecimal, decimal_array_db_value_to_wit), +// &Type::TEXT_ARRAY | &Type::VARCHAR_ARRAY | &Type::BPCHAR_ARRAY => db_value(row, index, DbValue::ArrayStr), +// &Type::INTERVAL => map_db_value(row, index, DbValue::Interval, |v: Interval| v.0), +// t => { +// tracing::debug!( +// "Couldn't convert Postgres type {} in column {}", +// t.name(), +// column.name() +// ); +// Ok(DbValue::Unsupported) +// } +// } +// } + +// fn tuplify_range_bound( +// value: &postgres_range::RangeBound, +// ) -> (T, v4::RangeBoundKind) { +// (value.value, wit_bound_kind(value.type_)) +// } + +// fn tuplify_range_bound_map( +// value: &postgres_range::RangeBound, +// map_fn: impl Fn(&T) -> U, +// ) -> (U, v4::RangeBoundKind) { +// (map_fn(&value.value), wit_bound_kind(value.type_)) +// } + +// fn wit_bound_kind(bound_type: postgres_range::BoundType) -> v4::RangeBoundKind { +// match bound_type { +// postgres_range::BoundType::Inclusive => v4::RangeBoundKind::Inclusive, +// postgres_range::BoundType::Exclusive => v4::RangeBoundKind::Exclusive, +// } +// } + +// // Functions to convert from the chrono types to the WIT interface tuples +// fn tuplify_date_time( +// value: chrono::NaiveDateTime, +// ) -> anyhow::Result<(i32, u8, u8, u8, u8, u8, u32)> { +// use chrono::{Datelike, Timelike}; +// Ok(( +// value.year(), +// value.month().try_into()?, +// value.day().try_into()?, +// value.hour().try_into()?, +// value.minute().try_into()?, +// value.second().try_into()?, +// value.nanosecond(), +// )) +// } + +// fn tuplify_date(value: chrono::NaiveDate) -> anyhow::Result<(i32, u8, u8)> { +// use chrono::Datelike; +// Ok(( +// value.year(), +// value.month().try_into()?, +// value.day().try_into()?, +// )) +// } + +// fn tuplify_time(value: chrono::NaiveTime) -> anyhow::Result<(u8, u8, u8, u32)> { +// use chrono::Timelike; +// Ok(( +// value.hour().try_into()?, +// value.minute().try_into()?, +// value.second().try_into()?, +// value.nanosecond(), +// )) +// } + +// /// Although the Postgres crate converts Rust Option::None to Postgres NULL, +// /// it enforces the type of the Option as it does so. (For example, trying to +// /// pass an Option::::None to a VARCHAR column fails conversion.) As we +// /// do not know expected column types, we instead use a "neutral" custom type +// /// which allows conversion to any type but always tells the Postgres crate to +// /// treat it as a SQL NULL. +// struct PgNull; + +// impl ToSql for PgNull { +// fn to_sql( +// &self, +// _ty: &Type, +// _out: &mut tokio_postgres::types::private::BytesMut, +// ) -> Result> +// where +// Self: Sized, +// { +// Ok(tokio_postgres::types::IsNull::Yes) +// } + +// fn accepts(_ty: &Type) -> bool +// where +// Self: Sized, +// { +// true +// } + +// fn to_sql_checked( +// &self, +// _ty: &Type, +// _out: &mut tokio_postgres::types::private::BytesMut, +// ) -> Result> { +// Ok(tokio_postgres::types::IsNull::Yes) +// } +// } + +// impl std::fmt::Debug for PgNull { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.debug_struct("NULL").finish() +// } +// } + +// #[derive(Debug)] +// struct Interval(v4::Interval); + +// impl ToSql for Interval { +// tokio_postgres::types::to_sql_checked!(); + +// fn to_sql( +// &self, +// _ty: &Type, +// out: &mut tokio_postgres::types::private::BytesMut, +// ) -> Result> +// where +// Self: Sized, +// { +// use bytes::BufMut; + +// out.put_i64(self.0.micros); +// out.put_i32(self.0.days); +// out.put_i32(self.0.months); + +// Ok(tokio_postgres::types::IsNull::No) +// } + +// fn accepts(ty: &Type) -> bool +// where +// Self: Sized, +// { +// matches!(ty, &Type::INTERVAL) +// } +// } + +// impl FromSql<'_> for Interval { +// fn from_sql( +// _ty: &Type, +// raw: &'_ [u8], +// ) -> std::result::Result> { +// const EXPECTED_LEN: usize = size_of::() + size_of::() + size_of::(); + +// if raw.len() != EXPECTED_LEN { +// return Err(Box::new(IntervalLengthError)); +// } + +// let (micro_bytes, rest) = raw.split_at(size_of::()); +// let (day_bytes, rest) = rest.split_at(size_of::()); +// let month_bytes = rest; +// let months = i32::from_be_bytes(month_bytes.try_into().unwrap()); +// let days = i32::from_be_bytes(day_bytes.try_into().unwrap()); +// let micros = i64::from_be_bytes(micro_bytes.try_into().unwrap()); + +// Ok(Self(v4::Interval { +// micros, +// days, +// months, +// })) +// } + +// fn accepts(ty: &Type) -> bool { +// matches!(ty, &Type::INTERVAL) +// } +// } + +// struct IntervalLengthError; + +// impl std::error::Error for IntervalLengthError {} + +// impl std::fmt::Display for IntervalLengthError { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.write_str("unexpected binary format for Postgres INTERVAL") +// } +// } + +// impl std::fmt::Debug for IntervalLengthError { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// std::fmt::Display::fmt(self, f) +// } +// } + +// #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +// struct RangeableDecimal(rust_decimal::Decimal); + +// impl ToSql for RangeableDecimal { +// tokio_postgres::types::to_sql_checked!(); + +// fn to_sql( +// &self, +// ty: &Type, +// out: &mut tokio_postgres::types::private::BytesMut, +// ) -> Result> +// where +// Self: Sized, +// { +// self.0.to_sql(ty, out) +// } + +// fn accepts(ty: &Type) -> bool +// where +// Self: Sized, +// { +// ::accepts(ty) +// } +// } + +// impl FromSql<'_> for RangeableDecimal { +// fn from_sql( +// ty: &Type, +// raw: &'_ [u8], +// ) -> std::result::Result> { +// let d = ::from_sql(ty, raw)?; +// Ok(Self(d)) +// } + +// fn accepts(ty: &Type) -> bool { +// ::accepts(ty) +// } +// } + +// impl postgres_range::Normalizable for RangeableDecimal { +// fn normalize( +// bound: postgres_range::RangeBound, +// ) -> postgres_range::RangeBound +// where +// S: postgres_range::BoundSided, +// { +// bound +// } +// } diff --git a/crates/factor-outbound-pg/src/lib.rs b/crates/factor-outbound-pg/src/lib.rs index 0026ef9d31..86e0e425e1 100644 --- a/crates/factor-outbound-pg/src/lib.rs +++ b/crates/factor-outbound-pg/src/lib.rs @@ -1,5 +1,6 @@ pub mod client; mod host; +mod types; use client::Client; use spin_factor_outbound_networking::{ diff --git a/crates/factor-outbound-pg/src/types.rs b/crates/factor-outbound-pg/src/types.rs new file mode 100644 index 0000000000..39b08c8d9a --- /dev/null +++ b/crates/factor-outbound-pg/src/types.rs @@ -0,0 +1,162 @@ +use spin_world::spin::postgres4_0_0::postgres::{DbDataType, DbValue, ParameterValue}; +use tokio_postgres::types::{FromSql, Type}; +use tokio_postgres::{types::ToSql, Row}; + +mod convert; +mod decimal; +mod interval; +mod pg_null; + +use convert::{ + date_pg_to_wit, date_wit_to_pg, datetime_pg_to_wit, datetime_wit_to_pg, + decimal_array_pg_to_wit, decimal_array_wit_to_pg, decimal_range_pg_to_wit, + decimal_range_wit_to_pg, decimal_wit_to_pg, jsonb_pg_to_wit, jsonb_wit_to_pg, range_pg_to_wit, + range_wit_to_pg, time_pg_to_wit, time_wit_to_pg, timestamp_wit_to_pg, uuid_wit_to_pg, +}; +use interval::Interval; +use pg_null::PgNull; + +pub fn convert_data_type(pg_type: &Type) -> DbDataType { + match *pg_type { + Type::BOOL => DbDataType::Boolean, + Type::BYTEA => DbDataType::Binary, + Type::FLOAT4 => DbDataType::Floating32, + Type::FLOAT8 => DbDataType::Floating64, + Type::INT2 => DbDataType::Int16, + Type::INT4 => DbDataType::Int32, + Type::INT8 => DbDataType::Int64, + Type::TEXT | Type::VARCHAR | Type::BPCHAR => DbDataType::Str, + Type::TIMESTAMP | Type::TIMESTAMPTZ => DbDataType::Datetime, + Type::DATE => DbDataType::Date, + Type::TIME => DbDataType::Time, + Type::UUID => DbDataType::Uuid, + Type::JSONB => DbDataType::Jsonb, + Type::NUMERIC => DbDataType::Decimal, + Type::INT4_RANGE => DbDataType::RangeInt32, + Type::INT8_RANGE => DbDataType::RangeInt64, + Type::NUM_RANGE => DbDataType::RangeDecimal, + Type::INT4_ARRAY => DbDataType::ArrayInt32, + Type::INT8_ARRAY => DbDataType::ArrayInt64, + Type::NUMERIC_ARRAY => DbDataType::ArrayDecimal, + Type::TEXT_ARRAY | Type::VARCHAR_ARRAY | Type::BPCHAR_ARRAY => DbDataType::ArrayStr, + Type::INTERVAL => DbDataType::Interval, + _ => { + tracing::debug!("Couldn't convert Postgres type {} to WIT", pg_type.name(),); + DbDataType::Other + } + } +} + +fn db_value<'a, T: FromSql<'a>>( + row: &'a Row, + index: usize, + convert_fn: impl Fn(T) -> DbValue, +) -> anyhow::Result { + let value: Option = row.try_get(index)?; + Ok(match value { + Some(v) => convert_fn(v), + None => DbValue::DbNull, + }) +} + +fn map_db_value<'a, T: FromSql<'a>, W>( + row: &'a Row, + index: usize, + ctor: impl Fn(W) -> DbValue, + convert_fn: impl Fn(T) -> W, +) -> anyhow::Result { + let value: Option = row.try_get(index)?; + Ok(match value { + Some(v) => ctor(convert_fn(v)), + None => DbValue::DbNull, + }) +} + +fn try_map_db_value<'a, T: FromSql<'a>, W>( + row: &'a Row, + index: usize, + ctor: impl Fn(W) -> DbValue, + convert_fn: impl Fn(T) -> anyhow::Result, +) -> anyhow::Result { + let value: Option = row.try_get(index)?; + Ok(match value { + Some(v) => ctor(convert_fn(v)?), + None => DbValue::DbNull, + }) +} + +pub fn convert_entry(row: &Row, index: usize) -> anyhow::Result { + let column = &row.columns()[index]; + match column.type_() { + &Type::BOOL => db_value(row, index, DbValue::Boolean), + &Type::BYTEA => db_value(row, index, DbValue::Binary), + &Type::FLOAT4 => db_value(row, index, DbValue::Floating32), + &Type::FLOAT8 => db_value(row, index, DbValue::Floating64), + &Type::INT2 => db_value(row, index, DbValue::Int16), + &Type::INT4 => db_value(row, index, DbValue::Int32), + &Type::INT8 => db_value(row, index, DbValue::Int64), + &Type::TEXT | &Type::VARCHAR | &Type::BPCHAR => db_value(row, index, DbValue::Str), + &Type::TIMESTAMP | &Type::TIMESTAMPTZ => { + try_map_db_value(row, index, DbValue::Datetime, datetime_pg_to_wit) + } + &Type::DATE => try_map_db_value(row, index, DbValue::Date, date_pg_to_wit), + &Type::TIME => try_map_db_value(row, index, DbValue::Time, time_pg_to_wit), + &Type::UUID => map_db_value(row, index, DbValue::Uuid, |v: uuid::Uuid| v.to_string()), + &Type::JSONB => try_map_db_value(row, index, DbValue::Jsonb, jsonb_pg_to_wit), + &Type::NUMERIC => map_db_value(row, index, DbValue::Decimal, |v: rust_decimal::Decimal| { + v.to_string() + }), + &Type::INT4_RANGE => map_db_value(row, index, DbValue::RangeInt32, range_pg_to_wit), + &Type::INT8_RANGE => map_db_value(row, index, DbValue::RangeInt64, range_pg_to_wit), + &Type::NUM_RANGE => { + map_db_value(row, index, DbValue::RangeDecimal, decimal_range_pg_to_wit) + } + &Type::INT4_ARRAY => db_value(row, index, DbValue::ArrayInt32), + &Type::INT8_ARRAY => db_value(row, index, DbValue::ArrayInt64), + &Type::NUMERIC_ARRAY => { + map_db_value(row, index, DbValue::ArrayDecimal, decimal_array_pg_to_wit) + } + &Type::TEXT_ARRAY | &Type::VARCHAR_ARRAY | &Type::BPCHAR_ARRAY => { + db_value(row, index, DbValue::ArrayStr) + } + &Type::INTERVAL => map_db_value(row, index, DbValue::Interval, |v: Interval| v.into()), + t => { + tracing::debug!( + "Couldn't convert Postgres type {} in column {}", + t.name(), + column.name() + ); + Ok(DbValue::Unsupported) + } + } +} + +pub fn to_sql_parameter(value: &ParameterValue) -> anyhow::Result> { + match value { + ParameterValue::Boolean(v) => Ok(Box::new(*v)), + ParameterValue::Int32(v) => Ok(Box::new(*v)), + ParameterValue::Int64(v) => Ok(Box::new(*v)), + ParameterValue::Int8(v) => Ok(Box::new(*v)), + ParameterValue::Int16(v) => Ok(Box::new(*v)), + ParameterValue::Floating32(v) => Ok(Box::new(*v)), + ParameterValue::Floating64(v) => Ok(Box::new(*v)), + ParameterValue::Str(v) => Ok(Box::new(v.clone())), + ParameterValue::Binary(v) => Ok(Box::new(v.clone())), + ParameterValue::Date(v) => Ok(Box::new(date_wit_to_pg(v)?)), + ParameterValue::Time(v) => Ok(Box::new(time_wit_to_pg(v)?)), + ParameterValue::Datetime(v) => Ok(Box::new(datetime_wit_to_pg(v)?)), + ParameterValue::Timestamp(v) => Ok(Box::new(timestamp_wit_to_pg(*v)?)), + ParameterValue::Uuid(v) => Ok(Box::new(uuid_wit_to_pg(v)?)), + ParameterValue::Jsonb(v) => Ok(Box::new(jsonb_wit_to_pg(v)?)), + ParameterValue::Decimal(v) => Ok(Box::new(decimal_wit_to_pg(v)?)), + ParameterValue::RangeInt32(v) => Ok(Box::new(range_wit_to_pg(*v))), + ParameterValue::RangeInt64(v) => Ok(Box::new(range_wit_to_pg(*v))), + ParameterValue::RangeDecimal(v) => Ok(Box::new(decimal_range_wit_to_pg(v)?)), + ParameterValue::ArrayInt32(vs) => Ok(Box::new(vs.to_owned())), + ParameterValue::ArrayInt64(vs) => Ok(Box::new(vs.to_owned())), + ParameterValue::ArrayDecimal(vs) => Ok(Box::new(decimal_array_wit_to_pg(vs)?)), + ParameterValue::ArrayStr(vs) => Ok(Box::new(vs.to_owned())), + ParameterValue::Interval(v) => Ok(Box::new(Interval(*v))), + ParameterValue::DbNull => Ok(Box::new(PgNull)), + } +} diff --git a/crates/factor-outbound-pg/src/types/convert.rs b/crates/factor-outbound-pg/src/types/convert.rs new file mode 100644 index 0000000000..ce806245af --- /dev/null +++ b/crates/factor-outbound-pg/src/types/convert.rs @@ -0,0 +1,318 @@ +//! Conversions between WIT representations and the SQL types as surfaced by +//! the tokio_postgres driver. + +use anyhow::{anyhow, Context}; +use spin_world::spin::postgres4_0_0::postgres::{self as v4}; + +use super::decimal::RangeableDecimal; + +pub fn jsonb_pg_to_wit(value: serde_json::Value) -> anyhow::Result> { + serde_json::to_vec(&value).context("invalid JSON from database") +} + +pub fn jsonb_wit_to_pg(value: &[u8]) -> anyhow::Result { + serde_json::from_slice(value) + .with_context(|| format!("invalid JSON {}", String::from_utf8_lossy(value))) +} + +pub fn uuid_wit_to_pg(value: &str) -> anyhow::Result { + uuid::Uuid::parse_str(value).with_context(|| format!("invalid UUID {value}")) +} + +pub fn decimal_wit_to_pg(value: &str) -> anyhow::Result { + rust_decimal::Decimal::from_str_exact(value).with_context(|| format!("invalid decimal {value}")) +} + +pub fn decimal_array_pg_to_wit(value: Vec>) -> Vec> { + value.iter().map(|opt| opt.map(|d| d.to_string())).collect() +} + +pub fn decimal_array_wit_to_pg( + value: &[Option], +) -> anyhow::Result>> { + value + .iter() + .map(|v| match v { + None => Ok(None), + Some(v) => rust_decimal::Decimal::from_str_exact(v) + .with_context(|| format!("invalid decimal {v}")) + .map(Some), + }) + .collect::>>() +} + +// Functions to convert between Postgres ranges and the WIT range representations + +pub type WitRange = ( + Option<(T, v4::RangeBoundKind)>, + Option<(T, v4::RangeBoundKind)>, +); + +pub fn range_pg_to_wit( + value: postgres_range::Range, +) -> WitRange { + let lower = value.lower().map(tuplify_range_bound); + let upper = value.upper().map(tuplify_range_bound); + (lower, upper) +} + +pub fn range_wit_to_pg( + value: WitRange, +) -> postgres_range::Range { + let (lower, upper) = value; + let lbound = lower.map(|(value, kind)| { + postgres_range::RangeBound::new(value, range_bound_kind_wit_to_pg(kind)) + }); + let ubound = upper.map(|(value, kind)| { + postgres_range::RangeBound::new(value, range_bound_kind_wit_to_pg(kind)) + }); + postgres_range::Range::new(lbound, ubound) +} + +pub fn decimal_range_pg_to_wit(value: postgres_range::Range) -> WitRange { + let lower = value + .lower() + .map(|b| tuplify_range_bound_map(b, |d| d.to_string())); + let upper = value + .upper() + .map(|b| tuplify_range_bound_map(b, |d| d.to_string())); + (lower, upper) +} + +pub fn decimal_range_wit_to_pg( + value: &WitRange, +) -> anyhow::Result> { + let (lower, upper) = value; + let lbound = lower + .as_ref() + .map(decimal_range_bound_wit_to_pg) + .transpose()?; + let ubound = upper + .as_ref() + .map(decimal_range_bound_wit_to_pg) + .transpose()?; + Ok(postgres_range::Range::new(lbound, ubound)) +} + +fn decimal_range_bound_wit_to_pg( + (value, kind): &(String, v4::RangeBoundKind), +) -> anyhow::Result> { + let dec = rust_decimal::Decimal::from_str_exact(value) + .with_context(|| format!("invalid decimal {value}"))?; + Ok(postgres_range::RangeBound::new( + RangeableDecimal(dec), + range_bound_kind_wit_to_pg(*kind), + )) +} + +fn tuplify_range_bound( + value: &postgres_range::RangeBound, +) -> (T, v4::RangeBoundKind) { + (value.value, range_bound_kind_pg_to_wit(value.type_)) +} + +fn tuplify_range_bound_map( + value: &postgres_range::RangeBound, + map_fn: impl Fn(&T) -> U, +) -> (U, v4::RangeBoundKind) { + ( + map_fn(&value.value), + range_bound_kind_pg_to_wit(value.type_), + ) +} + +fn range_bound_kind_wit_to_pg(wit_kind: v4::RangeBoundKind) -> postgres_range::BoundType { + match wit_kind { + v4::RangeBoundKind::Inclusive => postgres_range::BoundType::Inclusive, + v4::RangeBoundKind::Exclusive => postgres_range::BoundType::Exclusive, + } +} + +fn range_bound_kind_pg_to_wit(bound_type: postgres_range::BoundType) -> v4::RangeBoundKind { + match bound_type { + postgres_range::BoundType::Inclusive => v4::RangeBoundKind::Inclusive, + postgres_range::BoundType::Exclusive => v4::RangeBoundKind::Exclusive, + } +} + +// Functions to convert between the chrono types (Postgres-facing) and the WIT interface tuples + +pub fn datetime_pg_to_wit( + value: chrono::NaiveDateTime, +) -> anyhow::Result<(i32, u8, u8, u8, u8, u8, u32)> { + use chrono::{Datelike, Timelike}; + Ok(( + value.year(), + value.month().try_into()?, + value.day().try_into()?, + value.hour().try_into()?, + value.minute().try_into()?, + value.second().try_into()?, + value.nanosecond(), + )) +} + +pub fn datetime_wit_to_pg( + (y, mon, d, h, min, s, ns): &(i32, u8, u8, u8, u8, u8, u32), +) -> anyhow::Result { + let naive_date = chrono::NaiveDate::from_ymd_opt(*y, (*mon).into(), (*d).into()) + .ok_or_else(|| anyhow!("invalid date y={y}, m={mon}, d={d}"))?; + let naive_time = + chrono::NaiveTime::from_hms_nano_opt((*h).into(), (*min).into(), (*s).into(), *ns) + .ok_or_else(|| anyhow!("invalid time {h}:{min}:{s}:{ns}"))?; + Ok(chrono::NaiveDateTime::new(naive_date, naive_time)) +} + +pub fn date_wit_to_pg((y, mon, d): &(i32, u8, u8)) -> anyhow::Result { + chrono::NaiveDate::from_ymd_opt(*y, (*mon).into(), (*d).into()) + .ok_or_else(|| anyhow!("invalid date y={y}, m={mon}, d={d}")) +} + +pub fn date_pg_to_wit(value: chrono::NaiveDate) -> anyhow::Result<(i32, u8, u8)> { + use chrono::Datelike; + Ok(( + value.year(), + value.month().try_into()?, + value.day().try_into()?, + )) +} + +pub fn time_wit_to_pg((h, min, s, ns): &(u8, u8, u8, u32)) -> anyhow::Result { + chrono::NaiveTime::from_hms_nano_opt((*h).into(), (*min).into(), (*s).into(), *ns) + .ok_or_else(|| anyhow!("invalid time {h}:{min}:{s}:{ns}")) +} + +pub fn time_pg_to_wit(value: chrono::NaiveTime) -> anyhow::Result<(u8, u8, u8, u32)> { + use chrono::Timelike; + Ok(( + value.hour().try_into()?, + value.minute().try_into()?, + value.second().try_into()?, + value.nanosecond(), + )) +} + +pub fn timestamp_wit_to_pg(value: i64) -> anyhow::Result> { + chrono::DateTime::::from_timestamp(value, 0) + .ok_or_else(|| anyhow!("invalid epoch timestamp {value}")) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn timestamp_is_interpreted_as_seconds() { + let ts = timestamp_wit_to_pg(0).expect("should have converted 0"); + assert_eq!( + chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + ts.date_naive() + ); + + let ts = timestamp_wit_to_pg(60 * 60 + 2 * 60 + 3).expect("should have converted 1h2m3s"); + assert_eq!(chrono::NaiveTime::from_hms_opt(1, 2, 3).unwrap(), ts.time()); + + let ts = timestamp_wit_to_pg(-60 * 60 * 24).expect("should have converted -1d"); + assert_eq!( + chrono::NaiveDate::from_ymd_opt(1969, 12, 31).unwrap(), + ts.date_naive() + ); + + let ts = timestamp_wit_to_pg(60 * 60 * 24 * 365).expect("should have converted -1d"); + assert_eq!( + chrono::NaiveDate::from_ymd_opt(1971, 1, 1).unwrap(), + ts.date_naive() + ); + } + + #[test] + fn can_convert_decimal_range_wit_to_pg() { + let range = ( + Some(("123.45".to_string(), v4::RangeBoundKind::Inclusive)), + Some(("456.789789".to_string(), v4::RangeBoundKind::Exclusive)), + ); + + let pg = decimal_range_wit_to_pg(&range).expect("should have converted decimal range"); + + let (Some(pg_lower), Some(pg_upper)) = (pg.lower(), pg.upper()) else { + panic!("both PG bounds should have been Some"); + }; + + assert_eq!(rust_decimal::Decimal::new(12345, 2), pg_lower.value.0); + assert_eq!(postgres_range::BoundType::Inclusive, pg_lower.type_); + assert_eq!(rust_decimal::Decimal::new(456789789, 6), pg_upper.value.0); + assert_eq!(postgres_range::BoundType::Exclusive, pg_upper.type_); + } + + #[test] + fn can_convert_decimal_range_pg_to_wit() { + let lo = rust_decimal::Decimal::new(123456, 3); + let hi = rust_decimal::Decimal::new(789987654, 2); + + let pg = postgres_range::Range::new( + Some(postgres_range::RangeBound::new( + RangeableDecimal(lo), + postgres_range::BoundType::Exclusive, + )), + Some(postgres_range::RangeBound::new( + RangeableDecimal(hi), + postgres_range::BoundType::Inclusive, + )), + ); + + let range = decimal_range_pg_to_wit(pg); + + let (Some(lower), Some(upper)) = range else { + panic!("both WIT bounds should have been Some"); + }; + + assert_eq!("123.456", lower.0); + assert_eq!(v4::RangeBoundKind::Exclusive, lower.1); + assert_eq!("7899876.54", upper.0); + assert_eq!(v4::RangeBoundKind::Inclusive, upper.1); + } + + #[test] + fn can_convert_decimal_array_wit_to_pg() { + let arr = vec![ + Some("12.34".to_string()), + None, + Some("123456789.987654321".to_string()), + ]; + + let pg = decimal_array_wit_to_pg(&arr).expect("should have converted decimal array"); + + assert_eq!(arr.len(), pg.len()); + assert_eq!( + rust_decimal::Decimal::new(1234, 2), + *pg[0].as_ref().expect("some should convert to some") + ); + assert!(pg[1].is_none(), "none should convert to none"); + assert_eq!( + rust_decimal::Decimal::new(123456789987654321, 9), + *pg[2].as_ref().expect("some should convert to some") + ); + } + + #[test] + fn can_convert_decimal_array_pg_to_wit() { + let pg = vec![ + Some(rust_decimal::Decimal::new(1234, 2)), + None, + Some(rust_decimal::Decimal::new(123456789987654321, 9)), + ]; + + let arr = decimal_array_pg_to_wit(pg); + + assert_eq!(3, arr.len()); + assert_eq!( + "12.34", + *arr[0].as_ref().expect("some should convert to some") + ); + assert!(arr[1].is_none(), "none should convert to none"); + assert_eq!( + "123456789.987654321", + *arr[2].as_ref().expect("some should convert to some") + ); + } +} diff --git a/crates/factor-outbound-pg/src/types/decimal.rs b/crates/factor-outbound-pg/src/types/decimal.rs new file mode 100644 index 0000000000..ea7aa7d4c5 --- /dev/null +++ b/crates/factor-outbound-pg/src/types/decimal.rs @@ -0,0 +1,59 @@ +use anyhow::Result; +use tokio_postgres::types::{FromSql, ToSql, Type}; + +/// Wraps the `Decimal` type to allow its use in postgres_range::Range. +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct RangeableDecimal(pub rust_decimal::Decimal); + +impl ToSql for RangeableDecimal { + tokio_postgres::types::to_sql_checked!(); + + fn to_sql( + &self, + ty: &Type, + out: &mut tokio_postgres::types::private::BytesMut, + ) -> Result> + where + Self: Sized, + { + self.0.to_sql(ty, out) + } + + fn accepts(ty: &Type) -> bool + where + Self: Sized, + { + ::accepts(ty) + } +} + +impl FromSql<'_> for RangeableDecimal { + fn from_sql( + ty: &Type, + raw: &'_ [u8], + ) -> std::result::Result> { + let d = ::from_sql(ty, raw)?; + Ok(Self(d)) + } + + fn accepts(ty: &Type) -> bool { + ::accepts(ty) + } +} + +impl postgres_range::Normalizable for RangeableDecimal { + fn normalize( + bound: postgres_range::RangeBound, + ) -> postgres_range::RangeBound + where + S: postgres_range::BoundSided, + { + bound + } +} + +impl std::fmt::Display for RangeableDecimal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/crates/factor-outbound-pg/src/types/interval.rs b/crates/factor-outbound-pg/src/types/interval.rs new file mode 100644 index 0000000000..f494070a47 --- /dev/null +++ b/crates/factor-outbound-pg/src/types/interval.rs @@ -0,0 +1,92 @@ +use anyhow::Result; +use spin_world::spin::postgres4_0_0::postgres::{self as v4}; +use tokio_postgres::types::{FromSql, ToSql, Type}; + +#[derive(Debug)] +pub struct Interval(pub v4::Interval); + +impl ToSql for Interval { + tokio_postgres::types::to_sql_checked!(); + + fn to_sql( + &self, + _ty: &Type, + out: &mut tokio_postgres::types::private::BytesMut, + ) -> Result> + where + Self: Sized, + { + use bytes::BufMut; + + out.put_i64(self.0.micros); + out.put_i32(self.0.days); + out.put_i32(self.0.months); + + Ok(tokio_postgres::types::IsNull::No) + } + + fn accepts(ty: &Type) -> bool + where + Self: Sized, + { + matches!(ty, &Type::INTERVAL) + } +} + +impl FromSql<'_> for Interval { + fn from_sql( + _ty: &Type, + raw: &'_ [u8], + ) -> std::result::Result> { + const EXPECTED_LEN: usize = size_of::() + size_of::() + size_of::(); + + if raw.len() != EXPECTED_LEN { + return Err(Box::new(IntervalLengthError)); + } + + let (micro_bytes, rest) = raw.split_at(size_of::()); + let (day_bytes, rest) = rest.split_at(size_of::()); + let month_bytes = rest; + let months = i32::from_be_bytes(month_bytes.try_into().unwrap()); + let days = i32::from_be_bytes(day_bytes.try_into().unwrap()); + let micros = i64::from_be_bytes(micro_bytes.try_into().unwrap()); + + Ok(Self(v4::Interval { + micros, + days, + months, + })) + } + + fn accepts(ty: &Type) -> bool { + matches!(ty, &Type::INTERVAL) + } +} + +struct IntervalLengthError; + +impl std::error::Error for IntervalLengthError {} + +impl std::fmt::Display for IntervalLengthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("unexpected binary format for Postgres INTERVAL") + } +} + +impl std::fmt::Debug for IntervalLengthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + +impl From for v4::Interval { + fn from(value: Interval) -> Self { + value.0 + } +} + +impl From for Interval { + fn from(value: v4::Interval) -> Self { + Self(value) + } +} diff --git a/crates/factor-outbound-pg/src/types/pg_null.rs b/crates/factor-outbound-pg/src/types/pg_null.rs new file mode 100644 index 0000000000..1e0cd5ff86 --- /dev/null +++ b/crates/factor-outbound-pg/src/types/pg_null.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use tokio_postgres::types::{ToSql, Type}; + +/// Although the Postgres crate converts Rust Option::None to Postgres NULL, +/// it enforces the type of the Option as it does so. (For example, trying to +/// pass an Option::::None to a VARCHAR column fails conversion.) As we +/// do not know expected column types, we instead use a "neutral" custom type +/// which allows conversion to any type but always tells the Postgres crate to +/// treat it as a SQL NULL. +pub struct PgNull; + +impl ToSql for PgNull { + fn to_sql( + &self, + _ty: &Type, + _out: &mut tokio_postgres::types::private::BytesMut, + ) -> Result> + where + Self: Sized, + { + Ok(tokio_postgres::types::IsNull::Yes) + } + + fn accepts(_ty: &Type) -> bool + where + Self: Sized, + { + true + } + + fn to_sql_checked( + &self, + _ty: &Type, + _out: &mut tokio_postgres::types::private::BytesMut, + ) -> Result> { + Ok(tokio_postgres::types::IsNull::Yes) + } +} + +impl std::fmt::Debug for PgNull { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NULL").finish() + } +} From 86ea8dfc50198e8b85d5df8d87e29fff2d66ef88 Mon Sep 17 00:00:00 2001 From: itowlson Date: Fri, 27 Jun 2025 13:27:20 +1200 Subject: [PATCH 7/9] Return unsupported Postgres types as raw blobs Signed-off-by: itowlson --- crates/factor-outbound-pg/src/types.rs | 6 ++++-- crates/factor-outbound-pg/src/types/other.rs | 22 ++++++++++++++++++++ crates/world/src/conversions.rs | 12 +++++------ wit/deps/spin-postgres@4.0.0/postgres.wit | 6 +++--- 4 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 crates/factor-outbound-pg/src/types/other.rs diff --git a/crates/factor-outbound-pg/src/types.rs b/crates/factor-outbound-pg/src/types.rs index 39b08c8d9a..c3b586892f 100644 --- a/crates/factor-outbound-pg/src/types.rs +++ b/crates/factor-outbound-pg/src/types.rs @@ -5,6 +5,7 @@ use tokio_postgres::{types::ToSql, Row}; mod convert; mod decimal; mod interval; +mod other; mod pg_null; use convert::{ @@ -14,6 +15,7 @@ use convert::{ range_wit_to_pg, time_pg_to_wit, time_wit_to_pg, timestamp_wit_to_pg, uuid_wit_to_pg, }; use interval::Interval; +use other::Other; use pg_null::PgNull; pub fn convert_data_type(pg_type: &Type) -> DbDataType { @@ -42,7 +44,7 @@ pub fn convert_data_type(pg_type: &Type) -> DbDataType { Type::INTERVAL => DbDataType::Interval, _ => { tracing::debug!("Couldn't convert Postgres type {} to WIT", pg_type.name(),); - DbDataType::Other + DbDataType::Other(pg_type.name().to_owned()) } } } @@ -126,7 +128,7 @@ pub fn convert_entry(row: &Row, index: usize) -> anyhow::Result { t.name(), column.name() ); - Ok(DbValue::Unsupported) + map_db_value(row, index, DbValue::Unsupported, |v: Other| v.into()) } } } diff --git a/crates/factor-outbound-pg/src/types/other.rs b/crates/factor-outbound-pg/src/types/other.rs new file mode 100644 index 0000000000..57123a9c66 --- /dev/null +++ b/crates/factor-outbound-pg/src/types/other.rs @@ -0,0 +1,22 @@ +/// A catch-all type via which we return unsupported Postgres values as blobs. +#[derive(Debug)] +pub struct Other(Vec); + +impl tokio_postgres::types::FromSql<'_> for Other { + fn from_sql( + _ty: &tokio_postgres::types::Type, + raw: &'_ [u8], + ) -> Result> { + Ok(Self(raw.to_owned())) + } + + fn accepts(_ty: &tokio_postgres::types::Type) -> bool { + true + } +} + +impl From for Vec { + fn from(value: Other) -> Self { + value.0 + } +} diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index b5aba5473b..0434072818 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -76,7 +76,7 @@ mod rdbms_types { pg4::DbValue::Str(s) => v1::rdbms_types::DbValue::Str(s), pg4::DbValue::Binary(b) => v1::rdbms_types::DbValue::Binary(b), pg4::DbValue::DbNull => v1::rdbms_types::DbValue::DbNull, - pg4::DbValue::Unsupported => v1::rdbms_types::DbValue::Unsupported, + pg4::DbValue::Unsupported(_) => v1::rdbms_types::DbValue::Unsupported, _ => v1::rdbms_types::DbValue::Unsupported, } } @@ -95,7 +95,7 @@ mod rdbms_types { pg4::DbValue::Str(s) => v2::rdbms_types::DbValue::Str(s), pg4::DbValue::Binary(b) => v2::rdbms_types::DbValue::Binary(b), pg4::DbValue::DbNull => v2::rdbms_types::DbValue::DbNull, - pg4::DbValue::Unsupported => v2::rdbms_types::DbValue::Unsupported, + pg4::DbValue::Unsupported(_) => v2::rdbms_types::DbValue::Unsupported, _ => v2::rdbms_types::DbValue::Unsupported, } } @@ -129,7 +129,7 @@ mod rdbms_types { pg4::DbValue::ArrayStr(_) => pg3::DbValue::Unsupported, pg4::DbValue::Interval(_) => pg3::DbValue::Unsupported, pg4::DbValue::DbNull => pg3::DbValue::DbNull, - pg4::DbValue::Unsupported => pg3::DbValue::Unsupported, + pg4::DbValue::Unsupported(_) => pg3::DbValue::Unsupported, } } } @@ -146,7 +146,7 @@ mod rdbms_types { pg4::DbDataType::Floating64 => v1::rdbms_types::DbDataType::Floating64, pg4::DbDataType::Str => v1::rdbms_types::DbDataType::Str, pg4::DbDataType::Binary => v1::rdbms_types::DbDataType::Binary, - pg4::DbDataType::Other => v1::rdbms_types::DbDataType::Other, + pg4::DbDataType::Other(_) => v1::rdbms_types::DbDataType::Other, _ => v1::rdbms_types::DbDataType::Other, } } @@ -164,7 +164,7 @@ mod rdbms_types { pg4::DbDataType::Floating64 => v2::rdbms_types::DbDataType::Floating64, pg4::DbDataType::Str => v2::rdbms_types::DbDataType::Str, pg4::DbDataType::Binary => v2::rdbms_types::DbDataType::Binary, - pg4::DbDataType::Other => v2::rdbms_types::DbDataType::Other, + pg4::DbDataType::Other(_) => v2::rdbms_types::DbDataType::Other, _ => v2::rdbms_types::DbDataType::Other, } } @@ -197,7 +197,7 @@ mod rdbms_types { pg4::DbDataType::ArrayDecimal => pg3::DbDataType::Other, pg4::DbDataType::ArrayStr => pg3::DbDataType::Other, pg4::DbDataType::Interval => pg3::DbDataType::Other, - pg4::DbDataType::Other => pg3::DbDataType::Other, + pg4::DbDataType::Other(_) => pg3::DbDataType::Other, } } } diff --git a/wit/deps/spin-postgres@4.0.0/postgres.wit b/wit/deps/spin-postgres@4.0.0/postgres.wit index 3ebc744514..5a34097de9 100644 --- a/wit/deps/spin-postgres@4.0.0/postgres.wit +++ b/wit/deps/spin-postgres@4.0.0/postgres.wit @@ -11,7 +11,7 @@ interface postgres { } /// Data types for a database column - enum db-data-type { + variant db-data-type { boolean, int8, int16, @@ -36,7 +36,7 @@ interface postgres { array-decimal, array-str, interval, - other, + other(string), } /// Database values @@ -69,7 +69,7 @@ interface postgres { array-str(list>), interval(interval), db-null, - unsupported, + unsupported(list), } /// Values used in parameterized queries From 69fb432440117aca425f51fbbaedaa172ff33137 Mon Sep 17 00:00:00 2001 From: itowlson Date: Mon, 14 Jul 2025 13:01:44 +1200 Subject: [PATCH 8/9] Structured errors for PostgreSQL DB errors Signed-off-by: itowlson --- crates/factor-outbound-pg/src/client.rs | 48 +++++++++++++++++++++-- crates/world/src/conversions.rs | 13 ++++-- wit/deps/spin-postgres@4.0.0/postgres.wit | 20 +++++++++- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index ae4fb796da..da0ad4f205 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -29,6 +29,48 @@ pub trait Client { ) -> Result; } +/// Extract weak-typed error data for WIT purposes +fn pg_extras(dbe: &tokio_postgres::error::DbError) -> Vec<(String, String)> { + let mut extras = vec![]; + + macro_rules! pg_extra { + ( $n:ident ) => { + if let Some(value) = dbe.$n() { + extras.push((stringify!($n).to_owned(), value.to_string())); + } + }; + } + + pg_extra!(column); + pg_extra!(constraint); + pg_extra!(routine); + pg_extra!(hint); + pg_extra!(table); + pg_extra!(datatype); + pg_extra!(schema); + pg_extra!(file); + pg_extra!(line); + pg_extra!(where_); + + extras +} + +fn query_failed(e: tokio_postgres::error::Error) -> v4::Error { + let flattened = format!("{e:?}"); + let query_error = match e.as_db_error() { + None => v4::QueryError::Text(flattened), + Some(dbe) => v4::QueryError::DbError(v4::DbError { + as_text: flattened, + severity: dbe.severity().to_owned(), + code: dbe.code().code().to_owned(), + message: dbe.message().to_owned(), + detail: dbe.detail().map(|s| s.to_owned()), + extras: pg_extras(dbe), + }), + }; + v4::Error::QueryFailed(query_error) +} + #[async_trait] impl Client for TokioClient { async fn build_client(address: &str) -> Result @@ -70,7 +112,7 @@ impl Client for TokioClient { self.execute(&statement, params_refs.as_slice()) .await - .map_err(|e| v4::Error::QueryFailed(format!("{e:?}"))) + .map_err(query_failed) } async fn query( @@ -92,7 +134,7 @@ impl Client for TokioClient { let results = self .query(&statement, params_refs.as_slice()) .await - .map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?; + .map_err(query_failed)?; if results.is_empty() { return Ok(RowSet { @@ -106,7 +148,7 @@ impl Client for TokioClient { .iter() .map(convert_row) .collect::, _>>() - .map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?; + .map_err(|e| v4::Error::QueryFailed(v4::QueryError::Text(format!("{e:?}"))))?; Ok(RowSet { columns, rows }) } diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 0434072818..8177b623b2 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -376,7 +376,7 @@ mod rdbms_types { match error { pg4::Error::ConnectionFailed(e) => v1::postgres::PgError::ConnectionFailed(e), pg4::Error::BadParameter(e) => v1::postgres::PgError::BadParameter(e), - pg4::Error::QueryFailed(e) => v1::postgres::PgError::QueryFailed(e), + pg4::Error::QueryFailed(e) => v1::postgres::PgError::QueryFailed(pg_error_text(e)), pg4::Error::ValueConversionFailed(e) => { v1::postgres::PgError::ValueConversionFailed(e) } @@ -390,7 +390,7 @@ mod rdbms_types { match error { pg4::Error::ConnectionFailed(e) => v2::rdbms_types::Error::ConnectionFailed(e), pg4::Error::BadParameter(e) => v2::rdbms_types::Error::BadParameter(e), - pg4::Error::QueryFailed(e) => v2::rdbms_types::Error::QueryFailed(e), + pg4::Error::QueryFailed(e) => v2::rdbms_types::Error::QueryFailed(pg_error_text(e)), pg4::Error::ValueConversionFailed(e) => { v2::rdbms_types::Error::ValueConversionFailed(e) } @@ -404,12 +404,19 @@ mod rdbms_types { match error { pg4::Error::ConnectionFailed(e) => pg3::Error::ConnectionFailed(e), pg4::Error::BadParameter(e) => pg3::Error::BadParameter(e), - pg4::Error::QueryFailed(e) => pg3::Error::QueryFailed(e), + pg4::Error::QueryFailed(e) => pg3::Error::QueryFailed(pg_error_text(e)), pg4::Error::ValueConversionFailed(e) => pg3::Error::ValueConversionFailed(e), pg4::Error::Other(e) => pg3::Error::Other(e), } } } + + pub fn pg_error_text(error: pg4::QueryError) -> String { + match error { + pg4::QueryError::Text(text) => text, + pg4::QueryError::DbError(e) => e.as_text, + } + } } mod postgres { diff --git a/wit/deps/spin-postgres@4.0.0/postgres.wit b/wit/deps/spin-postgres@4.0.0/postgres.wit index 5a34097de9..652703a88f 100644 --- a/wit/deps/spin-postgres@4.0.0/postgres.wit +++ b/wit/deps/spin-postgres@4.0.0/postgres.wit @@ -5,11 +5,29 @@ interface postgres { variant error { connection-failed(string), bad-parameter(string), - query-failed(string), + query-failed(query-error), value-conversion-failed(string), other(string) } + variant query-error { + /// An error occurred but we do not have structured info for it + text(string), + /// Postgres returned a structured database error + db-error(db-error), + } + + record db-error { + /// Stringised version of the error. This is primarily to facilitate migration of older code. + as-text: string, + severity: string, + code: string, + message: string, + detail: option, + /// Any error information provided by Postgres and not captured above. + extras: list>, + } + /// Data types for a database column variant db-data-type { boolean, From 246e55a1c63eaa2af634de9320c80724093bda57 Mon Sep 17 00:00:00 2001 From: itowlson Date: Fri, 18 Jul 2025 14:17:46 +1200 Subject: [PATCH 9/9] Update conformance tests Signed-off-by: itowlson --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- tests/conformance-tests/src/lib.rs | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8cb66f82fc..894dd3fa04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1755,7 +1755,7 @@ dependencies = [ [[package]] name = "conformance-tests" version = "0.1.0" -source = "git+https://github.com/fermyon/conformance-tests?rev=ecd22a45bcc5c775a56c67689a89aa4005866ac0#ecd22a45bcc5c775a56c67689a89aa4005866ac0" +source = "git+https://github.com/itowlson/conformance-tests?rev=fa529a698616116beb024881cfd6fdeda7f39415#fa529a698616116beb024881cfd6fdeda7f39415" dependencies = [ "anyhow", "flate2", @@ -9439,7 +9439,7 @@ dependencies = [ [[package]] name = "test-environment" version = "0.1.0" -source = "git+https://github.com/fermyon/conformance-tests?rev=ecd22a45bcc5c775a56c67689a89aa4005866ac0#ecd22a45bcc5c775a56c67689a89aa4005866ac0" +source = "git+https://github.com/itowlson/conformance-tests?rev=fa529a698616116beb024881cfd6fdeda7f39415#fa529a698616116beb024881cfd6fdeda7f39415" dependencies = [ "anyhow", "fslock", diff --git a/Cargo.toml b/Cargo.toml index aef5c3dee4..1924c9c32e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ base64 = "0.22" bytes = "1" chrono = "0.4" clap = "3.2" -conformance-tests = { git = "https://github.com/fermyon/conformance-tests", rev = "ecd22a45bcc5c775a56c67689a89aa4005866ac0" } +conformance-tests = { git = "https://github.com/itowlson/conformance-tests", rev = "fa529a698616116beb024881cfd6fdeda7f39415" } ctrlc = { version = "3.4", features = ["termination"] } dialoguer = "0.11" dirs = "6.0" @@ -158,7 +158,7 @@ sha2 = "0.10" syn = "2" tar = "0.4" tempfile = "3" -test-environment = { git = "https://github.com/fermyon/conformance-tests", rev = "ecd22a45bcc5c775a56c67689a89aa4005866ac0" } +test-environment = { git = "https://github.com/itowlson/conformance-tests", rev = "fa529a698616116beb024881cfd6fdeda7f39415" } thiserror = "2" tokio = "1" tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"] } diff --git a/tests/conformance-tests/src/lib.rs b/tests/conformance-tests/src/lib.rs index a030848196..24403789d3 100644 --- a/tests/conformance-tests/src/lib.rs +++ b/tests/conformance-tests/src/lib.rs @@ -31,6 +31,14 @@ pub fn run_test( return Ok(()); } } + conformance_tests::config::Precondition::Postgres => { + if should_run_docker_based_tests() { + services.push("postgres") + } else { + // Skip the test if docker is not installed. + return Ok(()); + } + } conformance_tests::config::Precondition::KeyValueStore(_) => {} conformance_tests::config::Precondition::Sqlite => {} }