From 2e81db77663ccefa4ca98904b9c437356929fcd3 Mon Sep 17 00:00:00 2001 From: yuly3 Date: Sun, 11 Jan 2026 19:00:22 +0900 Subject: [PATCH 1/2] feat(query): add SelectExprTrait for ergonomic alias and window chaining - Introduce `SelectExprTrait` providing `alias`, `window`, and `window_name` methods for both `SelectExpr` and `T: Into` - Allow fluent API patterns like `.expr(Expr::col(...).alias("C"))` - Refactor existing `expr_as`, `expr_window*` methods to use the new trait - Update window-related doc examples to use proper window functions --- src/query/select.rs | 153 ++++++++++++++++++++++++++++------------ tests/mysql/query.rs | 37 ++++++++++ tests/postgres/query.rs | 37 ++++++++++ tests/sqlite/query.rs | 37 ++++++++++ 4 files changed, 219 insertions(+), 45 deletions(-) diff --git a/src/query/select.rs b/src/query/select.rs index d315e16f2..64c11468e 100644 --- a/src/query/select.rs +++ b/src/query/select.rs @@ -201,6 +201,89 @@ where } } +/// Extension methods for building a [`SelectExpr`] from an expression. +/// +/// This makes it ergonomic to attach select-specific modifiers (like `AS` and `OVER`) and pass the +/// result into [`SelectStatement::expr`]. +/// +/// # Examples +/// +/// ``` +/// use sea_query::{tests_cfg::*, *}; +/// +/// let query = Query::select() +/// .from(Char::Table) +/// .expr( +/// Expr::col(Char::Character) +/// .max() +/// .window(WindowStatement::partition_by(Char::FontSize)) +/// .alias("C"), +/// ) +/// .to_owned(); +/// +/// assert_eq!( +/// query.to_string(MysqlQueryBuilder), +/// r#"SELECT MAX(`character`) OVER ( PARTITION BY `font_size` ) AS `C` FROM `character`"# +/// ); +/// ``` +pub trait SelectExprTrait: Sized { + fn alias(self, alias: A) -> SelectExpr + where + A: IntoIden; + + fn window(self, window: WindowStatement) -> SelectExpr; + + fn window_name(self, window: W) -> SelectExpr + where + W: IntoIden; +} + +impl SelectExprTrait for SelectExpr { + fn alias(mut self, alias: A) -> SelectExpr + where + A: IntoIden, + { + self.alias = Some(alias.into_iden()); + self + } + + fn window(mut self, window: WindowStatement) -> SelectExpr { + self.window = Some(WindowSelectType::Query(window)); + self + } + + fn window_name(mut self, window: W) -> SelectExpr + where + W: IntoIden, + { + self.window = Some(WindowSelectType::Name(window.into_iden())); + self + } +} + +impl SelectExprTrait for T +where + T: Into, +{ + fn alias(self, alias: A) -> SelectExpr + where + A: IntoIden, + { + SelectExpr::from(self).alias(alias) + } + + fn window(self, window: WindowStatement) -> SelectExpr { + SelectExpr::from(self).window(window) + } + + fn window_name(self, window: W) -> SelectExpr + where + W: IntoIden, + { + SelectExpr::from(self).window_name(window) + } +} + impl SelectStatement { /// Construct a new [`SelectStatement`] pub fn new() -> Self { @@ -678,11 +761,7 @@ impl SelectStatement { T: Into, A: IntoIden, { - self.expr(SelectExpr { - expr: expr.into(), - alias: Some(alias.into_iden()), - window: None, - }); + self.expr(expr.alias(alias)); self } @@ -696,33 +775,29 @@ impl SelectStatement { /// let query = Query::select() /// .from(Char::Table) /// .expr_window( - /// Expr::col(Char::Character), + /// Expr::col(Char::Character).max(), /// WindowStatement::partition_by(Char::FontSize), /// ) /// .to_owned(); /// /// assert_eq!( /// query.to_string(MysqlQueryBuilder), - /// r#"SELECT `character` OVER ( PARTITION BY `font_size` ) FROM `character`"# + /// r#"SELECT MAX(`character`) OVER ( PARTITION BY `font_size` ) FROM `character`"# /// ); /// assert_eq!( /// query.to_string(PostgresQueryBuilder), - /// r#"SELECT "character" OVER ( PARTITION BY "font_size" ) FROM "character""# + /// r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) FROM "character""# /// ); /// assert_eq!( /// query.to_string(SqliteQueryBuilder), - /// r#"SELECT "character" OVER ( PARTITION BY "font_size" ) FROM "character""# + /// r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) FROM "character""# /// ); /// ``` pub fn expr_window(&mut self, expr: T, window: WindowStatement) -> &mut Self where T: Into, { - self.expr(SelectExpr { - expr: expr.into(), - alias: None, - window: Some(WindowSelectType::Query(window)), - }); + self.expr(expr.window(window)); self } @@ -736,7 +811,7 @@ impl SelectStatement { /// let query = Query::select() /// .from(Char::Table) /// .expr_window_as( - /// Expr::col(Char::Character), + /// Expr::col(Char::Character).max(), /// WindowStatement::partition_by(Char::FontSize), /// "C", /// ) @@ -744,15 +819,15 @@ impl SelectStatement { /// /// assert_eq!( /// query.to_string(MysqlQueryBuilder), - /// r#"SELECT `character` OVER ( PARTITION BY `font_size` ) AS `C` FROM `character`"# + /// r#"SELECT MAX(`character`) OVER ( PARTITION BY `font_size` ) AS `C` FROM `character`"# /// ); /// assert_eq!( /// query.to_string(PostgresQueryBuilder), - /// r#"SELECT "character" OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# + /// r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# /// ); /// assert_eq!( /// query.to_string(SqliteQueryBuilder), - /// r#"SELECT "character" OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# + /// r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# /// ); /// ``` pub fn expr_window_as(&mut self, expr: T, window: WindowStatement, alias: A) -> &mut Self @@ -760,11 +835,7 @@ impl SelectStatement { T: Into, A: IntoIden, { - self.expr(SelectExpr { - expr: expr.into(), - alias: Some(alias.into_iden()), - window: Some(WindowSelectType::Query(window)), - }); + self.expr(expr.window(window).alias(alias)); self } @@ -777,21 +848,21 @@ impl SelectStatement { /// /// let query = Query::select() /// .from(Char::Table) - /// .expr_window_name(Expr::col(Char::Character), "w") + /// .expr_window_name(Expr::col(Char::Character).max(), "w") /// .window("w", WindowStatement::partition_by(Char::FontSize)) /// .to_owned(); /// /// assert_eq!( /// query.to_string(MysqlQueryBuilder), - /// r#"SELECT `character` OVER `w` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# + /// r#"SELECT MAX(`character`) OVER `w` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# /// ); /// assert_eq!( /// query.to_string(PostgresQueryBuilder), - /// r#"SELECT "character" OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + /// r#"SELECT MAX("character") OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# /// ); /// assert_eq!( /// query.to_string(SqliteQueryBuilder), - /// r#"SELECT "character" OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + /// r#"SELECT MAX("character") OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# /// ); /// ``` pub fn expr_window_name(&mut self, expr: T, window: W) -> &mut Self @@ -799,11 +870,7 @@ impl SelectStatement { T: Into, W: IntoIden, { - self.expr(SelectExpr { - expr: expr.into(), - alias: None, - window: Some(WindowSelectType::Name(window.into_iden())), - }); + self.expr(expr.window_name(window)); self } @@ -816,21 +883,21 @@ impl SelectStatement { /// /// let query = Query::select() /// .from(Char::Table) - /// .expr_window_name_as(Expr::col(Char::Character), "w", "C") + /// .expr_window_name_as(Expr::col(Char::Character).max(), "w", "C") /// .window("w", WindowStatement::partition_by(Char::FontSize)) /// .to_owned(); /// /// assert_eq!( /// query.to_string(MysqlQueryBuilder), - /// r#"SELECT `character` OVER `w` AS `C` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# + /// r#"SELECT MAX(`character`) OVER `w` AS `C` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# /// ); /// assert_eq!( /// query.to_string(PostgresQueryBuilder), - /// r#"SELECT "character" OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + /// r#"SELECT MAX("character") OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# /// ); /// assert_eq!( /// query.to_string(SqliteQueryBuilder), - /// r#"SELECT "character" OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + /// r#"SELECT MAX("character") OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# /// ); /// ``` pub fn expr_window_name_as(&mut self, expr: T, window: W, alias: A) -> &mut Self @@ -839,11 +906,7 @@ impl SelectStatement { A: IntoIden, W: IntoIden, { - self.expr(SelectExpr { - expr: expr.into(), - alias: Some(alias.into_iden()), - window: Some(WindowSelectType::Name(window.into_iden())), - }); + self.expr(expr.window_name(window).alias(alias)); self } @@ -2549,21 +2612,21 @@ impl SelectStatement { /// /// let query = Query::select() /// .from(Char::Table) - /// .expr_window_name_as(Expr::col(Char::Character), "w", "C") + /// .expr_window_name_as(Expr::col(Char::Character).max(), "w", "C") /// .window("w", WindowStatement::partition_by(Char::FontSize)) /// .to_owned(); /// /// assert_eq!( /// query.to_string(MysqlQueryBuilder), - /// r#"SELECT `character` OVER `w` AS `C` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# + /// r#"SELECT MAX(`character`) OVER `w` AS `C` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# /// ); /// assert_eq!( /// query.to_string(PostgresQueryBuilder), - /// r#"SELECT "character" OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + /// r#"SELECT MAX("character") OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# /// ); /// assert_eq!( /// query.to_string(SqliteQueryBuilder), - /// r#"SELECT "character" OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + /// r#"SELECT MAX("character") OVER "w" AS "C" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# /// ); /// ``` pub fn window(&mut self, name: A, window: WindowStatement) -> &mut Self diff --git a/tests/mysql/query.rs b/tests/mysql/query.rs index b1415a7c9..d71664caa 100644 --- a/tests/mysql/query.rs +++ b/tests/mysql/query.rs @@ -447,6 +447,15 @@ fn select_32() { .to_string(MysqlQueryBuilder), "SELECT `character` AS `C` FROM `character`" ); + + // Same SQL as `expr_as`, but expressed via `SelectExprTrait`. + assert_eq!( + Query::select() + .expr(Expr::col(Char::Character).alias("C")) + .from(Char::Table) + .to_string(MysqlQueryBuilder), + "SELECT `character` AS `C` FROM `character`" + ); } #[test] @@ -1042,6 +1051,34 @@ fn select_61() { ); } +#[test] +fn select_62() { + assert_eq!( + Query::select() + .expr( + Expr::col(Char::Character) + .max() + .window(WindowStatement::partition_by(Char::FontSize)) + .alias("C"), + ) + .from(Char::Table) + .to_string(MysqlQueryBuilder), + r#"SELECT MAX(`character`) OVER ( PARTITION BY `font_size` ) AS `C` FROM `character`"# + ); +} + +#[test] +fn select_63() { + assert_eq!( + Query::select() + .expr(Expr::col(Char::Character).max().window_name("w")) + .from(Char::Table) + .window("w", WindowStatement::partition_by(Char::FontSize)) + .to_string(MysqlQueryBuilder), + r#"SELECT MAX(`character`) OVER `w` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# + ); +} + #[test] fn md5_fn() { assert_eq!( diff --git a/tests/postgres/query.rs b/tests/postgres/query.rs index 10566bbe3..58e970e47 100644 --- a/tests/postgres/query.rs +++ b/tests/postgres/query.rs @@ -499,6 +499,15 @@ fn select_32() { query.audit_unwrap().selected_tables(), [Char::Table.into_iden()] ); + + // Same SQL as `expr_as`, but expressed via `SelectExprTrait`. + assert_eq!( + Query::select() + .expr(Expr::col(Char::Character).alias("C")) + .from(Char::Table) + .to_string(PostgresQueryBuilder), + r#"SELECT "character" AS "C" FROM "character""# + ); } #[test] @@ -1262,6 +1271,34 @@ fn select_64() { assert_eq!(query.audit_unwrap().selected_tables(), []); } +#[test] +fn select_65() { + assert_eq!( + Query::select() + .expr( + Expr::col(Char::Character) + .max() + .window(WindowStatement::partition_by(Char::FontSize)) + .alias("C"), + ) + .from(Char::Table) + .to_string(PostgresQueryBuilder), + r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# + ); +} + +#[test] +fn select_66() { + assert_eq!( + Query::select() + .expr(Expr::col(Char::Character).max().window_name("w")) + .from(Char::Table) + .window("w", WindowStatement::partition_by(Char::FontSize)) + .to_string(PostgresQueryBuilder), + r#"SELECT MAX("character") OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + ); +} + #[test] #[allow(clippy::approx_constant)] fn insert_2() { diff --git a/tests/sqlite/query.rs b/tests/sqlite/query.rs index a82c2d4e6..0309dd8ce 100644 --- a/tests/sqlite/query.rs +++ b/tests/sqlite/query.rs @@ -448,6 +448,15 @@ fn select_32() { .to_string(SqliteQueryBuilder), r#"SELECT "character" AS "C" FROM "character""# ); + + // Same SQL as `expr_as`, but expressed via `SelectExprTrait`. + assert_eq!( + Query::select() + .expr(Expr::col(Char::Character).alias("C")) + .from(Char::Table) + .to_string(SqliteQueryBuilder), + r#"SELECT "character" AS "C" FROM "character""# + ); } #[test] @@ -987,6 +996,34 @@ fn select_58() { ); } +#[test] +fn select_59() { + assert_eq!( + Query::select() + .expr( + Expr::col(Char::Character) + .max() + .window(WindowStatement::partition_by(Char::FontSize)) + .alias("C"), + ) + .from(Char::Table) + .to_string(SqliteQueryBuilder), + r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# + ); +} + +#[test] +fn select_60() { + assert_eq!( + Query::select() + .expr(Expr::col(Char::Character).max().window_name("w")) + .from(Char::Table) + .window("w", WindowStatement::partition_by(Char::FontSize)) + .to_string(SqliteQueryBuilder), + r#"SELECT MAX("character") OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# + ); +} + #[test] fn glob_bin_oper() { assert_eq!( From 9ec7a074334d10224d2841ac709fb1642b6146f5 Mon Sep 17 00:00:00 2001 From: yuly3 Date: Mon, 12 Jan 2026 00:35:38 +0900 Subject: [PATCH 2/2] refactor: replace window/window_name API with unified over method - Add From and From impls for WindowSelectType - Replace SelectExprTrait::window() and window_name() with over(impl Into) --- src/query/select.rs | 51 ++++++++++++++++++----------------------- tests/mysql/query.rs | 6 ++--- tests/postgres/query.rs | 6 ++--- tests/sqlite/query.rs | 6 ++--- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/query/select.rs b/src/query/select.rs index 64c11468e..71288ac4e 100644 --- a/src/query/select.rs +++ b/src/query/select.rs @@ -201,6 +201,18 @@ where } } +impl From for WindowSelectType { + fn from(stmt: WindowStatement) -> Self { + Self::Query(stmt) + } +} + +impl From for WindowSelectType { + fn from(iden: T) -> Self { + Self::Name(iden.into_iden()) + } +} + /// Extension methods for building a [`SelectExpr`] from an expression. /// /// This makes it ergonomic to attach select-specific modifiers (like `AS` and `OVER`) and pass the @@ -216,7 +228,7 @@ where /// .expr( /// Expr::col(Char::Character) /// .max() -/// .window(WindowStatement::partition_by(Char::FontSize)) +/// .over(WindowStatement::partition_by(Char::FontSize)) /// .alias("C"), /// ) /// .to_owned(); @@ -231,11 +243,7 @@ pub trait SelectExprTrait: Sized { where A: IntoIden; - fn window(self, window: WindowStatement) -> SelectExpr; - - fn window_name(self, window: W) -> SelectExpr - where - W: IntoIden; + fn over(self, over_expr: impl Into) -> SelectExpr; } impl SelectExprTrait for SelectExpr { @@ -247,16 +255,8 @@ impl SelectExprTrait for SelectExpr { self } - fn window(mut self, window: WindowStatement) -> SelectExpr { - self.window = Some(WindowSelectType::Query(window)); - self - } - - fn window_name(mut self, window: W) -> SelectExpr - where - W: IntoIden, - { - self.window = Some(WindowSelectType::Name(window.into_iden())); + fn over(mut self, over_expr: impl Into) -> SelectExpr { + self.window = Some(over_expr.into()); self } } @@ -272,15 +272,8 @@ where SelectExpr::from(self).alias(alias) } - fn window(self, window: WindowStatement) -> SelectExpr { - SelectExpr::from(self).window(window) - } - - fn window_name(self, window: W) -> SelectExpr - where - W: IntoIden, - { - SelectExpr::from(self).window_name(window) + fn over(self, over_expr: impl Into) -> SelectExpr { + SelectExpr::from(self).over(over_expr) } } @@ -797,7 +790,7 @@ impl SelectStatement { where T: Into, { - self.expr(expr.window(window)); + self.expr(expr.over(window)); self } @@ -835,7 +828,7 @@ impl SelectStatement { T: Into, A: IntoIden, { - self.expr(expr.window(window).alias(alias)); + self.expr(expr.over(window).alias(alias)); self } @@ -870,7 +863,7 @@ impl SelectStatement { T: Into, W: IntoIden, { - self.expr(expr.window_name(window)); + self.expr(expr.over(window)); self } @@ -906,7 +899,7 @@ impl SelectStatement { A: IntoIden, W: IntoIden, { - self.expr(expr.window_name(window).alias(alias)); + self.expr(expr.over(window).alias(alias)); self } diff --git a/tests/mysql/query.rs b/tests/mysql/query.rs index d71664caa..5aba7ae25 100644 --- a/tests/mysql/query.rs +++ b/tests/mysql/query.rs @@ -1055,13 +1055,13 @@ fn select_61() { fn select_62() { assert_eq!( Query::select() + .from(Char::Table) .expr( Expr::col(Char::Character) .max() - .window(WindowStatement::partition_by(Char::FontSize)) + .over(WindowStatement::partition_by(Char::FontSize)) .alias("C"), ) - .from(Char::Table) .to_string(MysqlQueryBuilder), r#"SELECT MAX(`character`) OVER ( PARTITION BY `font_size` ) AS `C` FROM `character`"# ); @@ -1071,8 +1071,8 @@ fn select_62() { fn select_63() { assert_eq!( Query::select() - .expr(Expr::col(Char::Character).max().window_name("w")) .from(Char::Table) + .expr(Expr::col(Char::Character).max().over("w")) .window("w", WindowStatement::partition_by(Char::FontSize)) .to_string(MysqlQueryBuilder), r#"SELECT MAX(`character`) OVER `w` FROM `character` WINDOW `w` AS (PARTITION BY `font_size`)"# diff --git a/tests/postgres/query.rs b/tests/postgres/query.rs index 58e970e47..ba70fc013 100644 --- a/tests/postgres/query.rs +++ b/tests/postgres/query.rs @@ -1275,13 +1275,13 @@ fn select_64() { fn select_65() { assert_eq!( Query::select() + .from(Char::Table) .expr( Expr::col(Char::Character) .max() - .window(WindowStatement::partition_by(Char::FontSize)) + .over(WindowStatement::partition_by(Char::FontSize)) .alias("C"), ) - .from(Char::Table) .to_string(PostgresQueryBuilder), r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# ); @@ -1291,8 +1291,8 @@ fn select_65() { fn select_66() { assert_eq!( Query::select() - .expr(Expr::col(Char::Character).max().window_name("w")) .from(Char::Table) + .expr(Expr::col(Char::Character).max().over("w")) .window("w", WindowStatement::partition_by(Char::FontSize)) .to_string(PostgresQueryBuilder), r#"SELECT MAX("character") OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"# diff --git a/tests/sqlite/query.rs b/tests/sqlite/query.rs index 0309dd8ce..2112382ca 100644 --- a/tests/sqlite/query.rs +++ b/tests/sqlite/query.rs @@ -1000,13 +1000,13 @@ fn select_58() { fn select_59() { assert_eq!( Query::select() + .from(Char::Table) .expr( Expr::col(Char::Character) .max() - .window(WindowStatement::partition_by(Char::FontSize)) + .over(WindowStatement::partition_by(Char::FontSize)) .alias("C"), ) - .from(Char::Table) .to_string(SqliteQueryBuilder), r#"SELECT MAX("character") OVER ( PARTITION BY "font_size" ) AS "C" FROM "character""# ); @@ -1016,8 +1016,8 @@ fn select_59() { fn select_60() { assert_eq!( Query::select() - .expr(Expr::col(Char::Character).max().window_name("w")) .from(Char::Table) + .expr(Expr::col(Char::Character).max().over("w")) .window("w", WindowStatement::partition_by(Char::FontSize)) .to_string(SqliteQueryBuilder), r#"SELECT MAX("character") OVER "w" FROM "character" WINDOW "w" AS (PARTITION BY "font_size")"#