From 502c40319749272f48ea86232dd0799ca55a7056 Mon Sep 17 00:00:00 2001 From: "Can H. Tartanoglu" Date: Thu, 30 Apr 2026 10:17:37 +0200 Subject: [PATCH] Add Postgres-only SQLx bindings crate --- sea-query-sqlx-postgres/Cargo.toml | 65 +++++ sea-query-sqlx-postgres/src/lib.rs | 14 + sea-query-sqlx-postgres/src/postgres.rs | 357 ++++++++++++++++++++++++ sea-query-sqlx-postgres/src/sqlx.rs | 28 ++ sea-query-sqlx-postgres/src/values.rs | 3 + 5 files changed, 467 insertions(+) create mode 100644 sea-query-sqlx-postgres/Cargo.toml create mode 100644 sea-query-sqlx-postgres/src/lib.rs create mode 100644 sea-query-sqlx-postgres/src/postgres.rs create mode 100644 sea-query-sqlx-postgres/src/sqlx.rs create mode 100644 sea-query-sqlx-postgres/src/values.rs diff --git a/sea-query-sqlx-postgres/Cargo.toml b/sea-query-sqlx-postgres/Cargo.toml new file mode 100644 index 000000000..009359a0e --- /dev/null +++ b/sea-query-sqlx-postgres/Cargo.toml @@ -0,0 +1,65 @@ +[workspace] +# A separate workspace + +[package] +name = "sea-query-sqlx-postgres" +version = "0.1.0" +authors = ["SeaQL Contributors"] +edition = "2024" +description = "PostgreSQL-only SQLx bindings for SeaQuery" +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/sea-query" +repository = "https://github.com/SeaQL/sea-query" +categories = ["database"] +keywords = ["database", "sql", "postgres", "sqlx"] +rust-version = "1.88.0" + +[lib] + +[dependencies] +sea-query = { version = "1.0.0-rc.33", path = "..", default-features = false, features = [ + "backend-postgres", + "thread-safe", +] } +sqlx-core = { version = "0.8", default-features = false } +sqlx-postgres = { version = "0.8", default-features = false } + +[features] +with-chrono = ["sea-query/with-chrono", "sqlx-core/chrono", "sqlx-postgres/chrono"] +with-json = ["sea-query/with-json", "sqlx-core/json", "sqlx-postgres/json"] +with-rust_decimal = [ + "sea-query/with-rust_decimal", + "sqlx-core/rust_decimal", + "sqlx-postgres/rust_decimal", +] +with-bigdecimal = [ + "sea-query/with-bigdecimal", + "sqlx-core/bigdecimal", + "sqlx-postgres/bigdecimal", +] +with-uuid = ["sea-query/with-uuid", "sqlx-core/uuid", "sqlx-postgres/uuid"] +with-time = ["sea-query/with-time", "sqlx-core/time", "sqlx-postgres/time"] +with-ipnetwork = [ + "sea-query/with-ipnetwork", + "sqlx-core/ipnetwork", + "sqlx-postgres/ipnetwork", +] +with-mac_address = [ + "sea-query/with-mac_address", + "sqlx-core/mac_address", + "sqlx-postgres/mac_address", +] +postgres-array = ["sea-query/postgres-array"] +postgres-vector = ["sea-query/postgres-vector"] +runtime-async-std = ["sqlx-core/_rt-async-std"] +runtime-async-std-native-tls = ["runtime-async-std", "sqlx-core/_tls-native-tls"] +runtime-async-std-rustls = ["runtime-async-std", "sqlx-core/_tls-rustls-ring-webpki"] +runtime-actix = ["runtime-tokio"] +runtime-actix-native-tls = ["runtime-tokio-native-tls"] +runtime-actix-rustls = ["runtime-tokio-rustls"] +runtime-tokio = ["sqlx-core/_rt-tokio"] +runtime-tokio-native-tls = ["runtime-tokio", "sqlx-core/_tls-native-tls"] +runtime-tokio-rustls = ["runtime-tokio", "sqlx-core/_tls-rustls-ring-webpki"] + +[package.metadata.docs.rs] +all-features = true diff --git a/sea-query-sqlx-postgres/src/lib.rs b/sea-query-sqlx-postgres/src/lib.rs new file mode 100644 index 000000000..e5eb9294f --- /dev/null +++ b/sea-query-sqlx-postgres/src/lib.rs @@ -0,0 +1,14 @@ +#![forbid(unsafe_code)] + +//! PostgreSQL-only SQLx bindings for SeaQuery. +//! +//! This crate mirrors the Postgres parts of `sea-query-sqlx`, but its manifest does not mention +//! the top-level `sqlx` facade or the MySQL/SQLite SQLx driver crates. That gives Postgres-only +//! users a dependency that does not record unrelated SQLx optional packages in their lockfile. + +mod postgres; +mod sqlx; +mod values; + +pub use crate::sqlx::SqlxBinder; +pub use crate::values::SqlxValues; diff --git a/sea-query-sqlx-postgres/src/postgres.rs b/sea-query-sqlx-postgres/src/postgres.rs new file mode 100644 index 000000000..70db5d1de --- /dev/null +++ b/sea-query-sqlx-postgres/src/postgres.rs @@ -0,0 +1,357 @@ +#[cfg(feature = "with-bigdecimal")] +use sea_query::prelude::BigDecimal; +#[cfg(feature = "with-rust_decimal")] +use sea_query::prelude::Decimal; +#[cfg(feature = "with-ipnetwork")] +use sea_query::prelude::IpNetwork; +#[cfg(feature = "with-json")] +use sea_query::prelude::Json; +#[cfg(feature = "with-mac_address")] +use sea_query::prelude::MacAddress; +#[cfg(feature = "with-uuid")] +use sea_query::prelude::Uuid; +#[cfg(feature = "with-time")] +use sea_query::prelude::time; +#[cfg(feature = "with-chrono")] +use sea_query::prelude::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc}; + +use sea_query::{ArrayType, OptionEnum, Value}; +use sqlx_core::arguments::{Arguments, IntoArguments}; +use sqlx_postgres::{PgArguments, Postgres}; + +use crate::SqlxValues; + +impl IntoArguments<'_, Postgres> for SqlxValues { + fn into_arguments(self) -> PgArguments { + let mut args = PgArguments::default(); + for arg in self.0.into_iter() { + match arg { + Value::Bool(b) => { + let _ = args.add(b); + } + Value::TinyInt(i) => { + let _ = args.add(i); + } + Value::SmallInt(i) => { + let _ = args.add(i); + } + Value::Int(i) => { + let _ = args.add(i); + } + Value::BigInt(i) => { + let _ = args.add(i); + } + Value::TinyUnsigned(i) => { + let _ = args.add(i.map(|i| i as i16)); + } + Value::SmallUnsigned(i) => { + let _ = args.add(i.map(|i| i as i32)); + } + Value::Unsigned(i) => { + let _ = args.add(i.map(|i| i as i64)); + } + Value::BigUnsigned(i) => { + let _ = args.add(i.map(|i| >::try_from(i).unwrap())); + } + Value::Float(f) => { + let _ = args.add(f); + } + Value::Double(d) => { + let _ = args.add(d); + } + Value::String(s) => { + let _ = args.add(s.as_deref()); + } + Value::Enum(e) => { + let value = match e { + OptionEnum::Some(v) => Some(v.value.into_owned()), + OptionEnum::None(_) => None, + }; + let _ = args.add(value); + } + Value::Char(c) => { + let _ = args.add(c.map(|c| c.to_string())); + } + Value::Bytes(b) => { + let _ = args.add(b.as_deref()); + } + #[cfg(feature = "with-chrono")] + Value::ChronoDate(d) => { + let _ = args.add(d); + } + #[cfg(feature = "with-chrono")] + Value::ChronoTime(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-chrono")] + Value::ChronoDateTime(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeUtc(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeLocal(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeWithTimeZone(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-time")] + Value::TimeDate(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-time")] + Value::TimeTime(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-time")] + Value::TimeDateTime(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-time")] + Value::TimeDateTimeWithTimeZone(t) => { + let _ = args.add(t); + } + #[cfg(feature = "with-uuid")] + Value::Uuid(uuid) => { + let _ = args.add(uuid); + } + #[cfg(feature = "with-rust_decimal")] + Value::Decimal(d) => { + let _ = args.add(d); + } + #[cfg(feature = "with-bigdecimal")] + Value::BigDecimal(d) => { + let _ = args.add(d.as_deref()); + } + #[cfg(feature = "with-json")] + Value::Json(j) => { + let _ = args.add(j.as_deref()); + } + #[cfg(feature = "with-ipnetwork")] + Value::IpNetwork(ip) => { + let _ = args.add(ip); + } + #[cfg(feature = "with-mac_address")] + Value::MacAddress(mac) => { + let _ = args.add(mac); + } + #[cfg(feature = "postgres-array")] + Value::Array(ty, v) => match ty { + ArrayType::Bool => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Bool"); + let _ = args.add(value); + } + ArrayType::TinyInt => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::TinyInt"); + let _ = args.add(value); + } + ArrayType::SmallInt => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::SmallInt"); + let _ = args.add(value); + } + ArrayType::Int => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Int"); + let _ = args.add(value); + } + ArrayType::BigInt => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::BigInt"); + let _ = args.add(value); + } + ArrayType::TinyUnsigned => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::TinyUnsigned"); + let value: Option> = + value.map(|vec| vec.into_iter().map(|i| i as i16).collect()); + let _ = args.add(value); + } + ArrayType::SmallUnsigned => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::SmallUnsigned"); + let value: Option> = + value.map(|vec| vec.into_iter().map(|i| i as i32).collect()); + let _ = args.add(value); + } + ArrayType::Unsigned => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Unsigned"); + let value: Option> = + value.map(|vec| vec.into_iter().map(|i| i as i64).collect()); + let _ = args.add(value); + } + ArrayType::BigUnsigned => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::BigUnsigned"); + let value: Option> = value.map(|vec| { + vec.into_iter() + .map(|i| >::try_from(i).unwrap()) + .collect() + }); + let _ = args.add(value); + } + ArrayType::Float => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Float"); + let _ = args.add(value); + } + ArrayType::Double => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Double"); + let _ = args.add(value); + } + ArrayType::String => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::String"); + let _ = args.add(value); + } + ArrayType::Enum(_) => { + let value: Option> = v.map(|values| { + values + .into_iter() + .map(|value| match value { + Value::Enum(OptionEnum::Some(value)) => value.value.into_owned(), + _ => { + panic!( + "Value::Array(ArrayType::Enum) should contain Value::Enum" + ); + } + }) + .collect() + }); + let _ = args.add(value); + } + ArrayType::Char => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Char"); + let value: Option> = + value.map(|vec| vec.into_iter().map(|c| c.to_string()).collect()); + let _ = args.add(value); + } + ArrayType::Bytes => { + let value: Option>> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Bytes"); + let _ = args.add(value); + } + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDate => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::ChronoDate"); + let _ = args.add(value); + } + #[cfg(feature = "with-chrono")] + ArrayType::ChronoTime => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::ChronoTime"); + let _ = args.add(value); + } + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTime => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::ChronoDateTime"); + let _ = args.add(value); + } + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTimeUtc => { + let value: Option>> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::ChronoDateTimeUtc"); + let _ = args.add(value); + } + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTimeLocal => { + let value: Option>> = Value::Array(ty, v).expect( + "This Value::Array should consist of Value::ChronoDateTimeLocal", + ); + let _ = args.add(value); + } + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTimeWithTimeZone => { + let value: Option>> = Value::Array(ty, v).expect( + "This Value::Array should consist of Value::ChronoDateTimeWithTimeZone", + ); + let _ = args.add(value); + } + #[cfg(feature = "with-time")] + ArrayType::TimeDate => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::TimeDate"); + let _ = args.add(value); + } + #[cfg(feature = "with-time")] + ArrayType::TimeTime => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::TimeTime"); + let _ = args.add(value); + } + #[cfg(feature = "with-time")] + ArrayType::TimeDateTime => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::TimeDateTime"); + let _ = args.add(value); + } + #[cfg(feature = "with-time")] + ArrayType::TimeDateTimeWithTimeZone => { + let value: Option> = Value::Array(ty, v).expect( + "This Value::Array should consist of Value::TimeDateTimeWithTimeZone", + ); + let _ = args.add(value); + } + #[cfg(feature = "with-uuid")] + ArrayType::Uuid => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Uuid"); + let _ = args.add(value); + } + #[cfg(feature = "with-rust_decimal")] + ArrayType::Decimal => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Decimal"); + let _ = args.add(value); + } + #[cfg(feature = "with-bigdecimal")] + ArrayType::BigDecimal => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::BigDecimal"); + let _ = args.add(value); + } + #[cfg(feature = "with-json")] + ArrayType::Json => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::Json"); + let _ = args.add(value); + } + #[cfg(feature = "with-ipnetwork")] + ArrayType::IpNetwork => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::IpNetwork"); + let _ = args.add(value); + } + #[cfg(feature = "with-mac_address")] + ArrayType::MacAddress => { + let value: Option> = Value::Array(ty, v) + .expect("This Value::Array should consist of Value::MacAddress"); + let _ = args.add(value); + } + }, + #[cfg(feature = "postgres-vector")] + Value::Vector(_) => { + panic!( + "sea-query-sqlx-postgres does not support pgvector bindings yet because \ + pgvector's SQLx integration currently depends on the top-level sqlx crate" + ); + } /* #[cfg(feature = "postgres-range")] + Value::Range(v) => { + let _ = args.add(v); + } */ + } + } + args + } +} diff --git a/sea-query-sqlx-postgres/src/sqlx.rs b/sea-query-sqlx-postgres/src/sqlx.rs new file mode 100644 index 000000000..dbd3c5356 --- /dev/null +++ b/sea-query-sqlx-postgres/src/sqlx.rs @@ -0,0 +1,28 @@ +use crate::SqlxValues; +use sea_query::{QueryBuilder, query::*}; + +pub trait SqlxBinder { + fn build_sqlx(&self, query_builder: T) -> (String, SqlxValues) + where + T: QueryBuilder; +} + +macro_rules! impl_sqlx_binder { + ($l:ident) => { + impl SqlxBinder for $l { + fn build_sqlx(&self, query_builder: T) -> (String, SqlxValues) + where + T: QueryBuilder, + { + let (query, values) = self.build(query_builder); + (query, SqlxValues(values)) + } + } + }; +} + +impl_sqlx_binder!(SelectStatement); +impl_sqlx_binder!(UpdateStatement); +impl_sqlx_binder!(InsertStatement); +impl_sqlx_binder!(DeleteStatement); +impl_sqlx_binder!(WithQuery); diff --git a/sea-query-sqlx-postgres/src/values.rs b/sea-query-sqlx-postgres/src/values.rs new file mode 100644 index 000000000..9ffbcad6d --- /dev/null +++ b/sea-query-sqlx-postgres/src/values.rs @@ -0,0 +1,3 @@ +#[derive(Clone, Debug, PartialEq)] +#[repr(transparent)] +pub struct SqlxValues(pub sea_query::Values);