diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b3b7efc7..c4ed4182e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,7 +73,8 @@ pub struct ColumnName(pub Option, pub DynIden); * Add `Keyword::Default` https://github.com/SeaQL/sea-query/pull/965 * 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/pull/922 +* `CAST` methods now allow you to qualify the type name. + https://github.com/SeaQL/sea-query/pull/922 https://github.com/SeaQL/sea-query/pull/977 ```rust let query = Query::select() .expr(Func::cast_as_quoted("hello", ("MySchema", "MyType"))) @@ -314,11 +315,12 @@ error[E0308]: mismatched types * Replaced `ColumnSpec::Check(Expr)` with `ColumnSpec::Check(Check)` to support named check constraints https://github.com/SeaQL/sea-query/pull/920 * `SelectStatement::cross_join` no longer accepts a condition https://github.com/SeaQL/sea-query/pull/956 * Turned `TypeRef` from an enum into a struct that reuses `TableName`. https://github.com/SeaQL/sea-query/pull/969 -* Changed `Expr::TypeName(DynIden)` to `Expr::TypeName(TypeRef)`, 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 `Expr::TypeName`, because type names are not valid expressions by + themselves. They don't compose into more complex expressions like `REAL + 1`. + They only appear in very specific places like `CAST` expressions. +* Removed `Expr::AsEnum`. `CAST` expressions are now handled as `FunctionCall`s + instead. +* `FuncArgMod` is now `#[non_exhaustive]` and doesn't implement `Copy`. ### Bug Fixes diff --git a/src/audit/select.rs b/src/audit/select.rs index 93ca767d9..d13af2379 100644 --- a/src/audit/select.rs +++ b/src/audit/select.rs @@ -110,7 +110,7 @@ impl Walker { fn recurse_audit_expr(&mut self, expr: &Expr) -> Result<(), Error> { match expr { Expr::Column(_) => (), - Expr::Unary(_, expr) | Expr::AsEnum(_, expr) => self.recurse_audit_expr(expr)?, + Expr::Unary(_, expr) => self.recurse_audit_expr(expr)?, Expr::FunctionCall(function) => self.recurse_audit_function(function)?, Expr::Binary(left, _, right) => { self.recurse_audit_expr(left)?; @@ -128,7 +128,6 @@ impl Walker { Expr::Keyword(_) => (), Expr::Case(case) => self.recurse_audit_case(case)?, Expr::Constant(_) => (), - Expr::TypeName(_) => (), } Ok(()) } diff --git a/src/backend/postgres/query.rs b/src/backend/postgres/query.rs index a872adc96..65f85baf3 100644 --- a/src/backend/postgres/query.rs +++ b/src/backend/postgres/query.rs @@ -34,27 +34,25 @@ impl QueryBuilder for PostgresQueryBuilder { ("$", true) } - fn prepare_simple_expr(&self, simple_expr: &Expr, sql: &mut dyn SqlWriter) { - match simple_expr { - Expr::AsEnum(type_name, expr) => { - sql.write_str("CAST(").unwrap(); - self.prepare_simple_expr_common(expr, sql); - let q = self.quote(); - let type_name = &type_name.0; - let (ty, sfx) = if let Some(base) = type_name.strip_suffix("[]") { - (base, "[]") - } else { - (type_name.as_ref(), "") - }; - sql.write_str(" AS ").unwrap(); - sql.write_char(q.left()).unwrap(); - sql.write_str(ty).unwrap(); - sql.write_char(q.right()).unwrap(); - sql.write_str(sfx).unwrap(); - sql.write_char(')').unwrap(); - } - _ => QueryBuilder::prepare_simple_expr_common(self, simple_expr, sql), + /// Special handling of array type names in Postgres. + fn prepare_type_ref(&self, type_ref: &TypeRef, sql: &mut dyn SqlWriter) { + let q = self.quote(); + let TypeRef(schema_name, type_name) = type_ref; + let type_name = type_name.0.as_ref(); + let (ty, sfx) = if let Some(base) = type_name.strip_suffix("[]") { + (base, "[]") + } else { + (type_name, "") + }; + + if let Some(schema_name) = schema_name { + self.prepare_schema_name(schema_name, sql); + sql.write_str(".").unwrap(); } + sql.write_char(q.left()).unwrap(); + sql.write_str(ty).unwrap(); + sql.write_char(q.right()).unwrap(); + sql.write_str(sfx).unwrap(); } fn prepare_select_distinct(&self, select_distinct: &SelectDistinct, sql: &mut dyn SqlWriter) { diff --git a/src/backend/query_builder.rs b/src/backend/query_builder.rs index 41bb26a51..83553ca1d 100644 --- a/src/backend/query_builder.rs +++ b/src/backend/query_builder.rs @@ -490,18 +490,12 @@ pub trait QueryBuilder: Expr::Keyword(keyword) => { self.prepare_keyword(keyword, sql); } - Expr::AsEnum(_, expr) => { - self.prepare_simple_expr(expr, sql); - } Expr::Case(case_stmt) => { self.prepare_case_statement(case_stmt, sql); } Expr::Constant(val) => { self.prepare_constant(val, sql); } - Expr::TypeName(type_name) => { - self.prepare_type_ref(type_name, sql); - } } } @@ -791,23 +785,42 @@ pub trait QueryBuilder: let mut args = func.args.iter().zip(func.mods.iter()); if let Some((arg, modifier)) = args.next() { - if modifier.distinct { - sql.write_str("DISTINCT ").unwrap(); - } - self.prepare_simple_expr(arg, sql); + self.prepare_function_argument(arg, modifier, sql); } for (arg, modifier) in args { sql.write_str(", ").unwrap(); - if modifier.distinct { - sql.write_str("DISTINCT ").unwrap(); - } - self.prepare_simple_expr(arg, sql); + self.prepare_function_argument(arg, modifier, sql); } sql.write_str(")").unwrap(); } + fn prepare_function_argument( + &self, + arg: &Expr, + modifier: &FuncArgMod, + sql: &mut dyn SqlWriter, + ) { + // Intentionally destructure without `..` so that any new fields raise an error here + // and force us to implement the codegen for them. + let FuncArgMod { distinct, as_type } = modifier; + + if *distinct { + sql.write_str("DISTINCT ").unwrap(); + } + + self.prepare_simple_expr(arg, sql); + + if let Some((type_name, quoting)) = as_type { + sql.write_str(" AS ").unwrap(); + match quoting { + IdenQuoting::Quoted => self.prepare_type_ref(type_name, sql), + IdenQuoting::Unquoted => self.prepare_unquoted_type_ref(type_name, sql), + } + } + } + /// Translate [`QueryStatement`] into SQL statement. fn prepare_query_statement(&self, query: &SubQueryStatement, sql: &mut dyn SqlWriter); @@ -950,6 +963,8 @@ pub trait QueryBuilder: } /// Translate [`TypeRef`] into an SQL statement. + /// + /// The type name will be quoted. See also [`prepare_unquoted_type_ref`][QueryBuilder::prepare_unquoted_type_ref]. fn prepare_type_ref(&self, type_name: &TypeRef, sql: &mut dyn SqlWriter) { let TypeRef(schema_name, r#type) = type_name; if let Some(schema_name) = schema_name { @@ -959,6 +974,18 @@ pub trait QueryBuilder: self.prepare_iden(r#type, sql); } + /// Translate [`TypeRef`] into an SQL statement without quoting the type name. + /// + /// See also [`prepare_type_ref`][QueryBuilder::prepare_type_ref]. + fn prepare_unquoted_type_ref(&self, type_name: &TypeRef, sql: &mut dyn SqlWriter) { + let TypeRef(schema_name, r#type) = type_name; + if let Some(schema_name) = schema_name { + self.prepare_schema_name(schema_name, sql); + write!(sql, ".").unwrap(); + } + sql.write_str(&r#type.0).unwrap(); + } + /// 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) @@ -1812,8 +1839,7 @@ pub(crate) fn common_inner_expr_well_known_greater_precedence( | Expr::Value(_) | Expr::Keyword(_) | Expr::Case(_) - | Expr::SubQuery(_, _) - | Expr::TypeName(_) => true, + | Expr::SubQuery(_, _) => true, Expr::Binary(_, inner_oper, _) => { #[cfg(feature = "option-more-parentheses")] { diff --git a/src/expr.rs b/src/expr.rs index 3d3d901af..60aa0e235 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -40,10 +40,23 @@ pub enum Expr { Custom(String), CustomWithExpr(String, Vec), Keyword(Keyword), - AsEnum(DynIden, Box), Case(Box), Constant(Value), - TypeName(TypeRef), +} + +/// Whether an identifier should be quoted or not. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum IdenQuoting { + /// Quote the identifier when building the SQL statement. + /// + /// This is the default behavior, and allows to use special characters and reserved keywords in identifiers. + /// + /// In some databases, this also makes the identifier case-sensitive, while unquoted identifiers are case-insensitive. + Quoted, + /// Don't quote the identifier when building the SQL statement. + /// + /// In some databases, unquoted identifiers are case-insensitive, while quoted identifiers are case-sensitive. + Unquoted, } /// "Operator" methods for building expressions. @@ -151,6 +164,12 @@ pub trait ExprTrait: Sized { /// Express a `AS enum` expression. /// + /// Type can be qualified with a schema name. + /// + /// Unlike [`cast_as`][Expr::cast_as], this method puts the type name in quotes. + /// This is useful for type names that can be SQL keywords or contain special characters. + /// In some databases, quoted identifiers are case-sensitive, while unquoted identifiers are case-insensitive. + /// /// # Examples /// /// ``` @@ -184,11 +203,42 @@ pub trait ExprTrait: Sized { /// query.to_string(PostgresQueryBuilder), /// r#"SELECT CAST("font_size" AS "FontSizeEnum"[]) FROM "character""# /// ); + /// + /// // Also works with a schema-qualified type name: + /// + /// let query = Query::insert() + /// .into_table(Char::Table) + /// .columns([Char::FontSize]) + /// .values_panic(["large".as_enum(("MySchema", "FontSizeEnum"))]) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `character` (`font_size`) VALUES ('large')"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "character" ("font_size") VALUES (CAST('large' AS "MySchema"."FontSizeEnum"))"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO "character" ("font_size") VALUES ('large')"# + /// ); + /// + /// let query = Query::select() + /// .expr(Expr::col(Char::FontSize).as_enum(("MySchema", "FontSizeEnum[]"))) + /// .from(Char::Table) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT CAST("font_size" AS "MySchema"."FontSizeEnum"[]) FROM "character""# + /// ); /// ``` #[allow(clippy::wrong_self_convention)] fn as_enum(self, type_name: N) -> Expr where - N: IntoIden; + N: IntoTypeRef; /// Express a logical `AND` operation. /// @@ -297,6 +347,11 @@ pub trait ExprTrait: Sized { /// Express a `CAST AS` expression. /// + /// Type can be qualified with a schema name. + /// + /// Unlike [`as_enum`][Expr::as_enum], this method doesn't put the type name in quotes. + /// This can be used to get case-insensitive behavior in databases where quoted identifiers are case-sensitive. + /// /// # Examples /// /// ``` @@ -316,10 +371,27 @@ pub trait ExprTrait: Sized { /// query.to_string(SqliteQueryBuilder), /// r#"SELECT CAST('1' AS integer)"# /// ); + /// + /// // Also works with a schema-qualified type name: + /// + /// let query = Query::select().expr("1".cast_as(("MySchema", "integer"))).to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"SELECT CAST('1' AS `MySchema`.integer)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"SELECT CAST('1' AS "MySchema".integer)"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"SELECT CAST('1' AS "MySchema".integer)"# + /// ); /// ``` fn cast_as(self, type_name: N) -> Expr where - N: IntoIden; + N: IntoTypeRef; /// Express an arithmetic division operation. /// @@ -1517,9 +1589,9 @@ where { fn as_enum(self, type_name: N) -> Expr where - N: IntoIden, + N: IntoTypeRef, { - Expr::AsEnum(type_name.into_iden(), Box::new(self.into())) + Expr::FunctionCall(Func::cast_as_quoted(self, type_name)) } fn binary(self, op: O, right: R) -> Expr @@ -1532,7 +1604,7 @@ where fn cast_as(self, type_name: N) -> Expr where - N: IntoIden, + N: Into, { Expr::FunctionCall(Func::cast_as(self, type_name)) } diff --git a/src/extension/postgres/func.rs b/src/extension/postgres/func.rs index 68233c12c..c192e8f7b 100644 --- a/src/extension/postgres/func.rs +++ b/src/extension/postgres/func.rs @@ -495,7 +495,6 @@ impl PgFunc { where T: Into, { - FunctionCall::new(Func::PgFunction(PgFunc::ArrayAgg)) - .arg_with(expr, FuncArgMod { distinct: true }) + FunctionCall::new(Func::PgFunction(PgFunc::ArrayAgg)).arg_with(expr, FuncArgMod::distinct()) } } diff --git a/src/func.rs b/src/func.rs index 3e09cf228..55fe1df64 100644 --- a/src/func.rs +++ b/src/func.rs @@ -297,7 +297,7 @@ impl Func { where T: Into, { - FunctionCall::new(Func::Count).arg_with(expr, FuncArgMod { distinct: true }) + FunctionCall::new(Func::Count).arg_with(expr, FuncArgMod::distinct()) } /// Call `CHAR_LENGTH` function. @@ -440,6 +440,11 @@ impl Func { /// Call `CAST` function with a custom type. /// + /// The type name will not be quoted. + /// In some databases, unquoted identifiers are case-insensitive, while quoted identifiers are case-sensitive. + /// + /// Type can be qualified with a schema name. + /// /// # Examples /// /// ``` @@ -461,21 +466,39 @@ 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("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(expr: V, iden: I) -> FunctionCall + pub fn cast_as(expr: V, r#type: I) -> FunctionCall where V: Into, - I: IntoIden, + I: IntoTypeRef, { - let expr: Expr = expr.into(); - FunctionCall::new(Func::Cast).arg(expr.binary( - BinOper::As, - Expr::cust(iden.into_iden().to_string().as_str()), - )) + FunctionCall::new(Func::Cast) + .arg_with(expr, FuncArgMod::as_type(r#type, IdenQuoting::Unquoted)) } /// Call `CAST` function with a case-sensitive custom type. /// + /// The type name will be put in quotes. + /// /// Type can be qualified with a schema name. /// /// # Examples @@ -524,8 +547,8 @@ impl Func { V: Into, I: Into, { - let expr: Expr = expr.into(); - FunctionCall::new(Func::Cast).arg(expr.binary(BinOper::As, Expr::TypeName(r#type.into()))) + FunctionCall::new(Func::Cast) + .arg_with(expr, FuncArgMod::as_type(r#type, IdenQuoting::Quoted)) } /// Call `COALESCE` function. @@ -829,11 +852,6 @@ pub struct FunctionCall { pub(crate) mods: Vec, } -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct FuncArgMod { - pub distinct: bool, -} - impl FunctionCall { pub(crate) fn new(func: Func) -> Self { Self { @@ -882,3 +900,27 @@ impl FunctionCall { &self.mods } } + +#[derive(Debug, Default, Clone, PartialEq)] +#[non_exhaustive] +pub struct FuncArgMod { + pub distinct: bool, + /// Is used to implement `CAST(... AS ...)` "calls". + pub as_type: Option<(TypeRef, IdenQuoting)>, +} + +impl FuncArgMod { + pub(crate) fn distinct() -> Self { + Self { + distinct: true, + ..Default::default() + } + } + + pub(crate) fn as_type(type_: impl IntoTypeRef, quoting: IdenQuoting) -> Self { + Self { + as_type: Some((type_.into(), quoting)), + ..Default::default() + } + } +}