Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ pub struct ColumnName(pub Option<TableName>, 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")))
Expand Down Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions src/audit/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand All @@ -128,7 +128,6 @@ impl Walker {
Expr::Keyword(_) => (),
Expr::Case(case) => self.recurse_audit_case(case)?,
Expr::Constant(_) => (),
Expr::TypeName(_) => (),
}
Ok(())
}
Expand Down
38 changes: 18 additions & 20 deletions src/backend/postgres/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
58 changes: 42 additions & 16 deletions src/backend/query_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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")]
{
Expand Down
86 changes: 79 additions & 7 deletions src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,23 @@ pub enum Expr {
Custom(String),
CustomWithExpr(String, Vec<Expr>),
Keyword(Keyword),
AsEnum(DynIden, Box<Expr>),
Case(Box<CaseStatement>),
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.
Expand Down Expand Up @@ -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
///
/// ```
Expand Down Expand Up @@ -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<N>(self, type_name: N) -> Expr
where
N: IntoIden;
N: IntoTypeRef;

/// Express a logical `AND` operation.
///
Expand Down Expand Up @@ -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
///
/// ```
Expand All @@ -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<N>(self, type_name: N) -> Expr
where
N: IntoIden;
N: IntoTypeRef;

/// Express an arithmetic division operation.
///
Expand Down Expand Up @@ -1517,9 +1589,9 @@ where
{
fn as_enum<N>(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<O, R>(self, op: O, right: R) -> Expr
Expand All @@ -1532,7 +1604,7 @@ where

fn cast_as<N>(self, type_name: N) -> Expr
where
N: IntoIden,
N: Into<TypeRef>,
{
Expr::FunctionCall(Func::cast_as(self, type_name))
}
Expand Down
3 changes: 1 addition & 2 deletions src/extension/postgres/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,6 @@ impl PgFunc {
where
T: Into<Expr>,
{
FunctionCall::new(Func::PgFunction(PgFunc::ArrayAgg))
.arg_with(expr, FuncArgMod { distinct: true })
FunctionCall::new(Func::PgFunction(PgFunc::ArrayAgg)).arg_with(expr, FuncArgMod::distinct())
}
}
Loading
Loading