From ab0f673f44184c733ab8483b527ad05072373545 Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 1 Sep 2025 12:00:01 +0200 Subject: [PATCH 1/7] Add benchmark for result column expression handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new query combines multiple aggregate functions, plain columns, arithmetic expressions, and aggregates wrapped in additional expressions. Local run results: ``` Prepare `SELECT first_name, last_name, state, city, age + 10, LENGTH(email), UPPER(first_name), LOWE... time: [64.535 µs 64.623 µs 64.713 µs] Found 9 outliers among 100 measurements (9.00%) 4 (4.00%) high mild 5 (5.00%) high severe ``` --- core/benches/benchmark.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/benches/benchmark.rs b/core/benches/benchmark.rs index fda384712..5db976086 100644 --- a/core/benches/benchmark.rs +++ b/core/benches/benchmark.rs @@ -1,5 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use pprof::criterion::{Output, PProfProfiler}; +use regex::Regex; use std::{sync::Arc, time::Instant}; use turso_core::{Database, PlatformIO}; @@ -272,9 +273,41 @@ fn bench_prepare_query(criterion: &mut Criterion) { "SELECT 1", "SELECT * FROM users LIMIT 1", "SELECT first_name, count(1) FROM users GROUP BY first_name HAVING count(1) > 1 ORDER BY count(1) LIMIT 1", + "SELECT + first_name, + last_name, + state, + city, + age + 10, + LENGTH(email), + UPPER(first_name), + LOWER(last_name), + SUBSTR(phone_number, 1, 3), + zipcode || '-' || state, + AVG(age) + 5, + MAX(age) - MIN(age), + ROUND(AVG(age), 1), + SUM(age) / COUNT(*), + COUNT(*), + COUNT(email), + SUM(age), + AVG(age), + MIN(age), + MAX(age), + SUM(CASE WHEN age >= 18 THEN 1 ELSE 0 END), + SUM(CASE WHEN age < 18 THEN 1 ELSE 0 END), + AVG(CASE WHEN age >= 18 THEN age ELSE NULL END), + MAX(CASE WHEN age >= 18 THEN age ELSE NULL END) + FROM users + GROUP BY state, city", ]; + let whitespace_re = Regex::new(r"\s+").unwrap(); for query in queries.iter() { + // Normalize whitespace in the query string by replacing all sequences of whitespace with a single space. + let query = whitespace_re.replace_all(query, " ").to_string(); + let query = query.as_str(); + let mut group = criterion.benchmark_group(format!("Prepare `{query}`")); group.bench_with_input( From d3617348199c2034bf21849a22a81e2caf847c13 Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 1 Sep 2025 20:36:24 +0200 Subject: [PATCH 2/7] Remove unnecessary recursion in resolve_aggregates The walk_expr method already traverses arguments, so there is no need to do this explicitly. --- core/translate/planner.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 522256a25..1bbd21869 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -76,11 +76,7 @@ pub fn resolve_aggregates( aggs.push(Aggregate::new(f, args, expr, distinctness)); contains_aggregates = true; } - _ => { - for arg in args.iter() { - contains_aggregates |= resolve_aggregates(schema, arg, aggs)?; - } - } + _ => {} } } Expr::FunctionCallStar { name, filter_over } => { From f3cbc382ce0c8158bd81a819c896521e12653b5b Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 1 Sep 2025 14:36:29 +0200 Subject: [PATCH 3/7] Support external aggregate functions wrapped in expressions Handled in the same way as in `prepare_one_select_plan` for bare function calls. In `prepare_one_select_plan`, however, resolving external scalar functions is performed unnecessarily twice. --- core/translate/planner.rs | 41 ++++++++++++++++++++++++--------- core/translate/select.rs | 14 +++++++---- testing/cli_tests/extensions.py | 10 ++++++++ 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 1bbd21869..9abe38f1f 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -10,6 +10,7 @@ use super::{ select::prepare_select_plan, SymbolTable, }; +use crate::function::{AggFunc, ExtFunc}; use crate::translate::expr::WalkControl; use crate::{ function::Func, @@ -29,6 +30,7 @@ pub const ROWID: &str = "rowid"; pub fn resolve_aggregates( schema: &Schema, + syms: &SymbolTable, top_level_expr: &Expr, aggs: &mut Vec, ) -> Result { @@ -60,22 +62,39 @@ pub fn resolve_aggregates( ); } let args_count = args.len(); + let distinctness = Distinctness::from_ast(distinctness.as_ref()); + + if !schema.indexes_enabled() && distinctness.is_distinct() { + crate::bail_parse_error!( + "SELECT with DISTINCT is not allowed without indexes enabled" + ); + } + if distinctness.is_distinct() && args_count != 1 { + crate::bail_parse_error!( + "DISTINCT aggregate functions must have exactly one argument" + ); + } match Func::resolve_function(name.as_str(), args_count) { Ok(Func::Agg(f)) => { - let distinctness = Distinctness::from_ast(distinctness.as_ref()); - if !schema.indexes_enabled() && distinctness.is_distinct() { - crate::bail_parse_error!( - "SELECT with DISTINCT is not allowed without indexes enabled" - ); - } - if distinctness.is_distinct() && args.len() != 1 { - crate::bail_parse_error!( - "DISTINCT aggregate functions must have exactly one argument" - ); - } aggs.push(Aggregate::new(f, args, expr, distinctness)); contains_aggregates = true; } + Err(e) => { + if let Some(f) = syms.resolve_function(name.as_str(), args_count) { + if let ExtFunc::Aggregate { .. } = f.as_ref().func { + let agg = Aggregate::new( + AggFunc::External(f.func.clone().into()), + args, + expr, + distinctness, + ); + aggs.push(agg); + contains_aggregates = true; + } + } else { + return Err(e); + } + } _ => {} } } diff --git a/core/translate/select.rs b/core/translate/select.rs index 59239094e..ee7618dfc 100644 --- a/core/translate/select.rs +++ b/core/translate/select.rs @@ -387,6 +387,7 @@ fn prepare_one_select_plan( Ok(_) => { let contains_aggregates = resolve_aggregates( schema, + syms, expr, &mut aggregate_expressions, )?; @@ -408,6 +409,7 @@ fn prepare_one_select_plan( if let ExtFunc::Scalar(_) = f.as_ref().func { let contains_aggregates = resolve_aggregates( schema, + syms, expr, &mut aggregate_expressions, )?; @@ -499,8 +501,12 @@ fn prepare_one_select_plan( } } expr => { - let contains_aggregates = - resolve_aggregates(schema, expr, &mut aggregate_expressions)?; + let contains_aggregates = resolve_aggregates( + schema, + syms, + expr, + &mut aggregate_expressions, + )?; plan.result_columns.push(ResultSetColumn { alias: maybe_alias.as_ref().map(|alias| match alias { ast::As::Elided(alias) => alias.as_str().to_string(), @@ -554,7 +560,7 @@ fn prepare_one_select_plan( connection, )?; let contains_aggregates = - resolve_aggregates(schema, expr, &mut aggregate_expressions)?; + resolve_aggregates(schema, syms, expr, &mut aggregate_expressions)?; if !contains_aggregates { // TODO: sqlite allows HAVING clauses with non aggregate expressions like // HAVING id = 5. We should support this too eventually (I guess). @@ -586,7 +592,7 @@ fn prepare_one_select_plan( Some(&plan.result_columns), connection, )?; - resolve_aggregates(schema, &o.expr, &mut plan.aggregates)?; + resolve_aggregates(schema, syms, &o.expr, &mut plan.aggregates)?; key.push((o.expr, o.order.unwrap_or(ast::SortOrder::Asc))); } diff --git a/testing/cli_tests/extensions.py b/testing/cli_tests/extensions.py index 8ce7341f0..f53621fb9 100755 --- a/testing/cli_tests/extensions.py +++ b/testing/cli_tests/extensions.py @@ -153,6 +153,11 @@ def test_aggregates(): validate_median, "median agg function works", ) + limbo.run_test_fn( + "select CASE WHEN median(value) > 0 THEN median(value) ELSE 0 END from numbers;", + validate_median, + "median agg function wrapped in expression works", + ) limbo.execute_dot("INSERT INTO numbers (value) VALUES (8.0);\n") limbo.run_test_fn( "select median(value) from numbers;", @@ -184,6 +189,11 @@ def test_grouped_aggregates(): lambda res: "2.0\n5.5" == res, "median aggregate function works", ) + limbo.run_test_fn( + "select CASE WHEN median(value) > 0 THEN median(value) ELSE 0 END from numbers GROUP BY category;", + lambda res: "2.0\n5.5" == res, + "median aggregate function wrapped in expression works", + ) limbo.run_test_fn( "SELECT percentile(value, percent) FROM test GROUP BY category;", lambda res: "12.5\n30.0\n45.0\n70.0" == res, From 9b742a64c2e3a6ac9a0158d1b0dcf2ced5d4bdde Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 1 Sep 2025 15:43:24 +0200 Subject: [PATCH 4/7] Handle functions with star argument wrapped in expressions Handled in the same way as in `prepare_one_select_plan` for bare function calls. --- core/translate/planner.rs | 22 +++++++++++++++++++--- testing/agg-functions.test | 16 ++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 9abe38f1f..c7e5c9f6a 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -104,9 +104,25 @@ pub fn resolve_aggregates( "FILTER clause is not supported yet in aggregate functions" ); } - if let Ok(Func::Agg(f)) = Func::resolve_function(name.as_str(), 0) { - aggs.push(Aggregate::new(f, &[], expr, Distinctness::NonDistinct)); - contains_aggregates = true; + match Func::resolve_function(name.as_str(), 0) { + Ok(Func::Agg(f)) => { + aggs.push(Aggregate::new(f, &[], expr, Distinctness::NonDistinct)); + contains_aggregates = true; + } + Ok(_) => { + crate::bail_parse_error!("Invalid aggregate function: {}", name.as_str()); + } + Err(e) => match e { + crate::LimboError::ParseError(e) => { + crate::bail_parse_error!("{}", e); + } + _ => { + crate::bail_parse_error!( + "Invalid aggregate function: {}", + name.as_str() + ); + } + }, } } _ => {} diff --git a/testing/agg-functions.test b/testing/agg-functions.test index 13b4600d7..7caf628b9 100755 --- a/testing/agg-functions.test +++ b/testing/agg-functions.test @@ -175,3 +175,19 @@ do_execsql_test select-json-group-object-no-sorting-required { 3|{"3437":"Amanda"} 5|{"2378":"Amy","3227":"Amy","5605":"Amanda"} 7|{"2454":"Amber"}} + +do_execsql_test_error_content select-max-star { + SELECT max(*) FROM users; +} {"wrong number of arguments to function"} + +do_execsql_test_error_content select-max-star-in-expression { + SELECT CASE WHEN max(*) > 0 THEN 1 ELSE 0 END FROM users; +} {"wrong number of arguments to function"} + +do_execsql_test_error select-scalar-func-star { + SELECT abs(*) FROM users; +} {.*(Invalid aggregate function|wrong number of arguments to function).*} + +do_execsql_test_error select-scalar-func-star-in-expression { + SELECT CASE WHEN abs(*) > 0 THEN 1 ELSE 0 END FROM users; +} {.*(Invalid aggregate function|wrong number of arguments to function).*} From 569e41cb1e8340ef35dccce186d83d8e9c32bb2d Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 1 Sep 2025 16:16:14 +0200 Subject: [PATCH 5/7] Skip traversing children of aggregate functions Aggregate functions cannot be nested, and this is validated during the translation of aggregate function arguments. Therefore, traversing their child expressions is unnecessary. --- core/translate/planner.rs | 5 ++++- testing/agg-functions.test | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/translate/planner.rs b/core/translate/planner.rs index c7e5c9f6a..001c734a1 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -41,7 +41,7 @@ pub fn resolve_aggregates( .any(|a| exprs_are_equivalent(&a.original_expr, expr)) { contains_aggregates = true; - return Ok(WalkControl::Continue); + return Ok(WalkControl::SkipChildren); } match expr { Expr::FunctionCall { @@ -78,6 +78,7 @@ pub fn resolve_aggregates( Ok(Func::Agg(f)) => { aggs.push(Aggregate::new(f, args, expr, distinctness)); contains_aggregates = true; + return Ok(WalkControl::SkipChildren); } Err(e) => { if let Some(f) = syms.resolve_function(name.as_str(), args_count) { @@ -90,6 +91,7 @@ pub fn resolve_aggregates( ); aggs.push(agg); contains_aggregates = true; + return Ok(WalkControl::SkipChildren); } } else { return Err(e); @@ -108,6 +110,7 @@ pub fn resolve_aggregates( Ok(Func::Agg(f)) => { aggs.push(Aggregate::new(f, &[], expr, Distinctness::NonDistinct)); contains_aggregates = true; + return Ok(WalkControl::SkipChildren); } Ok(_) => { crate::bail_parse_error!("Invalid aggregate function: {}", name.as_str()); diff --git a/testing/agg-functions.test b/testing/agg-functions.test index 7caf628b9..52c45ce9b 100755 --- a/testing/agg-functions.test +++ b/testing/agg-functions.test @@ -191,3 +191,11 @@ do_execsql_test_error select-scalar-func-star { do_execsql_test_error select-scalar-func-star-in-expression { SELECT CASE WHEN abs(*) > 0 THEN 1 ELSE 0 END FROM users; } {.*(Invalid aggregate function|wrong number of arguments to function).*} + +do_execsql_test_error_content select-nested-agg-func { + SELECT max(abs(sum(age))), sum(age) FROM users; +} {"misuse of aggregate function"} + +do_execsql_test_error_content select-nested-agg-func-in-expression { + SELECT CASE WHEN max(abs(sum(age))) > 0 THEN 1 ELSE 0 END, sum(age) FROM users; +} {"misuse of aggregate function"} From 517f23013adde3ad6add95f154b007b4d269b277 Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 1 Sep 2025 21:26:47 +0200 Subject: [PATCH 6/7] Delay deduplication of aggregate expressions It is not necessary to iterate over existing aggregates for every traversed expression. Instead, do so only when an aggregate function is found. --- core/translate/planner.rs | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 001c734a1..df496a1d9 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -36,13 +36,6 @@ pub fn resolve_aggregates( ) -> Result { let mut contains_aggregates = false; walk_expr(top_level_expr, &mut |expr: &Expr| -> Result { - if aggs - .iter() - .any(|a| exprs_are_equivalent(&a.original_expr, expr)) - { - contains_aggregates = true; - return Ok(WalkControl::SkipChildren); - } match expr { Expr::FunctionCall { name, @@ -76,20 +69,20 @@ pub fn resolve_aggregates( } match Func::resolve_function(name.as_str(), args_count) { Ok(Func::Agg(f)) => { - aggs.push(Aggregate::new(f, args, expr, distinctness)); + add_aggregate_if_not_exists(aggs, expr, args, distinctness, f); contains_aggregates = true; return Ok(WalkControl::SkipChildren); } Err(e) => { if let Some(f) = syms.resolve_function(name.as_str(), args_count) { if let ExtFunc::Aggregate { .. } = f.as_ref().func { - let agg = Aggregate::new( - AggFunc::External(f.func.clone().into()), - args, + add_aggregate_if_not_exists( + aggs, expr, + args, distinctness, + AggFunc::External(f.func.clone().into()), ); - aggs.push(agg); contains_aggregates = true; return Ok(WalkControl::SkipChildren); } @@ -108,7 +101,7 @@ pub fn resolve_aggregates( } match Func::resolve_function(name.as_str(), 0) { Ok(Func::Agg(f)) => { - aggs.push(Aggregate::new(f, &[], expr, Distinctness::NonDistinct)); + add_aggregate_if_not_exists(aggs, expr, &[], Distinctness::NonDistinct, f); contains_aggregates = true; return Ok(WalkControl::SkipChildren); } @@ -137,6 +130,21 @@ pub fn resolve_aggregates( Ok(contains_aggregates) } +fn add_aggregate_if_not_exists( + aggs: &mut Vec, + expr: &Expr, + args: &[Box], + distinctness: Distinctness, + func: AggFunc, +) { + if aggs + .iter() + .all(|a| !exprs_are_equivalent(&a.original_expr, expr)) + { + aggs.push(Aggregate::new(func, args, expr, distinctness)); + } +} + pub fn bind_column_references( top_level_expr: &mut Expr, referenced_tables: &mut TableReferences, From e97cc64ad0f61f00708a75972e029f45b6c79ebf Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 1 Sep 2025 15:50:58 +0200 Subject: [PATCH 7/7] Remove duplicated code for resolving aggregates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This also gave a small performance boost. Local run results: ``` Prepare `SELECT first_name, last_name, state, city, age + 10, LENGTH(email), UPPER(first_name), LOWE... time: [59.791 µs 59.898 µs 60.006 µs] change: [-7.7090% -7.2760% -6.8242%] (p = 0.00 < 0.05) Performance has improved. Found 10 outliers among 100 measurements (10.00%) 8 (8.00%) high mild 2 (2.00%) high severe ``` --- core/translate/select.rs | 190 +++------------------------------------ 1 file changed, 11 insertions(+), 179 deletions(-) diff --git a/core/translate/select.rs b/core/translate/select.rs index ee7618dfc..cee03b87a 100644 --- a/core/translate/select.rs +++ b/core/translate/select.rs @@ -3,10 +3,9 @@ use super::plan::{ select_star, Distinctness, JoinOrderMember, Operation, OuterQueryReference, QueryDestination, Search, TableReferences, WhereTerm, }; -use crate::function::{AggFunc, ExtFunc, Func}; use crate::schema::Table; use crate::translate::optimizer::optimize_plan; -use crate::translate::plan::{Aggregate, GroupBy, Plan, ResultSetColumn, SelectPlan}; +use crate::translate::plan::{GroupBy, Plan, ResultSetColumn, SelectPlan}; use crate::translate::planner::{ bind_column_references, break_predicate_at_and_boundaries, parse_from, parse_limit, parse_where, resolve_aggregates, @@ -340,183 +339,16 @@ fn prepare_one_select_plan( Some(&plan.result_columns), connection, )?; - match expr.as_ref() { - ast::Expr::FunctionCall { - name, - distinctness, - args, - filter_over, - order_by, - } => { - if filter_over.filter_clause.is_some() - || filter_over.over_clause.is_some() - { - crate::bail_parse_error!( - "FILTER clause is not supported yet in aggregate functions" - ); - } - if !order_by.is_empty() { - crate::bail_parse_error!("ORDER BY clause is not supported yet in aggregate functions"); - } - let args_count = args.len(); - let distinctness = Distinctness::from_ast(distinctness.as_ref()); - - if !schema.indexes_enabled() && distinctness.is_distinct() { - crate::bail_parse_error!( - "SELECT with DISTINCT is not allowed without indexes enabled" - ); - } - if distinctness.is_distinct() && args_count != 1 { - crate::bail_parse_error!("DISTINCT aggregate functions must have exactly one argument"); - } - match Func::resolve_function(name.as_str(), args_count) { - Ok(Func::Agg(f)) => { - let agg = Aggregate::new(f, args, expr, distinctness); - aggregate_expressions.push(agg); - plan.result_columns.push(ResultSetColumn { - alias: maybe_alias.as_ref().map(|alias| match alias { - ast::As::Elided(alias) => { - alias.as_str().to_string() - } - ast::As::As(alias) => alias.as_str().to_string(), - }), - expr: *expr.clone(), - contains_aggregates: true, - }); - } - Ok(_) => { - let contains_aggregates = resolve_aggregates( - schema, - syms, - expr, - &mut aggregate_expressions, - )?; - plan.result_columns.push(ResultSetColumn { - alias: maybe_alias.as_ref().map(|alias| match alias { - ast::As::Elided(alias) => { - alias.as_str().to_string() - } - ast::As::As(alias) => alias.as_str().to_string(), - }), - expr: *expr.clone(), - contains_aggregates, - }); - } - Err(e) => { - if let Some(f) = - syms.resolve_function(name.as_str(), args_count) - { - if let ExtFunc::Scalar(_) = f.as_ref().func { - let contains_aggregates = resolve_aggregates( - schema, - syms, - expr, - &mut aggregate_expressions, - )?; - plan.result_columns.push(ResultSetColumn { - alias: maybe_alias.as_ref().map(|alias| { - match alias { - ast::As::Elided(alias) => { - alias.as_str().to_string() - } - ast::As::As(alias) => { - alias.as_str().to_string() - } - } - }), - expr: *expr.clone(), - contains_aggregates, - }); - } else { - let agg = Aggregate::new( - AggFunc::External(f.func.clone().into()), - args, - expr, - distinctness, - ); - aggregate_expressions.push(agg); - plan.result_columns.push(ResultSetColumn { - alias: maybe_alias.as_ref().map(|alias| { - match alias { - ast::As::Elided(alias) => { - alias.as_str().to_string() - } - ast::As::As(alias) => { - alias.as_str().to_string() - } - } - }), - expr: *expr.clone(), - contains_aggregates: true, - }); - } - continue; // Continue with the normal flow instead of returning - } else { - return Err(e); - } - } - } - } - ast::Expr::FunctionCallStar { name, filter_over } => { - if filter_over.filter_clause.is_some() - || filter_over.over_clause.is_some() - { - crate::bail_parse_error!( - "FILTER clause is not supported yet in aggregate functions" - ); - } - match Func::resolve_function(name.as_str(), 0) { - Ok(Func::Agg(f)) => { - let agg = - Aggregate::new(f, &[], expr, Distinctness::NonDistinct); - aggregate_expressions.push(agg); - plan.result_columns.push(ResultSetColumn { - alias: maybe_alias.as_ref().map(|alias| match alias { - ast::As::Elided(alias) => { - alias.as_str().to_string() - } - ast::As::As(alias) => alias.as_str().to_string(), - }), - expr: *expr.clone(), - contains_aggregates: true, - }); - } - Ok(_) => { - crate::bail_parse_error!( - "Invalid aggregate function: {}", - name.as_str() - ); - } - Err(e) => match e { - crate::LimboError::ParseError(e) => { - crate::bail_parse_error!("{}", e); - } - _ => { - crate::bail_parse_error!( - "Invalid aggregate function: {}", - name.as_str() - ); - } - }, - } - } - expr => { - let contains_aggregates = resolve_aggregates( - schema, - syms, - expr, - &mut aggregate_expressions, - )?; - plan.result_columns.push(ResultSetColumn { - alias: maybe_alias.as_ref().map(|alias| match alias { - ast::As::Elided(alias) => alias.as_str().to_string(), - ast::As::As(alias) => alias.as_str().to_string(), - }), - expr: expr.clone(), - contains_aggregates, - }); - } - } + let contains_aggregates = + resolve_aggregates(schema, syms, expr, &mut aggregate_expressions)?; + plan.result_columns.push(ResultSetColumn { + alias: maybe_alias.as_ref().map(|alias| match alias { + ast::As::Elided(alias) => alias.as_str().to_string(), + ast::As::As(alias) => alias.as_str().to_string(), + }), + expr: expr.as_ref().clone(), + contains_aggregates, + }); } } }