From e14e1e4dc5dfb080a68e67422016cd64639bbebb Mon Sep 17 00:00:00 2001 From: itowlson Date: Thu, 17 Oct 2024 16:07:06 +1300 Subject: [PATCH] Initial commit (untested) of Postgres date-time types Signed-off-by: itowlson --- Cargo.lock | 2 + crates/factor-outbound-pg/Cargo.toml | 3 +- crates/factor-outbound-pg/src/client.rs | 70 +++- crates/factor-outbound-pg/src/host.rs | 125 ++++-- crates/factor-outbound-pg/src/lib.rs | 1 + .../factor-outbound-pg/tests/factor_test.rs | 8 +- crates/world/src/conversions.rs | 378 +++++++++++++++++- crates/world/src/lib.rs | 2 + wit/deps/spin-postgres@3.0.0/postgres.wit | 17 + wit/deps/spin-postgres@3.0.0/rdbms-types.wit | 92 +++++ wit/deps/spin@3.0.0/world.wit | 1 + 11 files changed, 646 insertions(+), 53 deletions(-) create mode 100644 wit/deps/spin-postgres@3.0.0/postgres.wit create mode 100644 wit/deps/spin-postgres@3.0.0/rdbms-types.wit diff --git a/Cargo.lock b/Cargo.lock index dcb490ed1f..a7fcd7db7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5401,6 +5401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" dependencies = [ "bytes", + "chrono", "fallible-iterator 0.2.0", "postgres-protocol", ] @@ -7206,6 +7207,7 @@ name = "spin-factor-outbound-pg" version = "2.8.0-pre0" dependencies = [ "anyhow", + "chrono", "native-tls", "postgres-native-tls", "spin-core", diff --git a/crates/factor-outbound-pg/Cargo.toml b/crates/factor-outbound-pg/Cargo.toml index 5a621b436a..47899aee91 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 } +chrono = "0.4" native-tls = "0.2" postgres-native-tls = "0.5" spin-core = { path = "../core" } @@ -14,7 +15,7 @@ spin-factors = { path = "../factors" } spin-resource-table = { path = "../table" } spin-world = { path = "../world" } tokio = { workspace = true, features = ["rt-multi-thread"] } -tokio-postgres = "0.7" +tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] } tracing = { workspace = true } [dev-dependencies] diff --git a/crates/factor-outbound-pg/src/client.rs b/crates/factor-outbound-pg/src/client.rs index 06a93a6311..e1d4029a5f 100644 --- a/crates/factor-outbound-pg/src/client.rs +++ b/crates/factor-outbound-pg/src/client.rs @@ -2,8 +2,9 @@ use anyhow::{anyhow, Result}; use native_tls::TlsConnector; use postgres_native_tls::MakeTlsConnector; use spin_world::async_trait; -use spin_world::v2::postgres::{self as v2}; -use spin_world::v2::rdbms_types::{Column, DbDataType, DbValue, ParameterValue, RowSet}; +use spin_world::spin::postgres::rdbms_types::{ + self as v2, Column, DbDataType, DbValue, ParameterValue, RowSet, +}; use tokio_postgres::types::Type; use tokio_postgres::{config::SslMode, types::ToSql, Row}; use tokio_postgres::{Client as TokioClient, NoTls, Socket}; @@ -55,13 +56,18 @@ impl Client for TokioClient { statement: String, params: Vec, ) -> Result { - let params: Vec<&(dyn ToSql + Sync)> = params + let params = params .iter() .map(to_sql_parameter) .collect::>>() .map_err(|e| v2::Error::ValueConversionFailed(format!("{:?}", e)))?; - self.execute(&statement, params.as_slice()) + let params_refs: Vec<&(dyn ToSql + Sync)> = params + .iter() + .map(|b| b.as_ref() as &(dyn ToSql + Sync)) + .collect(); + + self.execute(&statement, params_refs.as_slice()) .await .map_err(|e| v2::Error::QueryFailed(format!("{:?}", e))) } @@ -71,14 +77,19 @@ impl Client for TokioClient { statement: String, params: Vec, ) -> Result { - let params: Vec<&(dyn ToSql + Sync)> = params + let params = params .iter() .map(to_sql_parameter) .collect::>>() .map_err(|e| v2::Error::BadParameter(format!("{:?}", e)))?; + let params_refs: Vec<&(dyn ToSql + Sync)> = params + .iter() + .map(|b| b.as_ref() as &(dyn ToSql + Sync)) + .collect(); + let results = self - .query(&statement, params.as_slice()) + .query(&statement, params_refs.as_slice()) .await .map_err(|e| v2::Error::QueryFailed(format!("{:?}", e)))?; @@ -111,22 +122,47 @@ where }); } -fn to_sql_parameter(value: &ParameterValue) -> Result<&(dyn ToSql + Sync)> { +fn to_sql_parameter(value: &ParameterValue) -> Result> { match value { - ParameterValue::Boolean(v) => Ok(v), - ParameterValue::Int32(v) => Ok(v), - ParameterValue::Int64(v) => Ok(v), - ParameterValue::Int8(v) => Ok(v), - ParameterValue::Int16(v) => Ok(v), - ParameterValue::Floating32(v) => Ok(v), - ParameterValue::Floating64(v) => Ok(v), + 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::Uint8(_) | ParameterValue::Uint16(_) | ParameterValue::Uint32(_) | ParameterValue::Uint64(_) => Err(anyhow!("Postgres does not support unsigned integers")), - ParameterValue::Str(v) => Ok(v), - ParameterValue::Binary(v) => Ok(v), - ParameterValue::DbNull => Ok(&PgNull), + 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::DbNull => Ok(Box::new(PgNull)), } } diff --git a/crates/factor-outbound-pg/src/host.rs b/crates/factor-outbound-pg/src/host.rs index 09065d0eff..ed3f929021 100644 --- a/crates/factor-outbound-pg/src/host.rs +++ b/crates/factor-outbound-pg/src/host.rs @@ -1,10 +1,10 @@ use anyhow::Result; use spin_core::{async_trait, wasmtime::component::Resource}; +use spin_world::spin::postgres::{self as v3}; use spin_world::v1::postgres as v1; use spin_world::v1::rdbms_types as v1_types; -use spin_world::v2::postgres::{self as v2, Connection}; -use spin_world::v2::rdbms_types; -use spin_world::v2::rdbms_types::{ParameterValue, RowSet}; +use spin_world::v2::postgres::{self as v2}; +use spin_world::v2::rdbms_types as v2types; use tracing::field::Empty; use tracing::instrument; use tracing::Level; @@ -13,21 +13,27 @@ use crate::client::Client; use crate::InstanceState; impl InstanceState { - async fn open_connection(&mut self, address: &str) -> Result, v2::Error> { + async fn open_connection( + &mut self, + address: &str, + ) -> Result, v3::rdbms_types::Error> { self.connections .push( C::build_client(address) .await - .map_err(|e| v2::Error::ConnectionFailed(format!("{e:?}")))?, + .map_err(|e| v3::rdbms_types::Error::ConnectionFailed(format!("{e:?}")))?, ) - .map_err(|_| v2::Error::ConnectionFailed("too many connections".into())) + .map_err(|_| v3::rdbms_types::Error::ConnectionFailed("too many connections".into())) .map(Resource::new_own) } - async fn get_client(&mut self, connection: Resource) -> Result<&C, v2::Error> { + async fn get_client( + &mut self, + connection: Resource, + ) -> Result<&C, v3::rdbms_types::Error> { self.connections .get(connection.rep()) - .ok_or_else(|| v2::Error::ConnectionFailed("no connection found".into())) + .ok_or_else(|| v3::rdbms_types::Error::ConnectionFailed("no connection found".into())) } async fn is_address_allowed(&self, address: &str) -> Result { @@ -60,20 +66,29 @@ impl InstanceState { } #[async_trait] -impl v2::Host for InstanceState {} +impl v3::postgres::Host for InstanceState {} + +fn v2_params_to_v3(params: Vec) -> Vec { + params.into_iter().map(|p| p.into()).collect() +} #[async_trait] -impl v2::HostConnection for InstanceState { +impl spin_world::spin::postgres::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))] - async fn open(&mut self, address: String) -> Result, v2::Error> { + async fn open( + &mut self, + address: String, + ) -> Result, v3::rdbms_types::Error> { spin_factor_outbound_networking::record_address_fields(&address); if !self .is_address_allowed(&address) .await - .map_err(|e| v2::Error::Other(e.to_string()))? + .map_err(|e| v3::rdbms_types::Error::Other(e.to_string()))? { - return Err(v2::Error::ConnectionFailed(format!( + return Err(v3::rdbms_types::Error::ConnectionFailed(format!( "address {address} is not permitted" ))); } @@ -83,10 +98,10 @@ impl v2::HostConnection for InstanceState { #[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, + connection: Resource, statement: String, - params: Vec, - ) -> Result { + params: Vec, + ) -> Result { Ok(self .get_client(connection) .await? @@ -97,10 +112,10 @@ impl v2::HostConnection for InstanceState { #[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, + connection: Resource, statement: String, - params: Vec, - ) -> Result { + params: Vec, + ) -> Result { Ok(self .get_client(connection) .await? @@ -108,22 +123,28 @@ impl v2::HostConnection for InstanceState { .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(()) } } -impl rdbms_types::Host for InstanceState { +impl v2types::Host for InstanceState { fn convert_error(&mut self, error: v2::Error) -> Result { Ok(error) } } -/// Delegate a function call to the v2::HostConnection implementation +impl v3::rdbms_types::Host for InstanceState { + fn convert_error(&mut self, error: v3::rdbms_types::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| v2::Error::Other(e.to_string()))? { + if !$self.is_address_allowed(&$address).await.map_err(|e| v3::rdbms_types::Error::Other(e.to_string()))? { return Err(v1::PgError::ConnectionFailed(format!( "address {} is not permitted", $address ))); @@ -132,12 +153,68 @@ 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()) }}; } +#[async_trait] +impl v2::Host for InstanceState {} + +#[async_trait] +impl v2::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, v2::Error> { + spin_factor_outbound_networking::record_address_fields(&address); + + if !self + .is_address_allowed(&address) + .await + .map_err(|e| v2::Error::Other(e.to_string()))? + { + return Err(v2::Error::ConnectionFailed(format!( + "address {address} is not permitted" + ))); + } + 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))] + async fn execute( + &mut self, + connection: Resource, + statement: String, + params: Vec, + ) -> Result { + Ok(self + .get_client(connection) + .await? + .execute(statement, v2_params_to_v3(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 { + Ok(self + .get_client(connection) + .await? + .query(statement, v2_params_to_v3(params)) + .await? + .into()) + } + + async fn drop(&mut self, connection: Resource) -> anyhow::Result<()> { + self.connections.remove(connection.rep()); + Ok(()) + } +} + #[async_trait] impl v1::Host for InstanceState { async fn execute( diff --git a/crates/factor-outbound-pg/src/lib.rs b/crates/factor-outbound-pg/src/lib.rs index 4061f4b7f5..4ca3663531 100644 --- a/crates/factor-outbound-pg/src/lib.rs +++ b/crates/factor-outbound-pg/src/lib.rs @@ -23,6 +23,7 @@ impl Factor for OutboundPgFactor { ) -> anyhow::Result<()> { ctx.link_bindings(spin_world::v1::postgres::add_to_linker)?; ctx.link_bindings(spin_world::v2::postgres::add_to_linker)?; + ctx.link_bindings(spin_world::spin::postgres::postgres::add_to_linker)?; Ok(()) } diff --git a/crates/factor-outbound-pg/tests/factor_test.rs b/crates/factor-outbound-pg/tests/factor_test.rs index b765d805f6..3d47c91a15 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::v2::postgres::HostConnection; -use spin_world::v2::postgres::{self as v2}; -use spin_world::v2::rdbms_types::Error as PgError; -use spin_world::v2::rdbms_types::{ParameterValue, RowSet}; +use spin_world::spin::postgres::postgres::HostConnection; +use spin_world::spin::postgres::postgres::{self as v2}; +use spin_world::spin::postgres::rdbms_types::Error as PgError; +use spin_world::spin::postgres::rdbms_types::{ParameterValue, RowSet}; #[derive(RuntimeFactors)] struct TestFactors { diff --git a/crates/world/src/conversions.rs b/crates/world/src/conversions.rs index 29b21f408a..2c64aeea57 100644 --- a/crates/world/src/conversions.rs +++ b/crates/world/src/conversions.rs @@ -12,6 +12,24 @@ mod rdbms_types { } } + impl From for v1::rdbms_types::Column { + fn from(value: spin::postgres::rdbms_types::Column) -> Self { + v1::rdbms_types::Column { + name: value.name, + data_type: value.data_type.into(), + } + } + } + + impl From for v2::rdbms_types::Column { + fn from(value: spin::postgres::rdbms_types::Column) -> Self { + v2::rdbms_types::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 { @@ -34,6 +52,192 @@ mod rdbms_types { } } + impl From for v1::rdbms_types::DbValue { + fn from(value: spin::postgres::rdbms_types::DbValue) -> v1::rdbms_types::DbValue { + match value { + spin::postgres::rdbms_types::DbValue::Boolean(b) => { + v1::rdbms_types::DbValue::Boolean(b) + } + spin::postgres::rdbms_types::DbValue::Int8(i) => v1::rdbms_types::DbValue::Int8(i), + spin::postgres::rdbms_types::DbValue::Int16(i) => { + v1::rdbms_types::DbValue::Int16(i) + } + spin::postgres::rdbms_types::DbValue::Int32(i) => { + v1::rdbms_types::DbValue::Int32(i) + } + spin::postgres::rdbms_types::DbValue::Int64(i) => { + v1::rdbms_types::DbValue::Int64(i) + } + spin::postgres::rdbms_types::DbValue::Uint8(j) => { + v1::rdbms_types::DbValue::Uint8(j) + } + spin::postgres::rdbms_types::DbValue::Uint16(u) => { + v1::rdbms_types::DbValue::Uint16(u) + } + spin::postgres::rdbms_types::DbValue::Uint32(u) => { + v1::rdbms_types::DbValue::Uint32(u) + } + spin::postgres::rdbms_types::DbValue::Uint64(u) => { + v1::rdbms_types::DbValue::Uint64(u) + } + spin::postgres::rdbms_types::DbValue::Floating32(r) => { + v1::rdbms_types::DbValue::Floating32(r) + } + spin::postgres::rdbms_types::DbValue::Floating64(r) => { + v1::rdbms_types::DbValue::Floating64(r) + } + spin::postgres::rdbms_types::DbValue::Str(s) => v1::rdbms_types::DbValue::Str(s), + spin::postgres::rdbms_types::DbValue::Binary(b) => { + v1::rdbms_types::DbValue::Binary(b) + } + spin::postgres::rdbms_types::DbValue::DbNull => v1::rdbms_types::DbValue::DbNull, + spin::postgres::rdbms_types::DbValue::Unsupported => { + v1::rdbms_types::DbValue::Unsupported + } + _ => v1::rdbms_types::DbValue::Unsupported, + } + } + } + + impl From for v2::rdbms_types::DbValue { + fn from(value: spin::postgres::rdbms_types::DbValue) -> v2::rdbms_types::DbValue { + match value { + spin::postgres::rdbms_types::DbValue::Boolean(b) => { + v2::rdbms_types::DbValue::Boolean(b) + } + spin::postgres::rdbms_types::DbValue::Int8(i) => v2::rdbms_types::DbValue::Int8(i), + spin::postgres::rdbms_types::DbValue::Int16(i) => { + v2::rdbms_types::DbValue::Int16(i) + } + spin::postgres::rdbms_types::DbValue::Int32(i) => { + v2::rdbms_types::DbValue::Int32(i) + } + spin::postgres::rdbms_types::DbValue::Int64(i) => { + v2::rdbms_types::DbValue::Int64(i) + } + spin::postgres::rdbms_types::DbValue::Uint8(j) => { + v2::rdbms_types::DbValue::Uint8(j) + } + spin::postgres::rdbms_types::DbValue::Uint16(u) => { + v2::rdbms_types::DbValue::Uint16(u) + } + spin::postgres::rdbms_types::DbValue::Uint32(u) => { + v2::rdbms_types::DbValue::Uint32(u) + } + spin::postgres::rdbms_types::DbValue::Uint64(u) => { + v2::rdbms_types::DbValue::Uint64(u) + } + spin::postgres::rdbms_types::DbValue::Floating32(r) => { + v2::rdbms_types::DbValue::Floating32(r) + } + spin::postgres::rdbms_types::DbValue::Floating64(r) => { + v2::rdbms_types::DbValue::Floating64(r) + } + spin::postgres::rdbms_types::DbValue::Str(s) => v2::rdbms_types::DbValue::Str(s), + spin::postgres::rdbms_types::DbValue::Binary(b) => { + v2::rdbms_types::DbValue::Binary(b) + } + spin::postgres::rdbms_types::DbValue::DbNull => v2::rdbms_types::DbValue::DbNull, + spin::postgres::rdbms_types::DbValue::Unsupported => { + v2::rdbms_types::DbValue::Unsupported + } + _ => v2::rdbms_types::DbValue::Unsupported, + } + } + } + + impl From for v1::rdbms_types::DbDataType { + fn from(value: spin::postgres::rdbms_types::DbDataType) -> v1::rdbms_types::DbDataType { + match value { + spin::postgres::rdbms_types::DbDataType::Boolean => { + v1::rdbms_types::DbDataType::Boolean + } + spin::postgres::rdbms_types::DbDataType::Int8 => v1::rdbms_types::DbDataType::Int8, + spin::postgres::rdbms_types::DbDataType::Int16 => { + v1::rdbms_types::DbDataType::Int16 + } + spin::postgres::rdbms_types::DbDataType::Int32 => { + v1::rdbms_types::DbDataType::Int32 + } + spin::postgres::rdbms_types::DbDataType::Int64 => { + v1::rdbms_types::DbDataType::Int64 + } + spin::postgres::rdbms_types::DbDataType::Uint8 => { + v1::rdbms_types::DbDataType::Uint8 + } + spin::postgres::rdbms_types::DbDataType::Uint16 => { + v1::rdbms_types::DbDataType::Uint16 + } + spin::postgres::rdbms_types::DbDataType::Uint32 => { + v1::rdbms_types::DbDataType::Uint32 + } + spin::postgres::rdbms_types::DbDataType::Uint64 => { + v1::rdbms_types::DbDataType::Uint64 + } + spin::postgres::rdbms_types::DbDataType::Floating32 => { + v1::rdbms_types::DbDataType::Floating32 + } + spin::postgres::rdbms_types::DbDataType::Floating64 => { + v1::rdbms_types::DbDataType::Floating64 + } + spin::postgres::rdbms_types::DbDataType::Str => v1::rdbms_types::DbDataType::Str, + spin::postgres::rdbms_types::DbDataType::Binary => { + v1::rdbms_types::DbDataType::Binary + } + spin::postgres::rdbms_types::DbDataType::Other => { + v1::rdbms_types::DbDataType::Other + } + _ => v1::rdbms_types::DbDataType::Other, + } + } + } + + impl From for v2::rdbms_types::DbDataType { + fn from(value: spin::postgres::rdbms_types::DbDataType) -> v2::rdbms_types::DbDataType { + match value { + spin::postgres::rdbms_types::DbDataType::Boolean => { + v2::rdbms_types::DbDataType::Boolean + } + spin::postgres::rdbms_types::DbDataType::Int8 => v2::rdbms_types::DbDataType::Int8, + spin::postgres::rdbms_types::DbDataType::Int16 => { + v2::rdbms_types::DbDataType::Int16 + } + spin::postgres::rdbms_types::DbDataType::Int32 => { + v2::rdbms_types::DbDataType::Int32 + } + spin::postgres::rdbms_types::DbDataType::Int64 => { + v2::rdbms_types::DbDataType::Int64 + } + spin::postgres::rdbms_types::DbDataType::Uint8 => { + v2::rdbms_types::DbDataType::Uint8 + } + spin::postgres::rdbms_types::DbDataType::Uint16 => { + v2::rdbms_types::DbDataType::Uint16 + } + spin::postgres::rdbms_types::DbDataType::Uint32 => { + v2::rdbms_types::DbDataType::Uint32 + } + spin::postgres::rdbms_types::DbDataType::Uint64 => { + v2::rdbms_types::DbDataType::Uint64 + } + spin::postgres::rdbms_types::DbDataType::Floating32 => { + v2::rdbms_types::DbDataType::Floating32 + } + spin::postgres::rdbms_types::DbDataType::Floating64 => { + v2::rdbms_types::DbDataType::Floating64 + } + spin::postgres::rdbms_types::DbDataType::Str => v2::rdbms_types::DbDataType::Str, + spin::postgres::rdbms_types::DbDataType::Binary => { + v2::rdbms_types::DbDataType::Binary + } + spin::postgres::rdbms_types::DbDataType::Other => { + v2::rdbms_types::DbDataType::Other + } + _ => v2::rdbms_types::DbDataType::Other, + } + } + } + impl From for v1::rdbms_types::DbDataType { fn from(value: v2::rdbms_types::DbDataType) -> v1::rdbms_types::DbDataType { match value { @@ -100,6 +304,108 @@ mod rdbms_types { } } + impl From for spin::postgres::rdbms_types::ParameterValue { + fn from( + value: v1::rdbms_types::ParameterValue, + ) -> spin::postgres::rdbms_types::ParameterValue { + match value { + v1::rdbms_types::ParameterValue::Boolean(b) => { + spin::postgres::rdbms_types::ParameterValue::Boolean(b) + } + v1::rdbms_types::ParameterValue::Int8(i) => { + spin::postgres::rdbms_types::ParameterValue::Int8(i) + } + v1::rdbms_types::ParameterValue::Int16(i) => { + spin::postgres::rdbms_types::ParameterValue::Int16(i) + } + v1::rdbms_types::ParameterValue::Int32(i) => { + spin::postgres::rdbms_types::ParameterValue::Int32(i) + } + v1::rdbms_types::ParameterValue::Int64(i) => { + spin::postgres::rdbms_types::ParameterValue::Int64(i) + } + v1::rdbms_types::ParameterValue::Uint8(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint8(u) + } + v1::rdbms_types::ParameterValue::Uint16(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint16(u) + } + v1::rdbms_types::ParameterValue::Uint32(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint32(u) + } + v1::rdbms_types::ParameterValue::Uint64(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint64(u) + } + v1::rdbms_types::ParameterValue::Floating32(r) => { + spin::postgres::rdbms_types::ParameterValue::Floating32(r) + } + v1::rdbms_types::ParameterValue::Floating64(r) => { + spin::postgres::rdbms_types::ParameterValue::Floating64(r) + } + v1::rdbms_types::ParameterValue::Str(s) => { + spin::postgres::rdbms_types::ParameterValue::Str(s) + } + v1::rdbms_types::ParameterValue::Binary(b) => { + spin::postgres::rdbms_types::ParameterValue::Binary(b) + } + v1::rdbms_types::ParameterValue::DbNull => { + spin::postgres::rdbms_types::ParameterValue::DbNull + } + } + } + } + + impl From for spin::postgres::rdbms_types::ParameterValue { + fn from( + value: v2::rdbms_types::ParameterValue, + ) -> spin::postgres::rdbms_types::ParameterValue { + match value { + v2::rdbms_types::ParameterValue::Boolean(b) => { + spin::postgres::rdbms_types::ParameterValue::Boolean(b) + } + v2::rdbms_types::ParameterValue::Int8(i) => { + spin::postgres::rdbms_types::ParameterValue::Int8(i) + } + v2::rdbms_types::ParameterValue::Int16(i) => { + spin::postgres::rdbms_types::ParameterValue::Int16(i) + } + v2::rdbms_types::ParameterValue::Int32(i) => { + spin::postgres::rdbms_types::ParameterValue::Int32(i) + } + v2::rdbms_types::ParameterValue::Int64(i) => { + spin::postgres::rdbms_types::ParameterValue::Int64(i) + } + v2::rdbms_types::ParameterValue::Uint8(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint8(u) + } + v2::rdbms_types::ParameterValue::Uint16(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint16(u) + } + v2::rdbms_types::ParameterValue::Uint32(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint32(u) + } + v2::rdbms_types::ParameterValue::Uint64(u) => { + spin::postgres::rdbms_types::ParameterValue::Uint64(u) + } + v2::rdbms_types::ParameterValue::Floating32(r) => { + spin::postgres::rdbms_types::ParameterValue::Floating32(r) + } + v2::rdbms_types::ParameterValue::Floating64(r) => { + spin::postgres::rdbms_types::ParameterValue::Floating64(r) + } + v2::rdbms_types::ParameterValue::Str(s) => { + spin::postgres::rdbms_types::ParameterValue::Str(s) + } + v2::rdbms_types::ParameterValue::Binary(b) => { + spin::postgres::rdbms_types::ParameterValue::Binary(b) + } + v2::rdbms_types::ParameterValue::DbNull => { + spin::postgres::rdbms_types::ParameterValue::DbNull + } + } + } + } + impl From for v1::mysql::MysqlError { fn from(error: v2::rdbms_types::Error) -> v1::mysql::MysqlError { match error { @@ -114,16 +420,74 @@ mod rdbms_types { } } - impl From for v1::postgres::PgError { - fn from(error: v2::rdbms_types::Error) -> v1::postgres::PgError { + impl From for v1::postgres::PgError { + fn from(error: spin::postgres::rdbms_types::Error) -> v1::postgres::PgError { match error { - v2::mysql::Error::ConnectionFailed(e) => v1::postgres::PgError::ConnectionFailed(e), - v2::mysql::Error::BadParameter(e) => v1::postgres::PgError::BadParameter(e), - v2::mysql::Error::QueryFailed(e) => v1::postgres::PgError::QueryFailed(e), - v2::mysql::Error::ValueConversionFailed(e) => { + spin::postgres::rdbms_types::Error::ConnectionFailed(e) => { + v1::postgres::PgError::ConnectionFailed(e) + } + spin::postgres::rdbms_types::Error::BadParameter(e) => { + v1::postgres::PgError::BadParameter(e) + } + spin::postgres::rdbms_types::Error::QueryFailed(e) => { + v1::postgres::PgError::QueryFailed(e) + } + spin::postgres::rdbms_types::Error::ValueConversionFailed(e) => { v1::postgres::PgError::ValueConversionFailed(e) } - v2::mysql::Error::Other(e) => v1::postgres::PgError::OtherError(e), + spin::postgres::rdbms_types::Error::Other(e) => { + v1::postgres::PgError::OtherError(e) + } + } + } + } + + impl From for v2::rdbms_types::Error { + fn from(error: spin::postgres::rdbms_types::Error) -> v2::rdbms_types::Error { + match error { + spin::postgres::rdbms_types::Error::ConnectionFailed(e) => { + v2::rdbms_types::Error::ConnectionFailed(e) + } + spin::postgres::rdbms_types::Error::BadParameter(e) => { + v2::rdbms_types::Error::BadParameter(e) + } + spin::postgres::rdbms_types::Error::QueryFailed(e) => { + v2::rdbms_types::Error::QueryFailed(e) + } + spin::postgres::rdbms_types::Error::ValueConversionFailed(e) => { + v2::rdbms_types::Error::ValueConversionFailed(e) + } + spin::postgres::rdbms_types::Error::Other(e) => v2::rdbms_types::Error::Other(e), + } + } + } +} + +mod postgres { + use super::*; + + impl From for v1::postgres::RowSet { + fn from(value: spin::postgres::postgres::RowSet) -> v1::postgres::RowSet { + v1::mysql::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(), + } + } + } + + impl From for v2::rdbms_types::RowSet { + fn from(value: spin::postgres::postgres::RowSet) -> v2::rdbms_types::RowSet { + v2::rdbms_types::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(), } } } diff --git a/crates/world/src/lib.rs b/crates/world/src/lib.rs index 0ecb57ee1d..39f843b5d4 100644 --- a/crates/world/src/lib.rs +++ b/crates/world/src/lib.rs @@ -1,4 +1,5 @@ #![allow(missing_docs)] +#![allow(non_camel_case_types)] // bindgen emits Host_Pre and Host_Indices pub use async_trait::async_trait; @@ -28,6 +29,7 @@ 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/rdbms-types/error" => spin::postgres::rdbms_types::Error, }, trappable_imports: true, }); diff --git a/wit/deps/spin-postgres@3.0.0/postgres.wit b/wit/deps/spin-postgres@3.0.0/postgres.wit new file mode 100644 index 0000000000..81341095ee --- /dev/null +++ b/wit/deps/spin-postgres@3.0.0/postgres.wit @@ -0,0 +1,17 @@ +package spin:postgres@3.0.0; + +interface postgres { + use rdbms-types.{parameter-value, row-set, error}; + + /// 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-postgres@3.0.0/rdbms-types.wit b/wit/deps/spin-postgres@3.0.0/rdbms-types.wit new file mode 100644 index 0000000000..3704ea230f --- /dev/null +++ b/wit/deps/spin-postgres@3.0.0/rdbms-types.wit @@ -0,0 +1,92 @@ +interface rdbms-types { + /// 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, + uint8, + uint16, + uint32, + uint64, + floating32, + floating64, + str, + binary, + date, + time, + datetime, + timestamp, + other, + } + + /// Database values + variant db-value { + boolean(bool), + int8(s8), + int16(s16), + int32(s32), + int64(s64), + uint8(u8), + uint16(u16), + uint32(u32), + uint64(u64), + floating32(float32), + floating64(float64), + str(string), + binary(list), + date(tuple), // (year, month, day) + time(tuple), // (hour, minute, second, nanosecond) + datetime(tuple), // (year, month, day, hour, minute, second, nanosecond) + timestamp(s64), // Unix timestamp (seconds since epoch) + db-null, + unsupported, + } + + /// Values used in parameterized queries + variant parameter-value { + boolean(bool), + int8(s8), + int16(s16), + int32(s32), + int64(s64), + uint8(u8), + uint16(u16), + uint32(u32), + uint64(u64), + floating32(float32), + floating64(float64), + str(string), + binary(list), + date(tuple), // (year, month, day) + time(tuple), // (hour, minute, second, nanosecond) + datetime(tuple), // (year, month, day, hour, minute, second, nanosecond) + timestamp(s64), // Unix timestamp (seconds since epoch) + 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, + } +} diff --git a/wit/deps/spin@3.0.0/world.wit b/wit/deps/spin@3.0.0/world.wit index e920055a9a..fd0b23e6b9 100644 --- a/wit/deps/spin@3.0.0/world.wit +++ b/wit/deps/spin@3.0.0/world.wit @@ -9,4 +9,5 @@ world http-trigger { /// The imports needed for a guest to run on a Spin host world platform { include fermyon:spin/platform@2.0.0; + import spin:postgres/postgres@3.0.0; }