From 66465529ba12f919be34a751428506a60cc25b0c Mon Sep 17 00:00:00 2001 From: Dmitrii Aleksandrov Date: Tue, 12 Aug 2025 18:24:10 +0400 Subject: [PATCH 1/2] Refactor conversions into qualified idens --- src/{types.rs => types/mod.rs} | 83 ++--------------------- src/types/qualification.rs | 118 +++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 79 deletions(-) rename src/{types.rs => types/mod.rs} (93%) create mode 100644 src/types/qualification.rs diff --git a/src/types.rs b/src/types/mod.rs similarity index 93% rename from src/types.rs rename to src/types/mod.rs index 4e2092f9f..0ba1274b0 100644 --- a/src/types.rs +++ b/src/types/mod.rs @@ -8,6 +8,10 @@ use crate::extension::postgres::PgBinOper; #[cfg(feature = "backend-sqlite")] use crate::extension::sqlite::SqliteBinOper; +mod qualification; + +pub use qualification::{MaybeQualifiedOnce, MaybeQualifiedTwice}; + /// A reference counted pointer: either [`Rc`][std::rc::Rc] or [`Arc`][std::sync::Arc], /// depending on the feature flags. /// @@ -556,85 +560,6 @@ where } } -impl From for SchemaName -where - T: IntoIden, -{ - fn from(iden: T) -> Self { - SchemaName(None, iden.into_iden()) - } -} - -impl From<(S, T)> for SchemaName -where - S: IntoIden, - T: IntoIden, -{ - fn from((db, schema): (S, T)) -> Self { - SchemaName(Some(db.into()), schema.into_iden()) - } -} - -impl From for TableName -where - T: IntoIden, -{ - fn from(iden: T) -> Self { - TableName(None, iden.into_iden()) - } -} - -impl From<(S, T)> for TableName -where - S: IntoIden, - T: IntoIden, -{ - fn from((schema, table): (S, T)) -> Self { - TableName(Some(schema.into()), table.into_iden()) - } -} - -impl From<(S, T, U)> for TableName -where - S: IntoIden, - T: IntoIden, - U: IntoIden, -{ - fn from((db, schema, table): (S, T, U)) -> Self { - TableName(Some((db, schema).into()), table.into_iden()) - } -} - -impl From for ColumnName -where - T: IntoIden, -{ - fn from(iden: T) -> Self { - ColumnName(None, iden.into_iden()) - } -} - -impl From<(S, T)> for ColumnName -where - S: IntoIden, - T: IntoIden, -{ - fn from((table, column): (S, T)) -> Self { - ColumnName(Some(table.into()), column.into_iden()) - } -} - -impl From<(S, T, U)> for ColumnName -where - S: IntoIden, - T: IntoIden, - U: IntoIden, -{ - fn from((schema, table, column): (S, T, U)) -> Self { - ColumnName(Some((schema, table).into()), column.into_iden()) - } -} - impl IntoColumnRef for T where T: Into, diff --git a/src/types/qualification.rs b/src/types/qualification.rs new file mode 100644 index 000000000..1430b1988 --- /dev/null +++ b/src/types/qualification.rs @@ -0,0 +1,118 @@ +//! Conversion traits/impls for "potentially qualified" names like `(schema?).(table?).column`. + +use super::*; + +// -------------------------- MaybeQualifiedOnce ------------------------------- + +/// A name that can be unqualified (`foo`) or qualified once (`foo.bar`). +/// +/// This is mostly a "private" helper trait to provide reusable conversions. +pub trait MaybeQualifiedOnce { + /// Represent a maybe-qualified name as a `(foo?, bar)` tuple. + fn into_2_parts(self) -> (Option, DynIden); +} + +/// Only the "base", no qualification (`foo`). +impl MaybeQualifiedOnce for T +where + T: IntoIden, +{ + fn into_2_parts(self) -> (Option, DynIden) { + (None, self.into_iden()) + } +} + +/// With a qualification (`foo.bar`). +impl MaybeQualifiedOnce for (S, T) +where + S: IntoIden, + T: IntoIden, +{ + fn into_2_parts(self) -> (Option, DynIden) { + let (qual, base) = self; + (Some(qual.into_iden()), base.into_iden()) + } +} + +// ------------------------- MaybeQualifiedTwice ------------------------------- + +/// A name that can be unqualified (`foo`), qualified once (`foo.bar`), or twice (`foo.bar.baz`). +/// +/// This is mostly a "private" helper trait to provide reusable conversions. +pub trait MaybeQualifiedTwice { + /// Represent a maybe-qualified name as a `(foo?, bar?, baz)` tuple. + /// + /// To be precise, it's actually `((foo?, bar)?, baz)` to rule out invalid states like `(Some, None, Some)`. + fn into_3_parts(self) -> (Option<(Option, DynIden)>, DynIden); +} + +/// From 1 or 2 parts (`foo` or `foo.bar`). +impl MaybeQualifiedTwice for T +where + T: MaybeQualifiedOnce, +{ + fn into_3_parts(self) -> (Option<(Option, DynIden)>, DynIden) { + let (middle, base) = self.into_2_parts(); + let qual = middle.map(|middle| (None, middle)); + (qual, base) + } +} + +/// Fully-qualified from 3 parts (`foo.bar.baz`). +impl MaybeQualifiedTwice for (S, T, U) +where + S: IntoIden, + T: IntoIden, + U: IntoIden, +{ + fn into_3_parts(self) -> (Option<(Option, DynIden)>, DynIden) { + let (q2, q1, base) = self; + let (q2, q1, base) = (q2.into_iden(), q1.into_iden(), base.into_iden()); + let q = (Some(q2), q1); + (Some(q), base) + } +} + +// -------------------------------- impls -------------------------------------- + +/// Construct a [`SchemaName`] from 1-2 parts (`(database?).schema`) +impl From for SchemaName +where + T: MaybeQualifiedOnce, +{ + fn from(value: T) -> Self { + let (db, schema) = value.into_2_parts(); + let db_name = db.map(DatabaseName); + SchemaName(db_name, schema) + } +} + +/// Construct a [`TableName`] from 1-3 parts (`(database?).(schema?).table`) +impl From for TableName +where + T: MaybeQualifiedTwice, +{ + fn from(value: T) -> Self { + let (schema_parts, table) = value.into_3_parts(); + let schema_name = schema_parts.map(|schema_parts| match schema_parts { + (Some(db), schema) => SchemaName(Some(DatabaseName(db)), schema), + (None, schema) => SchemaName(None, schema), + }); + TableName(schema_name, table) + } +} + +/// Construct a [`ColumnName`] from 1-3 parts (`(schema?).(table?).column`) +impl From for ColumnName +where + T: MaybeQualifiedTwice, +{ + fn from(value: T) -> Self { + let (table_parts, column) = value.into_3_parts(); + let table_name = table_parts.map(|table_parts| match table_parts { + (Some(schema), table) => TableName(Some(schema.into()), table), + (None, table) => TableName(None, table), + }); + ColumnName(table_name, column) + } +} From 427a2fcece834a4ed91427b74041eac4f891e509 Mon Sep 17 00:00:00 2001 From: Dmitrii Aleksandrov Date: Tue, 12 Aug 2025 19:09:34 +0400 Subject: [PATCH 2/2] Allow qualified type names in `cast_as_quoted` --- CHANGELOG.md | 7 +++++++ src/backend/query_builder.rs | 14 ++++++++++++-- src/backend/table_ref_builder.rs | 18 ++++++++++++------ src/expr.rs | 2 +- src/func.rs | 28 ++++++++++++++++++++++++---- src/types/mod.rs | 4 ++++ src/types/qualification.rs | 15 +++++++++++++++ 7 files changed, 75 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73719c7e5..7d90bd35f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ pub struct TableName(pub Option, pub DynIden); * Enable `clippy::nursery` https://github.com/SeaQL/sea-query/pull/938 * Removed unnecessary `'static` bounds from type signatures https://github.com/SeaQL/sea-query/pull/921 +* `cast_as_quoted` now allows you to [qualify the type + name](https://github.com/SeaQL/sea-query/issues/827). * Most `Value` variants are now unboxed (except `BigDecimal` and `Array`). Previously the size is 24 bytes. https://github.com/SeaQL/sea-query/pull/925 ```rust assert_eq!(std::mem::size_of::(), 32); @@ -96,6 +98,11 @@ assert_eq!( ### Breaking Changes +* Changed `Expr::TypeName(DynIden)` to `Expr::TypeName(TypeName)`, which can be + [qualified](https://github.com/SeaQL/sea-query/issues/827). + + If you manually construct this variant and it no longer compiles, just add + `.into()`. * Removed inherent `SimpleExpr` methods that duplicate `ExprTrait`. If you encounter the following error, please add `use sea_query::ExprTrait` in scope https://github.com/SeaQL/sea-query/pull/890 ```rust error[E0599]: no method named `like` found for enum `sea_query::Expr` in the current scope diff --git a/src/backend/query_builder.rs b/src/backend/query_builder.rs index 496e29c37..7299316c1 100644 --- a/src/backend/query_builder.rs +++ b/src/backend/query_builder.rs @@ -454,8 +454,8 @@ pub trait QueryBuilder: Expr::Constant(val) => { self.prepare_constant(val, sql); } - Expr::TypeName(iden) => { - self.prepare_iden(iden, sql); + Expr::TypeName(type_name) => { + self.prepare_type_name(type_name, sql); } } } @@ -910,6 +910,16 @@ pub trait QueryBuilder: self.prepare_function_name_common(function, sql) } + /// Translate [`TypeName`] into an SQL statement. + fn prepare_type_name(&self, type_name: &TypeName, sql: &mut dyn SqlWriter) { + let TypeName(schema_name, r#type) = type_name; + if let Some(schema_name) = schema_name { + self.prepare_schema_name(schema_name, sql); + write!(sql, ".").unwrap(); + } + self.prepare_iden(r#type, sql); + } + /// Translate [`JoinType`] into SQL statement. fn prepare_join_type(&self, join_type: &JoinType, sql: &mut dyn SqlWriter) { self.prepare_join_type_common(join_type, sql) diff --git a/src/backend/table_ref_builder.rs b/src/backend/table_ref_builder.rs index 57f64e681..a6c8af228 100644 --- a/src/backend/table_ref_builder.rs +++ b/src/backend/table_ref_builder.rs @@ -19,14 +19,20 @@ pub trait TableRefBuilder: QuotedBuilder { /// Translate [`TableName`] into an SQL statement. fn prepare_table_name(&self, table_name: &TableName, sql: &mut dyn SqlWriter) { let TableName(schema_name, table) = table_name; - if let Some(SchemaName(database_name, schema)) = schema_name { - if let Some(DatabaseName(database)) = database_name { - self.prepare_iden(database, sql); - write!(sql, ".").unwrap(); - } - self.prepare_iden(schema, sql); + if let Some(schema_name) = schema_name { + self.prepare_schema_name(schema_name, sql); write!(sql, ".").unwrap(); } self.prepare_iden(table, sql); } + + /// Translate [`SchemaName`] into an SQL statement. + fn prepare_schema_name(&self, schema_name: &SchemaName, sql: &mut dyn SqlWriter) { + let SchemaName(database_name, schema) = schema_name; + if let Some(DatabaseName(database)) = database_name { + self.prepare_iden(database, sql); + write!(sql, ".").unwrap(); + } + self.prepare_iden(schema, sql); + } } diff --git a/src/expr.rs b/src/expr.rs index a5ac64174..9582b01fa 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -43,7 +43,7 @@ pub enum Expr { AsEnum(DynIden, Box), Case(Box), Constant(Value), - TypeName(DynIden), + TypeName(TypeName), } /// "Operator" methods for building expressions. diff --git a/src/func.rs b/src/func.rs index 7c8b8977b..38ed889d0 100644 --- a/src/func.rs +++ b/src/func.rs @@ -476,6 +476,8 @@ impl Func { /// Call `CAST` function with a case-sensitive custom type. /// + /// Type can be qualified with a schema name. + /// /// # Examples /// /// ``` @@ -497,15 +499,33 @@ impl Func { /// query.to_string(SqliteQueryBuilder), /// r#"SELECT CAST('hello' AS "MyType")"# /// ); + /// + /// // Also works with a schema-qualified type name: + /// + /// let query = Query::select() + /// .expr(Func::cast_as_quoted("hello", ("MySchema", "MyType"))) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT CAST('hello' AS `MySchema`.`MyType`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT CAST('hello' AS "MySchema"."MyType")"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT CAST('hello' AS "MySchema"."MyType")"# + /// ); /// ``` - pub fn cast_as_quoted(expr: V, iden: I) -> FunctionCall + pub fn cast_as_quoted(expr: V, r#type: I) -> FunctionCall where V: Into, - I: IntoIden, + I: Into, { let expr: Expr = expr.into(); - FunctionCall::new(Func::Cast) - .arg(expr.binary(BinOper::As, Expr::TypeName(iden.into_iden()))) + FunctionCall::new(Func::Cast).arg(expr.binary(BinOper::As, Expr::TypeName(r#type.into()))) } /// Call `COALESCE` function. diff --git a/src/types/mod.rs b/src/types/mod.rs index 0ba1274b0..5982a958e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -146,6 +146,10 @@ pub struct DatabaseName(pub DynIden); #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SchemaName(pub Option, pub DynIden); +/// An SQL type name, potentially qualified as `(database.)(schema.)type`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TypeName(pub Option, pub DynIden); + /// A table name, potentially qualified as `(database.)(schema.)table`. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct TableName(pub Option, pub DynIden); diff --git a/src/types/qualification.rs b/src/types/qualification.rs index 1430b1988..ec68b655e 100644 --- a/src/types/qualification.rs +++ b/src/types/qualification.rs @@ -87,6 +87,21 @@ where } } +/// Construct a [`TypeName`] from 1-3 parts (`(database?).(schema?).type`) +impl From for TypeName +where + T: MaybeQualifiedTwice, +{ + fn from(value: T) -> Self { + let (schema_parts, r#type) = value.into_3_parts(); + let schema_name = schema_parts.map(|schema_parts| match schema_parts { + (Some(db), schema) => SchemaName(Some(DatabaseName(db)), schema), + (None, schema) => SchemaName(None, schema), + }); + TypeName(schema_name, r#type) + } +} + /// Construct a [`TableName`] from 1-3 parts (`(database?).(schema?).table`) impl From for TableName where