use super::SupportedDatabase; use super::csv_import::{CsvImport, extract_csv_copy_statement}; use super::sqlpage_functions::functions::SqlPageFunctionName; use super::syntax_tree::StmtParam; use crate::file_cache::AsyncFromStrWithState; use crate::webserver::database::DbInfo; use crate::webserver::database::error_highlighting::quote_source_with_highlight; use crate::{AppState, Database}; use async_trait::async_trait; use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::ast::{ CastKind, DataType, Expr, Function, FunctionArg, FunctionArgExpr, FunctionArgumentList, FunctionArguments, Ident, ObjectName, ObjectNamePart, SelectFlavor, SelectItem, Set, SetExpr, Spanned, Statement, Value, ValueWithSpan, }; use sqlparser::dialect::{ Dialect, DuckDbDialect, GenericDialect, MsSqlDialect, MySqlDialect, OracleDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, }; use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::Token::{self, EOF, SemiColon}; use sqlparser::tokenizer::{Location, Span, TokenWithSpan, Tokenizer}; use sqlx::any::AnyKind; use std::fmt::Write; use std::path::{Path, PathBuf}; use std::str::FromStr; mod parameter_extraction; pub(super) use self::parameter_extraction::{ DB_PLACEHOLDERS, DbPlaceHolder, ParamExtractContext, SqlPageFunctionError, function_args_to_stmt_params, }; use self::parameter_extraction::{ ParameterExtractor, TEMP_PLACEHOLDER_PREFIX, extract_ident_param, validate_function_calls, }; #[derive(Default)] pub struct ParsedSqlFile { pub(super) statements: Vec, pub source_path: PathBuf, } impl ParsedSqlFile { #[must_use] pub fn new(db: &Database, sql: &str, source_path: &Path) -> ParsedSqlFile { let dialect = dialect_for_db(db.info.database_type); log::debug!( "Parsing SQL file {} using dialect {:?}", source_path.display(), dialect ); let parsed_statements = match parse_sql(&db.info, dialect.as_ref(), sql) { Ok(parsed) => parsed, Err(err) => return Self::from_err(err, source_path), }; let statements = parsed_statements.collect(); ParsedSqlFile { statements, source_path: source_path.to_path_buf(), } } fn from_err(e: impl Into, source_path: &Path) -> Self { Self { statements: vec![ParsedStatement::Error( e.into() .context(format!("Error parsing file {}", source_path.display())), )], source_path: source_path.to_path_buf(), } } } #[async_trait(? Send)] impl AsyncFromStrWithState for ParsedSqlFile { async fn from_str_with_state( app_state: &AppState, source: &str, source_path: &Path, ) -> anyhow::Result { Ok(ParsedSqlFile::new(&app_state.db, source, source_path)) } } /// A single SQL statement that has been parsed from a SQL file. #[derive(Debug, PartialEq)] pub(super) struct StmtWithParams { /// The SQL query with placeholders for parameters. pub query: String, /// The line and column of the first token in the query. pub query_position: SourceSpan, /// Parameters that should be bound to the query. /// They can contain functions that will be called before the query is executed, /// the result of which will be bound to the query. pub params: Vec, /// Functions that are called on the result set after the query has been executed, /// and which can be passed the result of the query as an argument. pub delayed_functions: Vec, /// Columns that are JSON columns, and which should be converted to JSON objects after the query is executed. /// Only relevant for databases that do not have a native JSON type, and which return JSON values as text. pub json_columns: Vec, } /// A location in the source code. #[derive(Debug, PartialEq, Clone, Copy)] pub(super) struct SourceSpan { pub start: SourceLocation, pub end: SourceLocation, } /// A location in the source code. #[derive(Debug, PartialEq, Clone, Copy)] pub(super) struct SourceLocation { pub line: usize, pub column: usize, } #[derive(Debug)] pub(super) enum ParsedStatement { StmtWithParams(StmtWithParams), StaticSimpleSelect { values: Vec<(String, SimpleSelectValue)>, query_position: SourceSpan, }, SetVariable { variable: StmtParam, value: StmtWithParams, }, StaticSimpleSet { variable: StmtParam, value: SimpleSelectValue, }, CsvImport(CsvImport), Error(anyhow::Error), } #[derive(Debug, PartialEq)] pub(super) enum SimpleSelectValue { Static(serde_json::Value), Dynamic(StmtParam), } fn parse_sql<'a>( db_info: &'a DbInfo, dialect: &'a dyn Dialect, sql: &'a str, ) -> anyhow::Result + 'a> { log::trace!("Parsing {} SQL: {sql}", db_info.dbms_name); let tokens = Tokenizer::new(dialect, sql) .tokenize_with_location() .map_err(|err| { let location = err.location; anyhow::Error::new(err).context(format!("The SQLPage parser could not understand the SQL file. Tokenization failed. Please check for syntax errors:\n{}", quote_source_with_highlight(sql, location.line, location.column))) })?; let mut parser = Parser::new(dialect).with_tokens_with_locations(tokens); let mut has_error = false; Ok(std::iter::from_fn(move || { if has_error { // Return the first error and ignore the rest return None; } let statement = parse_single_statement(&mut parser, db_info, sql); log::debug!("Parsed statement: {statement:?}"); if let Some(ParsedStatement::Error(_)) = &statement { has_error = true; } statement })) } fn transform_to_positional_placeholders(stmt: &mut StmtWithParams, kind: AnyKind) { if let Some((_, DbPlaceHolder::Positional { placeholder })) = DB_PLACEHOLDERS .iter() .find(|(placeholder_kind, _)| *placeholder_kind == kind) { let mut new_params = Vec::new(); let mut query = stmt.query.clone(); while let Some(pos) = query.find(TEMP_PLACEHOLDER_PREFIX) { let start_of_number = pos + TEMP_PLACEHOLDER_PREFIX.len(); let end = query[start_of_number..] .find(|c: char| !c.is_ascii_digit()) .map_or(query.len(), |i| start_of_number + i); let param_idx = query[start_of_number..end].parse::().unwrap_or(1) - 1; query.replace_range(pos..end, placeholder); new_params.push(stmt.params[param_idx].clone()); } stmt.query = query; stmt.params = new_params; } } fn parse_single_statement( parser: &mut Parser<'_>, db_info: &DbInfo, source_sql: &str, ) -> Option { if parser.peek_token() == EOF { return None; } let mut stmt = match parser.parse_statement() { Ok(stmt) => stmt, Err(err) => return Some(syntax_error(err, parser, source_sql)), }; let mut semicolon = false; while parser.consume_token(&SemiColon) { semicolon = true; } let mut params = match ParameterExtractor::extract_parameters(&mut stmt, db_info.clone()) { Ok(p) => p, Err(err) => return Some(ParsedStatement::Error(err)), }; let dbms = db_info.database_type; if let Some(parsed) = extract_set_variable(&mut stmt, &mut params, db_info) { return Some(parsed); } if let Some(csv_import) = extract_csv_copy_statement(&mut stmt) { return Some(ParsedStatement::CsvImport(csv_import)); } if let Some(static_statement) = extract_static_simple_select(&stmt, ¶ms) { log::debug!( "Optimised a static simple select to avoid a trivial database query: {stmt} optimized to {static_statement:?}" ); return Some(ParsedStatement::StaticSimpleSelect { values: static_statement, query_position: extract_query_start(&stmt), }); } let delayed_functions = extract_toplevel_functions(&mut stmt); if let Err(err) = validate_function_calls(&stmt) { return Some(ParsedStatement::Error(err)); } let json_columns = extract_json_columns(&stmt, dbms); let query = format!( "{stmt}{semicolon}", semicolon = if semicolon { ";" } else { "" } ); let mut stmt_with_params = StmtWithParams { query, query_position: extract_query_start(&stmt), params, delayed_functions, json_columns, }; transform_to_positional_placeholders(&mut stmt_with_params, db_info.kind); log::debug!("Final transformed statement: {}", stmt_with_params.query); Some(ParsedStatement::StmtWithParams(stmt_with_params)) } fn extract_query_start(stmt: &impl Spanned) -> SourceSpan { let location = stmt.span(); SourceSpan { start: SourceLocation { line: usize::try_from(location.start.line).unwrap_or(0), column: usize::try_from(location.start.column).unwrap_or(0), }, end: SourceLocation { line: usize::try_from(location.end.line).unwrap_or(0), column: usize::try_from(location.end.column).unwrap_or(0), }, } } fn syntax_error(err: ParserError, parser: &Parser, sql: &str) -> ParsedStatement { let Span { start: Location { line: start_line, column: start_column, }, end: Location { line: end_line, .. }, } = parser.peek_token_no_skip().span; let mut msg = String::from( "Parsing failed: SQLPage couldn't understand the SQL file. Please check for syntax errors on ", ); if start_line == end_line { write!(&mut msg, "line {start_line}:").unwrap(); } else { write!(&mut msg, "lines {start_line} to {end_line}:").unwrap(); } write!( &mut msg, "\n{}", quote_source_with_highlight(sql, start_line, start_column) ) .unwrap(); ParsedStatement::Error(anyhow::Error::from(err).context(msg)) } fn dialect_for_db(dbms: SupportedDatabase) -> Box { match dbms { SupportedDatabase::Duckdb => Box::new(DuckDbDialect {}), SupportedDatabase::Oracle => Box::new(OracleDialect {}), SupportedDatabase::Postgres => Box::new(PostgreSqlDialect {}), SupportedDatabase::Generic => Box::new(GenericDialect {}), SupportedDatabase::Mssql => Box::new(MsSqlDialect {}), SupportedDatabase::MySql => Box::new(MySqlDialect {}), SupportedDatabase::Sqlite => Box::new(SQLiteDialect {}), SupportedDatabase::Snowflake => Box::new(SnowflakeDialect {}), } } #[derive(Debug, PartialEq)] pub struct DelayedFunctionCall { pub function: SqlPageFunctionName, pub argument_col_names: Vec, pub target_col_name: String, } /// The execution of top-level functions is delayed until after the query has been executed. /// For instance, `SELECT sqlpage.fetch(x) FROM t` will be executed as `SELECT x as _sqlpage_f0_a0 FROM t` /// and the `sqlpage.fetch` function will be called with the value of `_sqlpage_f0_a0` after the query has been executed, /// on each row of the result set. fn extract_toplevel_functions(stmt: &mut Statement) -> Vec { struct SelectItemToAdd { expr_to_insert: SelectItem, position: usize, } let mut delayed_function_calls: Vec = Vec::new(); let set_expr = match stmt { Statement::Query(q) => q.body.as_mut(), _ => return delayed_function_calls, }; let select_items = match set_expr { sqlparser::ast::SetExpr::Select(s) => &mut s.projection, _ => return delayed_function_calls, }; let mut select_items_to_add: Vec = Vec::new(); for (position, select_item) in select_items.iter_mut().enumerate() { let SelectItem::ExprWithAlias { expr: Expr::Function(Function { name: ObjectName(func_name_parts), args: FunctionArguments::List(FunctionArgumentList { args, duplicate_treatment: None, .. }), .. }), alias, } = select_item else { continue; }; let Some(func_name) = extract_sqlpage_function_name(func_name_parts) else { continue; }; func_name_parts.clear(); // mark the function for deletion let mut argument_col_names = Vec::with_capacity(args.len()); for (arg_idx, arg) in args.iter_mut().enumerate() { match arg { FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) | FunctionArg::Named { arg: FunctionArgExpr::Expr(expr), .. } => { let func_idx = delayed_function_calls.len(); let argument_col_name = format!("_sqlpage_f{func_idx}_a{arg_idx}"); argument_col_names.push(argument_col_name.clone()); let expr_to_insert = SelectItem::ExprWithAlias { expr: std::mem::replace(expr, Expr::value(Value::Null)), alias: Ident::with_quote('"', argument_col_name), }; select_items_to_add.push(SelectItemToAdd { expr_to_insert, position, }); } other => { log::error!("Unsupported argument to {func_name}: {other}"); } } } delayed_function_calls.push(DelayedFunctionCall { function: func_name, argument_col_names, target_col_name: alias.value.clone(), }); } // Insert the new select items (the function arguments) at the positions where the function calls were let mut it = select_items_to_add.into_iter().peekable(); *select_items = std::mem::take(select_items) .into_iter() .enumerate() .flat_map(|(position, item)| { let mut items = Vec::with_capacity(1); while it.peek().is_some_and(|x| x.position == position) { items.push(it.next().unwrap().expr_to_insert); } if items.is_empty() { items.push(item); } items }) .collect(); delayed_function_calls } fn extract_static_simple_select( stmt: &Statement, params: &[StmtParam], ) -> Option> { let set_expr = match stmt { Statement::Query(q) if q.limit_clause.is_none() && q.fetch.is_none() && q.order_by.is_none() && q.with.is_none() && q.locks.is_empty() => { q.body.as_ref() } _ => return None, }; let select_items = match set_expr { sqlparser::ast::SetExpr::Select(s) if s.cluster_by.is_empty() && s.distinct.is_none() && s.distribute_by.is_empty() && s.from.is_empty() && s.group_by == sqlparser::ast::GroupByExpr::Expressions(vec![], vec![]) && s.having.is_none() && s.into.is_none() && s.lateral_views.is_empty() && s.named_window.is_empty() && s.qualify.is_none() && s.selection.is_none() && s.sort_by.is_empty() && s.top.is_none() => { &s.projection } _ => return None, }; let mut items = Vec::with_capacity(select_items.len()); let mut params_iter = params.iter().cloned(); for select_item in select_items { let sqlparser::ast::SelectItem::ExprWithAlias { expr, alias } = select_item else { return None; }; let value = expr_to_simple_select_val(&mut params_iter, expr)?; let key = alias.value.clone(); items.push((key, value)); } if let Some(p) = params_iter.next() { log::error!("static select extraction failed because of extraneous parameter: {p:?}"); return None; } Some(items) } fn expr_to_simple_select_val( params_iter: &mut impl Iterator, expr: &Expr, ) -> Option { use SimpleSelectValue::{Dynamic, Static}; use serde_json::Value::{Bool, Null, Number, String}; Some(match expr { Expr::Value(ValueWithSpan { value: Value::Boolean(b), .. }) => Static(Bool(*b)), Expr::Value(ValueWithSpan { value: Value::Number(n, _), .. }) => Static(Number(n.parse().ok()?)), Expr::Value(ValueWithSpan { value: Value::SingleQuotedString(s), .. }) => Static(String(s.clone())), Expr::Value(ValueWithSpan { value: Value::Null, .. }) => Static(Null), e if is_simple_select_placeholder(e) => { if let Some(p) = params_iter.next() { Dynamic(p) } else { log::error!("Parameter not extracted for placehorder: {expr:?}"); return None; } } other => { log::trace!("Cancelling simple select optimization because of expr: {other:?}"); return None; } }) } fn is_simple_select_placeholder(e: &Expr) -> bool { match e { Expr::Value(ValueWithSpan { value: Value::Placeholder(_), .. }) => true, Expr::Cast { expr, data_type: DataType::Text | DataType::Varchar(_) | DataType::Char(_), format: None, kind: CastKind::Cast, .. } if is_simple_select_placeholder(expr) => true, _ => false, } } fn extract_set_variable( stmt: &mut Statement, params: &mut Vec, db_info: &DbInfo, ) -> Option { if let Statement::Set(Set::SingleAssignment { variable: ObjectName(name), values, scope: None, hivevar: false, }) = stmt && let ([ObjectNamePart::Identifier(ident)], [value]) = (name.as_mut_slice(), values.as_mut_slice()) { let variable = if let Some(variable) = extract_ident_param(ident) { variable } else { StmtParam::PostOrGet(std::mem::take(&mut ident.value)) }; let owned_expr = std::mem::replace(value, Expr::value(Value::Null)); let mut params_iter = params.iter().cloned(); if let Some(value) = expr_to_simple_select_val(&mut params_iter, &owned_expr) { return Some(ParsedStatement::StaticSimpleSet { variable, value }); } let mut select_stmt: Statement = expr_to_statement(owned_expr); let delayed_functions = extract_toplevel_functions(&mut select_stmt); if let Err(err) = validate_function_calls(&select_stmt) { return Some(ParsedStatement::Error(err)); } let json_columns = extract_json_columns(&select_stmt, db_info.database_type); let mut value = StmtWithParams { query: select_stmt.to_string(), query_position: extract_query_start(&select_stmt), params: std::mem::take(params), delayed_functions, json_columns, }; transform_to_positional_placeholders(&mut value, db_info.kind); return Some(ParsedStatement::SetVariable { variable, value }); } None } const SQLPAGE_FUNCTION_NAMESPACE: &str = "sqlpage"; fn is_sqlpage_func(func_name_parts: &[ObjectNamePart]) -> bool { if let [ ObjectNamePart::Identifier(Ident { value, .. }), ObjectNamePart::Identifier(Ident { .. }), ] = func_name_parts { value == SQLPAGE_FUNCTION_NAMESPACE } else { false } } fn extract_sqlpage_function_name( func_name_parts: &[ObjectNamePart], ) -> Option { if let [ ObjectNamePart::Identifier(Ident { value: namespace, .. }), ObjectNamePart::Identifier(Ident { value, .. }), ] = func_name_parts && namespace == SQLPAGE_FUNCTION_NAMESPACE { return SqlPageFunctionName::from_str(value).ok(); } None } fn sqlpage_func_name(func_name_parts: &[ObjectNamePart]) -> &str { if let [ ObjectNamePart::Identifier(Ident { .. }), ObjectNamePart::Identifier(Ident { value, .. }), ] = func_name_parts { value } else { debug_assert!( false, "sqlpage function name should have been checked by is_sqlpage_func" ); "" } } fn extract_json_columns(stmt: &Statement, dbms: SupportedDatabase) -> Vec { // Only extract JSON columns for databases without native JSON support if matches!(dbms, SupportedDatabase::Postgres | SupportedDatabase::Mssql) { return Vec::new(); } let mut json_columns = Vec::new(); if let Statement::Query(query) = stmt && let SetExpr::Select(select) = query.body.as_ref() { for item in &select.projection { if let SelectItem::ExprWithAlias { expr, alias } = item && is_json_function(expr) { json_columns.push(alias.value.clone()); log::trace!("Found JSON column: {alias}"); } } } json_columns } fn is_json_function(expr: &Expr) -> bool { match expr { Expr::Function(function) => { if let [ObjectNamePart::Identifier(Ident { value, .. })] = function.name.0.as_slice() { [ "json_object", "json_array", "json_build_object", "json_build_array", "to_json", "to_jsonb", "json_agg", "jsonb_agg", "json_arrayagg", "json_objectagg", "json_group_array", "json_group_object", "json", "jsonb", ] .iter() .any(|&func| value.eq_ignore_ascii_case(func)) } else { false } } Expr::Cast { data_type, .. } => { if matches!(data_type, DataType::JSON | DataType::JSONB) { true } else if let DataType::Custom(ObjectName(parts), _) = data_type { if let [ObjectNamePart::Identifier(ident)] = parts.as_slice() { ident.value.eq_ignore_ascii_case("json") } else { false } } else { false } } _ => false, } } fn expr_to_statement(expr: Expr) -> Statement { Statement::Query(Box::new(sqlparser::ast::Query { with: None, body: Box::new(sqlparser::ast::SetExpr::Select(Box::new( sqlparser::ast::Select { select_token: AttachedToken(TokenWithSpan::new( Token::make_keyword("SELECT"), expr.span(), )), distinct: None, top: None, projection: vec![SelectItem::ExprWithAlias { expr, alias: Ident::new("sqlpage_set_expr"), }], into: None, from: vec![], lateral_views: vec![], selection: None, group_by: sqlparser::ast::GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], having: None, named_window: vec![], qualify: None, top_before_distinct: false, prewhere: None, window_before_qualify: false, value_table_mode: None, connect_by: Vec::new(), optimizer_hints: vec![], select_modifiers: None, flavor: SelectFlavor::Standard, exclude: None, }, ))), order_by: None, limit_clause: None, fetch: None, locks: vec![], for_clause: None, settings: None, format_clause: None, pipe_operators: Vec::new(), })) } #[cfg(test)] mod test { use super::super::sqlpage_functions::functions::SqlPageFunctionName; use super::super::syntax_tree::SqlPageFunctionCall; use super::*; fn parse_stmt(sql: &str, dialect: &dyn Dialect) -> Statement { let mut ast = Parser::parse_sql(dialect, sql).unwrap(); assert_eq!(ast.len(), 1); ast.pop().unwrap() } fn parse_postgres_stmt(sql: &str) -> Statement { parse_stmt(sql, &PostgreSqlDialect {}) } #[test] fn test_statement_rewrite() { let mut ast = parse_postgres_stmt("select $a from t where $x > $a OR $x = sqlpage.cookie('cookoo')"); let db_info = create_test_db_info(SupportedDatabase::Postgres); let parameters = ParameterExtractor::extract_parameters(&mut ast, db_info).unwrap(); // $a -> $1 // $x -> $2 // sqlpage.cookie(...) -> $3 assert_eq!( ast.to_string(), "SELECT CAST($1 AS TEXT) FROM t WHERE CAST($2 AS TEXT) > CAST($1 AS TEXT) OR CAST($2 AS TEXT) = CAST($3 AS TEXT)" ); assert_eq!( parameters, [ StmtParam::PostOrGet("a".to_string()), StmtParam::PostOrGet("x".to_string()), StmtParam::FunctionCall(SqlPageFunctionCall { function: SqlPageFunctionName::cookie, arguments: vec![StmtParam::Literal("cookoo".to_string())] }), ] ); } #[test] fn test_statement_rewrite_sqlite() { let mut ast = parse_stmt("select $x, :y from t", &SQLiteDialect {}); let db_info = create_test_db_info(SupportedDatabase::Sqlite); let parameters = ParameterExtractor::extract_parameters(&mut ast, db_info).unwrap(); assert_eq!( ast.to_string(), "SELECT CAST(?1 AS TEXT), CAST(?2 AS TEXT) FROM t" ); assert_eq!( parameters, [ StmtParam::PostOrGet("x".to_string()), StmtParam::Post("y".to_string()), ] ); } const ALL_DIALECTS: &[(&dyn Dialect, SupportedDatabase)] = &[ (&PostgreSqlDialect {}, SupportedDatabase::Postgres), (&MsSqlDialect {}, SupportedDatabase::Mssql), (&MySqlDialect {}, SupportedDatabase::MySql), (&SQLiteDialect {}, SupportedDatabase::Sqlite), ]; fn create_test_db_info(database_type: SupportedDatabase) -> DbInfo { let kind = match database_type { SupportedDatabase::Postgres => AnyKind::Postgres, SupportedDatabase::Mssql => AnyKind::Mssql, SupportedDatabase::MySql => AnyKind::MySql, SupportedDatabase::Sqlite => AnyKind::Sqlite, _ => AnyKind::Odbc, }; DbInfo { dbms_name: database_type.display_name().to_string(), database_type, kind, } } #[test] fn test_duckdb_odbc_dialect_selection() { use std::any::Any; let dbms = SupportedDatabase::from_dbms_name("DuckDB"); assert_eq!(dbms, SupportedDatabase::Duckdb); let dialect = dialect_for_db(dbms); assert_eq!(dialect.as_ref().type_id(), (DuckDbDialect {}).type_id()); let sql = "select {'a': 1, 'b': 2} as payload"; let db_info = create_test_db_info(dbms); let mut parsed = parse_sql(&db_info, dialect.as_ref(), sql).unwrap(); let stmt = parsed.next().expect("expected one statement"); assert!( !matches!(stmt, ParsedStatement::Error(_)), "duckdb dictionary literals should parse" ); let pg_info = create_test_db_info(SupportedDatabase::Postgres); let mut parsed = parse_sql(&pg_info, &PostgreSqlDialect {}, sql).unwrap(); let stmt = parsed.next().expect("expected one statement"); assert!( matches!(stmt, ParsedStatement::Error(_)), "postgres should reject duckdb dictionary literals" ); } #[test] fn test_extract_toplevel_delayed_functions() { let mut ast = parse_stmt( "select sqlpage.fetch($x) as x, sqlpage.persist_uploaded_file('a', 'b') as y from t", &PostgreSqlDialect {}, ); let functions = extract_toplevel_functions(&mut ast); assert_eq!( ast.to_string(), "SELECT $x AS \"_sqlpage_f0_a0\", 'a' AS \"_sqlpage_f1_a0\", 'b' AS \"_sqlpage_f1_a1\" FROM t" ); assert_eq!( functions, vec![ DelayedFunctionCall { function: SqlPageFunctionName::fetch, argument_col_names: vec!["_sqlpage_f0_a0".to_string()], target_col_name: "x".to_string() }, DelayedFunctionCall { function: SqlPageFunctionName::persist_uploaded_file, argument_col_names: vec![ "_sqlpage_f1_a0".to_string(), "_sqlpage_f1_a1".to_string() ], target_col_name: "y".to_string() } ] ); } #[test] fn test_extract_toplevel_delayed_functions_parameter_order() { // The order of the function arguments should be preserved // Otherwise the statement parameters will be bound to the wrong arguments let sql = "select $a as a, sqlpage.exec('xxx', x = $b) as b, $c as c from t"; let mut ast = parse_postgres_stmt(sql); let delayed_functions = extract_toplevel_functions(&mut ast); assert_eq!( ast.to_string(), "SELECT $a AS a, 'xxx' AS \"_sqlpage_f0_a0\", x = $b AS \"_sqlpage_f0_a1\", $c AS c FROM t" ); assert_eq!( delayed_functions, &[DelayedFunctionCall { function: SqlPageFunctionName::exec, argument_col_names: vec![ "_sqlpage_f0_a0".to_string(), "_sqlpage_f0_a1".to_string() ], target_col_name: "b".to_string() }] ); } #[test] fn test_sqlpage_function_with_argument() { for &(dialect, _kind) in ALL_DIALECTS { let sql = "select sqlpage.fetch($x)"; let mut ast = parse_stmt(sql, dialect); let db_info = create_test_db_info(SupportedDatabase::Postgres); let parameters = ParameterExtractor::extract_parameters(&mut ast, db_info).unwrap(); assert_eq!( parameters, [StmtParam::FunctionCall(SqlPageFunctionCall { function: SqlPageFunctionName::fetch, arguments: vec![StmtParam::PostOrGet("x".to_string())] })], "Failed for dialect {dialect:?}" ); } } #[test] fn test_parse_sql_unsupported_expr_in_sqlpage_arg() { let sql = "SELECT sqlpage.link('x', json_build_object('k', c)) FROM (SELECT 1 AS c) t"; let db_info = create_test_db_info(SupportedDatabase::Postgres); let mut parsed = parse_sql(&db_info, &PostgreSqlDialect {}, sql).unwrap(); let stmt = parsed.next().expect("one statement"); let ParsedStatement::Error(err) = stmt else { panic!("expected ParsedStatement::Error: {stmt:?}"); }; let err_msg = format!("{err:#}"); assert!( err_msg.contains("Unsupported sqlpage function argument:"), "{err_msg}" ); assert!(err_msg.contains("\"c\" is an sql expression, which cannot be passed as a nested sqlpage function argument."), "{err_msg}"); } #[test] fn test_parse_sql_unemulated_function_in_sqlpage_arg() { let sql = "SELECT sqlpage.link('x', upper('a')) FROM (SELECT 1) t"; let db_info = create_test_db_info(SupportedDatabase::Postgres); let mut parsed = parse_sql(&db_info, &PostgreSqlDialect {}, sql).unwrap(); let stmt = parsed.next().expect("one statement"); let ParsedStatement::Error(err) = stmt else { panic!("expected ParsedStatement::Error: {stmt:?}"); }; let err_msg = format!("{err:#}"); assert!( err_msg.contains("Unsupported sqlpage function argument:"), "{err_msg}" ); assert!( err_msg.contains("\"upper\" is not a supported sqlpage function"), "{err_msg}" ); } #[test] fn test_set_variable_to_other_variable() { let sql = "set x = $y"; for &(dialect, dbms) in ALL_DIALECTS { let mut parser = Parser::new(dialect).try_with_sql(sql).unwrap(); let db_info = create_test_db_info(dbms); match parse_single_statement(&mut parser, &db_info, sql) { Some(ParsedStatement::StaticSimpleSet { variable, value }) => { assert_eq!( variable, StmtParam::PostOrGet("x".to_string()), "{dialect:?}" ); assert_eq!( value, SimpleSelectValue::Dynamic(StmtParam::PostOrGet("y".to_string())) ); } other => panic!("Failed for dialect {dialect:?}: {other:#?}"), } } } #[test] fn is_own_placeholder() { assert!( ParameterExtractor { db_info: create_test_db_info(SupportedDatabase::Postgres), parameters: vec![], extract_error: None, } .is_own_placeholder("$1") ); assert!( ParameterExtractor { db_info: create_test_db_info(SupportedDatabase::Postgres), parameters: vec![StmtParam::Get("x".to_string())], extract_error: None, } .is_own_placeholder("$2") ); assert!( !ParameterExtractor { db_info: create_test_db_info(SupportedDatabase::Postgres), parameters: vec![], extract_error: None, } .is_own_placeholder("$2") ); assert!( ParameterExtractor { db_info: create_test_db_info(SupportedDatabase::Sqlite), parameters: vec![], extract_error: None, } .is_own_placeholder("?1") ); assert!( !ParameterExtractor { db_info: create_test_db_info(SupportedDatabase::Sqlite), parameters: vec![], extract_error: None, } .is_own_placeholder("$1") ); } #[test] fn test_mssql_statement_rewrite() { let mut ast = parse_stmt( "select '' || $1 from [a schema].[a table]", &MsSqlDialect {}, ); let db_info = create_test_db_info(SupportedDatabase::Mssql); let parameters = ParameterExtractor::extract_parameters(&mut ast, db_info).unwrap(); assert_eq!( ast.to_string(), "SELECT CONCAT('', CAST(@p1 AS VARCHAR(MAX))) FROM [a schema].[a table]" ); assert_eq!(parameters, [StmtParam::PostOrGet("1".to_string()),]); } #[test] fn test_static_extract() { use SimpleSelectValue::Static; assert_eq!( extract_static_simple_select( &parse_postgres_stmt( "select 'hello' as hello, 42 as answer, null as nothing, 'world' as hello" ), &[] ), Some(vec![ ("hello".into(), Static("hello".into())), ("answer".into(), Static(42.into())), ("nothing".into(), Static(().into())), ("hello".into(), Static("world".into())), ]) ); } #[test] fn test_simple_select_with_sqlpage_pseudofunction() { let sql = "select 'text' as component, $x as contents, $y as title"; let dialects: &[&dyn Dialect] = &[ &PostgreSqlDialect {}, &SQLiteDialect {}, &MySqlDialect {}, &MsSqlDialect {}, ]; for &dialect in dialects { use SimpleSelectValue::{Dynamic, Static}; use StmtParam::PostOrGet; use std::any::Any; let db_info = if dialect.type_id() == (PostgreSqlDialect {}).type_id() { create_test_db_info(SupportedDatabase::Postgres) } else if dialect.type_id() == (SQLiteDialect {}).type_id() { create_test_db_info(SupportedDatabase::Sqlite) } else if dialect.type_id() == (MySqlDialect {}).type_id() { create_test_db_info(SupportedDatabase::MySql) } else if dialect.type_id() == (MsSqlDialect {}).type_id() { create_test_db_info(SupportedDatabase::Mssql) } else { create_test_db_info(SupportedDatabase::Generic) }; let parsed: Vec = parse_sql(&db_info, dialect, sql).unwrap().collect(); match &parsed[..] { [ParsedStatement::StaticSimpleSelect { values: q, .. }] => assert_eq!( q, &[ ("component".into(), Static("text".into())), ("contents".into(), Dynamic(PostOrGet("x".into()))), ("title".into(), Dynamic(PostOrGet("y".into()))), ] ), other => panic!("failed to extract simple select in {dialect:?}: {other:?}"), } } } #[test] fn test_simple_select_only_extraction() { use SimpleSelectValue::{Dynamic, Static}; use StmtParam::PostOrGet; assert_eq!( extract_static_simple_select( &parse_postgres_stmt("select 'text' as component, $1 as contents"), &[PostOrGet("cook".into())] ), Some(vec![ ("component".into(), Static("text".into())), ("contents".into(), Dynamic(PostOrGet("cook".into()))), ]) ); } #[test] fn test_extract_set_variable() { let sql = "set x = CURRENT_TIMESTAMP"; for &(dialect, dbms) in ALL_DIALECTS { let mut parser = Parser::new(dialect).try_with_sql(sql).unwrap(); let db_info = create_test_db_info(dbms); let stmt = parse_single_statement(&mut parser, &db_info, sql); if let Some(ParsedStatement::SetVariable { variable, value: StmtWithParams { query, params, .. }, }) = stmt { assert_eq!( variable, StmtParam::PostOrGet("x".to_string()), "{dialect:?}" ); assert_eq!(query, "SELECT CURRENT_TIMESTAMP AS sqlpage_set_expr"); assert!(params.is_empty()); } else { panic!("Failed for dialect {dialect:?}: {stmt:#?}"); } } } #[test] fn test_extract_set_variable_static() { let sql = "set x = 'hello'"; for &(dialect, dbms) in ALL_DIALECTS { let mut parser = Parser::new(dialect).try_with_sql(sql).unwrap(); let db_info = create_test_db_info(dbms); match parse_single_statement(&mut parser, &db_info, sql) { Some(ParsedStatement::StaticSimpleSet { variable: StmtParam::PostOrGet(var_name), value: SimpleSelectValue::Static(value), }) => { assert_eq!(var_name, "x"); assert_eq!(value, "hello"); } other => panic!("Failed for dialect {dialect:?}: {other:#?}"), } } } #[test] fn test_static_extract_doesnt_match() { assert_eq!( extract_static_simple_select( &parse_postgres_stmt("select 'hello' as hello, 42 as answer limit 0"), &[] ), None ); assert_eq!( extract_static_simple_select( &parse_postgres_stmt("select 'hello' as hello, 42 as answer order by 1"), &[] ), None ); assert_eq!( extract_static_simple_select( &parse_postgres_stmt("select 'hello' as hello, 42 as answer offset 1"), &[] ), None ); assert_eq!( extract_static_simple_select( &parse_postgres_stmt("select 'hello' as hello, 42 as answer where 1 = 0"), &[] ), None ); assert_eq!( extract_static_simple_select( &parse_postgres_stmt("select 'hello' as hello, 42 as answer FROM t"), &[] ), None ); assert_eq!( extract_static_simple_select( &parse_postgres_stmt("select x'CAFEBABE' as hello, 42 as answer"), &[] ), None ); } #[test] fn test_extract_json_columns() { let sql = r" WITH json_cte AS ( SELECT json_build_object('a', x, 'b', y) AS cte_json FROM generate_series(1, 3) x JOIN generate_series(4, 6) y ON true ) SELECT json_object('key', 'value') AS json_col1, json_array(1, 2, 3) AS json_col2, (SELECT json_build_object('nested', subq.val) FROM (SELECT AVG(x) AS val FROM generate_series(1, 5) x) subq ) AS json_col3, -- not supported because of the subquery CASE WHEN EXISTS (SELECT 1 FROM json_cte WHERE cte_json->>'a' = '2') THEN to_json(ARRAY(SELECT cte_json FROM json_cte)) ELSE json_build_array() END AS json_col4, -- not supported because of the CASE json_unknown_fn(regular_column) AS non_json_col, CAST(json_col1 AS json) AS json_col6 FROM some_table CROSS JOIN json_cte WHERE json_typeof(json_col1) = 'object' "; let stmt = parse_postgres_stmt(sql); let json_columns = extract_json_columns(&stmt, SupportedDatabase::Sqlite); assert_eq!( json_columns, vec![ "json_col1".to_string(), "json_col2".to_string(), "json_col6".to_string() ] ); } #[test] fn test_set_variable_with_sqlpage_function() { let sql = "set x = sqlpage.url_encode(some_db_function())"; for &(dialect, dbms) in ALL_DIALECTS { let mut parser = Parser::new(dialect).try_with_sql(sql).unwrap(); let db_info = create_test_db_info(dbms); let stmt = parse_single_statement(&mut parser, &db_info, sql); let Some(ParsedStatement::SetVariable { variable, value: StmtWithParams { query, params, delayed_functions, json_columns, .. }, }) = stmt else { panic!("for dialect {dialect:?}: {stmt:#?} instead of SetVariable"); }; assert_eq!( variable, StmtParam::PostOrGet("x".to_string()), "{dialect:?}" ); assert_eq!( delayed_functions, [DelayedFunctionCall { function: SqlPageFunctionName::url_encode, argument_col_names: vec!["_sqlpage_f0_a0".to_string()], target_col_name: "sqlpage_set_expr".to_string() }] ); assert_eq!(query, "SELECT some_db_function() AS \"_sqlpage_f0_a0\""); assert_eq!(params, []); assert_eq!(json_columns, Vec::::new()); } } #[test] fn test_extract_json_columns_from_literal() { let sql = r#" SELECT 'Pro Plan' as title, JSON('{"icon":"database","color":"blue","description":"1GB Database"}') as item, JSON('{"icon":"headset","color":"green","description":"Priority Support"}') as item "#; let stmt = parse_stmt(sql, &SQLiteDialect {}); let json_columns = extract_json_columns(&stmt, SupportedDatabase::Sqlite); assert!(json_columns.contains(&"item".to_string())); assert!(!json_columns.contains(&"title".to_string())); } #[test] fn test_positional_placeholders() { let sql = "select \ @SQLPAGE_TEMP10 as a1, \ @SQLPAGE_TEMP9 as a2, \ @SQLPAGE_TEMP8 as a3, \ @SQLPAGE_TEMP7 as a4, \ @SQLPAGE_TEMP6 as a5, \ @SQLPAGE_TEMP5 as a6, \ @SQLPAGE_TEMP4 as a7, \ @SQLPAGE_TEMP3 as a8, \ @SQLPAGE_TEMP2 as a9, \ @SQLPAGE_TEMP1 as a10 \ @SQLPAGE_TEMP10 as a1bis \ from t"; let mut stmt = StmtWithParams { query: sql.to_string(), query_position: SourceSpan { start: SourceLocation { line: 1, column: 1 }, end: SourceLocation { line: 1, column: 1 }, }, params: vec![ StmtParam::PostOrGet("x1".to_string()), StmtParam::PostOrGet("x2".to_string()), StmtParam::PostOrGet("x3".to_string()), StmtParam::PostOrGet("x4".to_string()), StmtParam::PostOrGet("x5".to_string()), StmtParam::PostOrGet("x6".to_string()), StmtParam::PostOrGet("x7".to_string()), StmtParam::PostOrGet("x8".to_string()), StmtParam::PostOrGet("x9".to_string()), StmtParam::PostOrGet("x10".to_string()), ], delayed_functions: vec![], json_columns: vec![], }; transform_to_positional_placeholders(&mut stmt, AnyKind::MySql); assert_eq!( stmt.query, "select \ ? as a1, \ ? as a2, \ ? as a3, \ ? as a4, \ ? as a5, \ ? as a6, \ ? as a7, \ ? as a8, \ ? as a9, \ ? as a10 \ ? as a1bis \ from t" ); assert_eq!( stmt.params, vec![ StmtParam::PostOrGet("x10".to_string()), StmtParam::PostOrGet("x9".to_string()), StmtParam::PostOrGet("x8".to_string()), StmtParam::PostOrGet("x7".to_string()), StmtParam::PostOrGet("x6".to_string()), StmtParam::PostOrGet("x5".to_string()), StmtParam::PostOrGet("x4".to_string()), StmtParam::PostOrGet("x3".to_string()), StmtParam::PostOrGet("x2".to_string()), StmtParam::PostOrGet("x1".to_string()), StmtParam::PostOrGet("x10".to_string()), ] ); } #[test] fn test_set_variable_error_handling() { let sql = "set x = db_function(sqlpage.fetch(other_db_function()))"; for &(dialect, dbms) in ALL_DIALECTS { let mut parser = Parser::new(dialect).try_with_sql(sql).unwrap(); let db_info = create_test_db_info(dbms); let stmt = parse_single_statement(&mut parser, &db_info, sql); if let Some(ParsedStatement::Error(err)) = stmt { assert!( err.to_string() .contains("Unsupported sqlpage function argument:"), "Expected error for invalid function, got: {err}" ); } else { panic!("Expected error for invalid function, got: {stmt:#?}"); } } } }