diff --git a/src/webserver/database/sql.rs b/src/webserver/database/sql.rs index b0fc2107..22de23bc 100644 --- a/src/webserver/database/sql.rs +++ b/src/webserver/database/sql.rs @@ -793,6 +793,19 @@ mod test { ); } + #[test] + fn test_constant_sqlpage_function_is_not_delayed() { + let mut ast = + parse_postgres_stmt("select sqlpage.url_encode(concat('/', $value)) as encoded"); + let functions = extract_delayed_functions_from_query(&mut ast); + + assert!(functions.is_empty()); + assert_eq!( + ast.to_string(), + "SELECT sqlpage.url_encode(concat('/', $value)) AS encoded" + ); + } + #[test] fn test_nested_sqlpage_functions_are_not_delayable() { let mut ast = diff --git a/src/webserver/database/sql/delayed_functions.rs b/src/webserver/database/sql/delayed_functions.rs index c1efd977..8333f017 100644 --- a/src/webserver/database/sql/delayed_functions.rs +++ b/src/webserver/database/sql/delayed_functions.rs @@ -4,6 +4,9 @@ use sqlparser::ast::{ }; use super::{DelayedFunctionCall, SqlPageFunctionName, extract_sqlpage_function_name}; +use crate::webserver::database::sql::parameter_extraction::{ + ParamExtractContext, function_args_to_stmt_params, +}; /// The execution of standalone projected `SQLPage` functions is delayed until after /// the query has been executed. For instance, `SELECT sqlpage.fetch(x) AS body FROM t` @@ -12,18 +15,36 @@ use super::{DelayedFunctionCall, SqlPageFunctionName, extract_sqlpage_function_n pub(super) fn extract_delayed_functions_from_query( stmt: &mut Statement, ) -> Vec { - let select_items = match stmt { - Statement::Query(q) => match q.body.as_mut() { - SetExpr::Select(s) => &mut s.projection, - _ => return Vec::new(), - }, - _ => return Vec::new(), - }; + match stmt { + Statement::Query(q) => { + let is_limited = q.limit_clause.is_some() || q.fetch.is_some(); + let SetExpr::Select(s) = q.body.as_mut() else { + return Vec::new(); + }; + // A constant single-row projection can be evaluated before the query. + // Besides avoiding an unnecessary query, this preserves SQLPage's + // cross-database argument semantics (notably NULL concatenation). + let is_constant_single_row = !is_limited && s.from.is_empty() && s.selection.is_none(); + extract_delayed_functions_from_projection(&mut s.projection, is_constant_single_row) + } + _ => Vec::new(), + } +} + +fn extract_delayed_functions_from_projection( + select_items: &mut Vec, + is_constant_single_row: bool, +) -> Vec { let mut delayed_function_calls = Vec::new(); let mut rewritten_projection = Vec::with_capacity(select_items.len()); for item in std::mem::take(select_items) { - rewrite_select_item(item, &mut rewritten_projection, &mut delayed_function_calls); + rewrite_select_item( + item, + is_constant_single_row, + &mut rewritten_projection, + &mut delayed_function_calls, + ); } *select_items = rewritten_projection; delayed_function_calls @@ -31,6 +52,7 @@ pub(super) fn extract_delayed_functions_from_query( fn rewrite_select_item( item: SelectItem, + is_constant_single_row: bool, rewritten_projection: &mut Vec, delayed_function_calls: &mut Vec, ) { @@ -39,7 +61,9 @@ fn rewrite_select_item( expr: Expr::Function(function), alias, } => { - if let Some(func_name) = delayable_sqlpage_function(&function) { + if let Some(func_name) = delayable_sqlpage_function(&function) + && (!is_constant_single_row || !can_be_evaluated_before_query(&function)) + { let (replacement_items, delayed_call) = rewrite_function_projection( function, func_name, @@ -56,7 +80,9 @@ fn rewrite_select_item( } } SelectItem::UnnamedExpr(Expr::Function(function)) => { - if let Some(func_name) = delayable_sqlpage_function(&function) { + if let Some(func_name) = delayable_sqlpage_function(&function) + && (!is_constant_single_row || !can_be_evaluated_before_query(&function)) + { let target_col_name = function.to_string(); let (replacement_items, delayed_call) = rewrite_function_projection( function, @@ -96,6 +122,14 @@ fn delayable_sqlpage_function(function: &Function) -> Option bool { + let FunctionArguments::List(FunctionArgumentList { args, .. }) = &function.args else { + return false; + }; + let mut args = args.clone(); + function_args_to_stmt_params(&mut args, &ParamExtractContext::default()).is_ok() +} + fn rewrite_function_projection( mut function: Function, func_name: SqlPageFunctionName, diff --git a/tests/sql_test_files/data/concat_str_in_pseudofunction.sql b/tests/sql_test_files/data/concat_str_in_pseudofunction.sql index 7532eaa8..724e44b1 100644 --- a/tests/sql_test_files/data/concat_str_in_pseudofunction.sql +++ b/tests/sql_test_files/data/concat_str_in_pseudofunction.sql @@ -1,3 +1,3 @@ select '%2F1' as expected, sqlpage.url_encode('/' || $x) as actual; select '%2F1' as expected, sqlpage.url_encode(CONCAT('/', $x)) as actual; -select 'fallback' as expected, coalesce(sqlpage.url_encode(CONCAT('/', $thisisnull)), 'fallback') as actual; +select NULL as expected, sqlpage.url_encode(CONCAT('/', $thisisnull)) as actual;