From 244d9b2b1bd77b51e742e1e5a0847f4a43b30800 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:14:52 +0200 Subject: [PATCH 1/6] `PostgreSQL`: Support the `VARIADIC` function call argument marker --- src/ast/mod.rs | 9 ++ src/ast/spans.rs | 1 + src/dialect/generic.rs | 4 + src/dialect/mod.rs | 10 ++ src/dialect/postgresql.rs | 14 ++- src/parser/mod.rs | 106 +++++++++++++++++- tests/sqlparser_postgres.rs | 212 ++++++++++++++++++++++++++++++++++++ 7 files changed, 351 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e798524c9..16fae831f3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8138,6 +8138,14 @@ pub enum FunctionArg { }, /// An unnamed argument (positional), given by expression or wildcard. Unnamed(FunctionArgExpr), + /// An argument carrying PostgreSQL's `VARIADIC` marker, e.g. + /// `concat(VARIADIC ARRAY['a', 'b'])`. Only the final argument of a call + /// may carry it. + /// + /// Enabled when `Dialect::supports_variadic_function_argument` returns `true`. + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-VARIADIC-FUNCTIONS) + Variadic(Box), } impl fmt::Display for FunctionArg { @@ -8154,6 +8162,7 @@ impl fmt::Display for FunctionArg { operator, } => fmt_named_function_arg(f, name, operator, arg), FunctionArg::Unnamed(unnamed_arg) => write!(f, "{unnamed_arg}"), + FunctionArg::Variadic(arg) => write!(f, "VARIADIC {arg}"), } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7638d7d6c0..90a467d113 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2215,6 +2215,7 @@ impl Spanned for FunctionArg { arg, operator: _, } => name.span().union(&arg.span()), + FunctionArg::Variadic(arg) => arg.span(), } } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 6aae6acaea..dd551857f5 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -357,6 +357,10 @@ impl Dialect for GenericDialect { true } + fn supports_variadic_function_argument(&self) -> bool { + true + } + fn supports_alter_column_position(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c867b76f61..291c57810b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2127,6 +2127,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports the `VARIADIC` marker on the final + /// argument of a function call, e.g. `concat(VARIADIC ARRAY['a', 'b'])`. + /// + /// When this returns false, `VARIADIC` is treated as a regular identifier. + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-VARIADIC-FUNCTIONS) + fn supports_variadic_function_argument(&self) -> bool { + false + } + /// Returns true if the dialect supports `USING ` in `CREATE TABLE`. /// /// Example: diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9e5257b30f..3e2b9b43bb 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -257,10 +257,12 @@ impl Dialect for PostgreSqlDialect { } fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { - if matches!(kw, Keyword::INTERVAL) { - false - } else { - RESERVED_FOR_IDENTIFIER.contains(&kw) + match kw { + Keyword::INTERVAL => false, + // PostgreSQL reserves `VARIADIC`, so inside a call it always + // introduces the argument marker rather than a column of that name. + Keyword::VARIADIC => true, + _ => RESERVED_FOR_IDENTIFIER.contains(&kw), } } @@ -553,6 +555,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_variadic_function_argument(&self) -> bool { + true + } + /// Postgres supports query optimizer hints via the `pg_hint_plan` extension, /// using the same comment-prefixed-with-`+` syntax as MySQL and Oracle. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..f24b12d67d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19494,8 +19494,100 @@ impl<'a> Parser<'a> { } } - /// Parse a single function argument, handling named and unnamed variants. + /// Parse a single function argument, handling the `VARIADIC` marker as well + /// as named and unnamed variants. + /// + /// Dialects that reserve `VARIADIC`, such as PostgreSQL, always mean the + /// marker. Where the keyword is unreserved, `f(variadic)` passes a column + /// of that name instead, so the ordinary reading is parsed first and kept + /// whenever it stands alone as an argument, and `f(VARIADIC a)` is left as + /// the only reading that takes the marker. pub fn parse_function_args(&mut self) -> Result { + if self.dialect.supports_variadic_function_argument() + && self.peek_keyword(Keyword::VARIADIC) + { + if !self.dialect.is_reserved_for_identifier(Keyword::VARIADIC) { + let plain = self.maybe_parse(|p| { + let arg = p.parse_plain_function_args()?; + if p.peek_ends_function_arg() { + Ok(arg) + } else { + p.expected_ref("end of the function argument", p.peek_token_ref()) + } + })?; + if let Some(arg) = plain { + return Ok(arg); + } + } + + let loc = self.peek_token_ref().span.start; + self.expect_keyword(Keyword::VARIADIC)?; + let arg = self.parse_plain_function_args()?; + // PostgreSQL's VARIADIC productions require expression operands, + // so no wildcard argument form is valid. + let carries_wildcard = match &arg { + FunctionArg::Unnamed(arg) + | FunctionArg::Named { arg, .. } + | FunctionArg::ExprNamed { arg, .. } => !matches!(arg, FunctionArgExpr::Expr(_)), + FunctionArg::Variadic(_) => false, + }; + if carries_wildcard { + return parser_err!("A VARIADIC argument cannot be a wildcard", loc); + } + return Ok(FunctionArg::Variadic(Box::new(arg))); + } + self.parse_plain_function_args() + } + + /// Returns true when the next token can follow a complete function + /// argument, for example the argument separator, the end of the list, or a + /// clause that [`Parser::parse_function_argument_list`] accepts after the + /// arguments. A clause keyword whose own syntax does not follow is an + /// operand instead, as in `f(VARIADIC separator)`. + fn peek_ends_function_arg(&self) -> bool { + let word = match &self.peek_token_ref().token { + Token::Comma | Token::RParen => return true, + Token::Word(word) => word, + _ => return false, + }; + let next = &self.peek_nth_token_ref(1).token; + let next_is = |keyword| matches!(next, Token::Word(next) if next.keyword == keyword); + match word.keyword { + Keyword::ORDER => next_is(Keyword::BY), + Keyword::IGNORE | Keyword::RESPECT => next_is(Keyword::NULLS), + // `ON OVERFLOW`, and the `ABSENT ON NULL` / `NULL ON NULL` clauses. + Keyword::ON => next_is(Keyword::OVERFLOW), + Keyword::ABSENT | Keyword::NULL => next_is(Keyword::ON), + Keyword::HAVING => next_is(Keyword::MIN) || next_is(Keyword::MAX), + // These take an expression, a value or a type of their own. + Keyword::LIMIT | Keyword::RETURNING | Keyword::SEPARATOR | Keyword::WHERE => { + !matches!(next, Token::Comma | Token::RParen) + } + _ => false, + } + } + + /// Rejects a `VARIADIC` marker on any but the final argument, as + /// PostgreSQL's grammar carries it on the last one only. + fn verify_variadic_argument_is_last(args: &[FunctionArg]) -> Result<(), ParserError> { + let Some((_, preceding)) = args.split_last() else { + return Ok(()); + }; + match preceding + .iter() + .find(|arg| matches!(arg, FunctionArg::Variadic(_))) + { + Some(arg) => parser_err!( + "A VARIADIC argument must be the last function argument", + arg.span().start + ), + None => Ok(()), + } + } + + /// Parse a single function argument carrying no `VARIADIC` marker, + /// handling named and unnamed variants. + fn parse_plain_function_args(&mut self) -> Result { // Parse the argument expression once, then check for a named-arg // operator. Parsing it speculatively and re-parsing on the unnamed // path is O(2^depth) on nested calls like `CAST(CASE (CAST(CASE (…`. @@ -19620,6 +19712,7 @@ impl<'a> Parser<'a> { Ok(vec![]) } else { let args = self.parse_comma_separated(Parser::parse_function_args)?; + Self::verify_variadic_argument_is_last(&args)?; self.expect_token(&Token::RParen)?; Ok(args) } @@ -19642,6 +19735,7 @@ impl<'a> Parser<'a> { break None; } }; + Self::verify_variadic_argument_is_last(&args)?; self.expect_token(&Token::RParen)?; Ok(TableFunctionArgs { args, settings }) } @@ -19677,6 +19771,7 @@ impl<'a> Parser<'a> { }); } + let loc = self.peek_token_ref().span.start; let duplicate_treatment = self.parse_duplicate_treatment()?; let args = if duplicate_treatment != Some(DuplicateTreatment::Distinct) && self.dialect.supports_aggregate_order_by_with_empty_args() @@ -19686,6 +19781,15 @@ impl<'a> Parser<'a> { } else { self.parse_comma_separated(Parser::parse_function_args)? }; + Self::verify_variadic_argument_is_last(&args)?; + + // PostgreSQL pairs the marker with neither `DISTINCT` nor `ALL`. + if duplicate_treatment.is_some() && matches!(args.last(), Some(FunctionArg::Variadic(_))) { + return parser_err!( + "A VARIADIC argument cannot be combined with DISTINCT or ALL", + loc + ); + } if self.parse_keyword(Keyword::WHERE) { clauses.push(FunctionArgumentClause::Where(self.parse_expr()?)); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index aa9252125b..594c5dba4c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -27,6 +27,7 @@ use sqlparser::ast::*; use sqlparser::dialect::{ Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, }; +use sqlparser::keywords::Keyword; use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::{Location, Span, Token, Tokenizer}; use test_utils::*; @@ -10875,3 +10876,214 @@ fn parse_create_aggregate_rejects_output_arguments_and_defaults() { ); } } + +#[test] +fn parse_variadic_function_argument() { + pg().verified_stmt("SELECT concat(VARIADIC ARRAY[1, 2])"); + pg().verified_stmt("SELECT concat_ws('-', VARIADIC ARRAY['a'])"); + pg().verified_stmt( + "SELECT * FROM pg_get_publication_tables(VARIADIC ARRAY['pub'::TEXT]) AS gpt", + ); + + // PostgreSQL accepts the marker on a named final argument too. + let select = pg().verified_only_select("SELECT f(VARIADIC a => ARRAY[1])"); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!( + &vec![FunctionArg::Variadic(Box::new(FunctionArg::ExprNamed { + name: Expr::Identifier(Ident::new("a")), + arg: FunctionArgExpr::Expr(Expr::Array(Array { + elem: vec![Expr::value(number("1"))], + named: true, + })), + operator: FunctionArgOperator::RightArrow, + }))], + args + ), + other => panic!("Expected a function call, found {other:?}"), + } + + // PostgreSQL reserves the keyword, so the marker holds even where an + // unreserved dialect would read `variadic - a` as an expression. + let select = pg().verified_only_select("SELECT concat(VARIADIC -a)"); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!( + &vec![FunctionArg::Variadic(Box::new(FunctionArg::Unnamed( + FunctionArgExpr::Expr(Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Identifier(Ident::new("a"))), + }) + )))], + args + ), + other => panic!("Expected a function call, found {other:?}"), + } + + // Since the keyword is reserved, it never passes a column of that name, + // matching PostgreSQL's own rejection of `SELECT f(variadic)`. + assert_eq!( + ParserError::ParserError("Expected: an expression, found: )".to_string()), + pg().parse_sql_statements("SELECT f(variadic)").unwrap_err() + ); + + // The marker only applies to the final argument. + let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument must be the last function argument".to_string() + ), + dialects + .parse_sql_statements("SELECT concat(VARIADIC a, b)") + .unwrap_err() + ); + + // A comma introducing the table-function `SETTINGS` clause is no argument. + all_dialects_where(|d| d.supports_variadic_function_argument() && d.supports_settings()) + .verified_stmt("SELECT * FROM f(VARIADIC a, SETTINGS x = 1)"); + + // A bare clause keyword is an operand, not the start of that clause. + dialects.verified_stmt("SELECT f(VARIADIC separator)"); + dialects.verified_stmt("SELECT f(VARIADIC returning)"); + + // PostgreSQL's VARIADIC productions require expression operands, so no + // wildcard argument form is valid. + for sql in ["SELECT f(VARIADIC *)", "SELECT f(VARIADIC a => *)"] { + assert_eq!( + ParserError::ParserError("A VARIADIC argument cannot be a wildcard".to_string()), + dialects.parse_sql_statements(sql).unwrap_err(), + "{sql}" + ); + } + dialects.verified_stmt("SELECT f(*)"); + + // Dialects without the marker read `VARIADIC` as an identifier, so the + // argument that follows it has nowhere to go. + let unsupported = all_dialects_except(|d| d.supports_variadic_function_argument()); + assert_eq!( + ParserError::ParserError("Expected: ), found: a".to_string()), + unsupported + .parse_sql_statements("SELECT concat(VARIADIC a)") + .unwrap_err() + ); + + // PostgreSQL has no production pairing the marker with a duplicate + // treatment, so `count(DISTINCT VARIADIC ARRAY[1])` is a syntax error there. + for sql in [ + "SELECT count(DISTINCT VARIADIC ARRAY[1])", + "SELECT count(ALL VARIADIC ARRAY[1])", + ] { + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument cannot be combined with DISTINCT or ALL".to_string() + ), + dialects.parse_sql_statements(sql).unwrap_err(), + "{sql}" + ); + } + + // The rejection points at the duplicate treatment it names, even when + // earlier arguments precede the marker. + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument cannot be combined with DISTINCT or ALL at Line: 1, Column: 14" + .to_string() + ), + Parser::parse_sql( + &PostgreSqlDialect {}, + "SELECT count(DISTINCT a, VARIADIC b)" + ) + .unwrap_err() + ); + + // A column of that name still may, where the keyword is unreserved. + all_dialects_where(|d| { + d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) + }) + .verified_stmt("SELECT count(DISTINCT variadic)"); +} + +/// Dialects that accept the marker without reserving `VARIADIC`, such as +/// `GenericDialect`, must still read `variadic` as an ordinary identifier +/// wherever it stands as an argument on its own. +#[test] +fn parse_variadic_as_identifier_function_argument() { + let dialects = all_dialects_where(|d| { + d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) + }); + for sql in [ + "SELECT f(variadic)", + "SELECT f(variadic, x)", + "SELECT f(x, variadic)", + "SELECT f(variadic + 1)", + "SELECT f(variadic.x)", + "SELECT f(variadic[1])", + "SELECT f(variadic(1))", + "SELECT f(variadic AS x)", + "SELECT f(variadic WHERE x)", + "SELECT f(variadic LIMIT 1)", + "SELECT array_agg(variadic ORDER BY x)", + "SELECT count(variadic) FROM t WHERE variadic IS NULL", + ] { + dialects.verified_stmt(sql); + } + + // Display alone cannot tell `Unnamed(variadic + 1)` from + // `Variadic(Unnamed(+1))` apart from spelling, so assert the shape. + for (sql, expected) in [ + ( + "SELECT f(variadic)", + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new( + "variadic", + )))), + ), + ( + "SELECT f(variadic + 1)", + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("variadic"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::value(number("1"))), + })), + ), + ] { + let select = dialects.verified_only_select(sql); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!(&vec![expected], args), + other => panic!("Expected a function call, found {other:?}"), + } + } + + // The marker still applies when the ordinary reading cannot stand alone. + dialects.verified_stmt("SELECT f(VARIADIC a)"); +} + +#[test] +fn parse_pg_publication_tables_view_definition() { + // `pg_get_viewdef('pg_publication_tables'::regclass, true)` on PostgreSQL 18. + let sql = r#" +SELECT p.pubname, + n.nspname AS schemaname, + c.relname AS tablename, + ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg + FROM pg_attribute a + WHERE a.attrelid = gpt.relid AND (a.attnum = ANY (gpt.attrs::smallint[]))) AS attnames, + pg_get_expr(gpt.qual, gpt.relid) AS rowfilter + FROM pg_publication p, + LATERAL pg_get_publication_tables(VARIADIC ARRAY[p.pubname::text]) gpt(pubid, relid, attrs, qual), + pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.oid = gpt.relid +"#; + let mut statements = pg().parse_sql_statements(sql).unwrap(); + assert_eq!(1, statements.len()); + let statement = statements.pop().unwrap(); + let reparsed = pg().parse_sql_statements(&statement.to_string()).unwrap(); + assert_eq!(vec![statement], reparsed); +} From 7542c9edd7fc57dbd6c632b7d4903e3963eaa9cc Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 12:51:21 +0200 Subject: [PATCH 2/6] Cover variadic argument boundaries and remove redundant dispatch --- src/parser/mod.rs | 11 +++--- tests/sqlparser_postgres.rs | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f24b12d67d..f2fc9d1175 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19521,16 +19521,17 @@ impl<'a> Parser<'a> { } let loc = self.peek_token_ref().span.start; - self.expect_keyword(Keyword::VARIADIC)?; + self.advance_token(); let arg = self.parse_plain_function_args()?; // PostgreSQL's VARIADIC productions require expression operands, // so no wildcard argument form is valid. - let carries_wildcard = match &arg { + let carries_wildcard = matches!( + &arg, FunctionArg::Unnamed(arg) | FunctionArg::Named { arg, .. } - | FunctionArg::ExprNamed { arg, .. } => !matches!(arg, FunctionArgExpr::Expr(_)), - FunctionArg::Variadic(_) => false, - }; + | FunctionArg::ExprNamed { arg, .. } + if !matches!(arg, FunctionArgExpr::Expr(_)) + ); if carries_wildcard { return parser_err!("A VARIADIC argument cannot be a wildcard", loc); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 594c5dba4c..365d50e1ff 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11064,6 +11064,83 @@ fn parse_variadic_as_identifier_function_argument() { dialects.verified_stmt("SELECT f(VARIADIC a)"); } +/// Where the ordinary reading of the argument stands alone and a clause +/// follows it, the plain reading is kept and the clause is parsed as usual. +#[test] +fn parse_variadic_plain_reading_before_argument_clauses() { + let dialects = all_dialects_where(|d| { + d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) + }); + // A token that cannot extend the expression leaves the marker reading, + // while clause keywords keep the plain reading. + for sql in [ + "SELECT f(VARIADIC 5)", + "SELECT f(variadic IGNORE NULLS)", + "SELECT f(variadic RESPECT NULLS)", + "SELECT f(variadic ON OVERFLOW ERROR)", + "SELECT f(variadic NULL ON NULL)", + "SELECT f(variadic ABSENT ON NULL)", + "SELECT f(variadic HAVING MIN 1)", + "SELECT f(variadic HAVING MAX 1)", + ] { + dialects.verified_stmt(sql); + } + + // The plain argument keeps its identifier shape next to the clause. + let select = dialects.verified_only_select("SELECT f(variadic IGNORE NULLS)"); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("variadic")) + ))], + args + ); + assert_eq!( + &vec![FunctionArgumentClause::IgnoreOrRespectNulls( + NullTreatment::IgnoreNulls + )], + clauses + ); + } + other => panic!("Expected a function call, found {other:?}"), + } +} + +/// The marker is rejected on non-final arguments in table function argument +/// lists and `LATERAL` function calls as well. +#[test] +fn parse_variadic_not_last_in_table_function_argument_lists() { + let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); + for sql in [ + "SELECT * FROM f(VARIADIC a, b)", + "SELECT * FROM LATERAL f(VARIADIC a, b)", + ] { + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument must be the last function argument".to_string() + ), + dialects.parse_sql_statements(sql).unwrap_err(), + "{sql}" + ); + } +} + +/// A recursion limit exhausted inside the speculative plain reading is +/// propagated rather than treated as a failed plain reading. +#[test] +fn parse_variadic_recursion_limit_in_plain_reading() { + let result = Parser::new(&GenericDialect {}) + .with_recursion_limit(0) + .try_with_sql("VARIADIC 5") + .unwrap() + .parse_function_args(); + assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +} + #[test] fn parse_pg_publication_tables_view_definition() { // `pg_get_viewdef('pg_publication_tables'::regclass, true)` on PostgreSQL 18. From 9624222e257c2ce9902594151aaaffbd9d8e5603 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 17:55:59 +0200 Subject: [PATCH 3/6] Locate invalid `VARIADIC` at the keyword --- src/parser/mod.rs | 60 ++++++++++++++++++++++++++----------- tests/sqlparser_postgres.rs | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f2fc9d1175..603d1c19ae 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19540,6 +19540,18 @@ impl<'a> Parser<'a> { self.parse_plain_function_args() } + fn parse_function_arg_in_list( + &mut self, + first_variadic_location: &mut Option, + ) -> Result { + let loc = self.peek_token_ref().span.start; + let arg = self.parse_function_args()?; + if matches!(arg, FunctionArg::Variadic(_)) && first_variadic_location.is_none() { + *first_variadic_location = Some(loc); + } + Ok(arg) + } + /// Returns true when the next token can follow a complete function /// argument, for example the argument separator, the end of the list, or a /// clause that [`Parser::parse_function_argument_list`] accepts after the @@ -19568,21 +19580,26 @@ impl<'a> Parser<'a> { } } - /// Rejects a `VARIADIC` marker on any but the final argument, as - /// PostgreSQL's grammar carries it on the last one only. - fn verify_variadic_argument_is_last(args: &[FunctionArg]) -> Result<(), ParserError> { + /// Rejects non-final `VARIADIC` arguments at the marker's source location. + fn verify_variadic_argument_is_last( + args: &[FunctionArg], + first_variadic_location: Option, + ) -> Result<(), ParserError> { let Some((_, preceding)) = args.split_last() else { return Ok(()); }; - match preceding - .iter() - .find(|arg| matches!(arg, FunctionArg::Variadic(_))) - { - Some(arg) => parser_err!( - "A VARIADIC argument must be the last function argument", - arg.span().start - ), - None => Ok(()), + match first_variadic_location { + Some(loc) + if preceding + .iter() + .any(|arg| matches!(arg, FunctionArg::Variadic(_))) => + { + parser_err!( + "A VARIADIC argument must be the last function argument", + loc + ) + } + _ => Ok(()), } } @@ -19712,8 +19729,11 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::RParen) { Ok(vec![]) } else { - let args = self.parse_comma_separated(Parser::parse_function_args)?; - Self::verify_variadic_argument_is_last(&args)?; + let mut first_variadic_location = None; + let args = self.parse_comma_separated(|p| { + p.parse_function_arg_in_list(&mut first_variadic_location) + })?; + Self::verify_variadic_argument_is_last(&args, first_variadic_location)?; self.expect_token(&Token::RParen)?; Ok(args) } @@ -19727,16 +19747,17 @@ impl<'a> Parser<'a> { }); } let mut args = vec![]; + let mut first_variadic_location = None; let settings = loop { if let Some(settings) = self.parse_settings()? { break Some(settings); } - args.push(self.parse_function_args()?); + args.push(self.parse_function_arg_in_list(&mut first_variadic_location)?); if self.is_parse_comma_separated_end() { break None; } }; - Self::verify_variadic_argument_is_last(&args)?; + Self::verify_variadic_argument_is_last(&args, first_variadic_location)?; self.expect_token(&Token::RParen)?; Ok(TableFunctionArgs { args, settings }) } @@ -19774,15 +19795,18 @@ impl<'a> Parser<'a> { let loc = self.peek_token_ref().span.start; let duplicate_treatment = self.parse_duplicate_treatment()?; + let mut first_variadic_location = None; let args = if duplicate_treatment != Some(DuplicateTreatment::Distinct) && self.dialect.supports_aggregate_order_by_with_empty_args() && self.peek_keywords(&[Keyword::ORDER, Keyword::BY]) { vec![] } else { - self.parse_comma_separated(Parser::parse_function_args)? + self.parse_comma_separated(|p| { + p.parse_function_arg_in_list(&mut first_variadic_location) + })? }; - Self::verify_variadic_argument_is_last(&args)?; + Self::verify_variadic_argument_is_last(&args, first_variadic_location)?; // PostgreSQL pairs the marker with neither `DISTINCT` nor `ALL`. if duplicate_treatment.is_some() && matches!(args.last(), Some(FunctionArg::Variadic(_))) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 365d50e1ff..c5c89b5669 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11164,3 +11164,58 @@ SELECT p.pubname, let reparsed = pg().parse_sql_statements(&statement.to_string()).unwrap(); assert_eq!(vec![statement], reparsed); } + +#[test] +fn variadic_not_last_error_points_to_keyword() { + for (sql, line, column) in [ + ("SELECT f(VARIADIC a, b)", 1, 10), + ("SELECT f(1, VARIADIC a, b)", 1, 13), + ("SELECT f(VARIADIC a, VARIADIC b)", 1, 10), + ("SELECT * FROM f(VARIADIC a, b)", 1, 17), + ("SELECT * FROM LATERAL f(VARIADIC a, b)", 1, 25), + ( + r#"SELECT f( + VARIADIC /* marker */ a, b)"#, + 2, + 3, + ), + ] { + assert_eq!( + Parser::parse_sql(&sqlparser::dialect::PostgreSqlDialect {}, sql), + Err(ParserError::ParserError(format!( + "A VARIADIC argument must be the last function argument at Line: {line}, Column: {column}", + ))), + "{sql}", + ); + } +} + +#[test] +fn variadic_argument_operand_span() { + let dialect = sqlparser::dialect::PostgreSqlDialect {}; + let arg = Parser::new(&dialect) + .try_with_sql("VARIADIC schema.x") + .unwrap() + .parse_function_args() + .unwrap(); + assert_eq!( + sqlparser::ast::Spanned::span(&arg), + sqlparser::tokenizer::Span::new( + sqlparser::tokenizer::Location::new(1, 10), + sqlparser::tokenizer::Location::new(1, 18), + ), + ); +} + +#[test] +fn variadic_table_argument_preserves_operand_errors() { + assert_eq!( + Parser::parse_sql( + &sqlparser::dialect::PostgreSqlDialect {}, + "SELECT * FROM LATERAL f(VARIADIC *)", + ), + Err(ParserError::ParserError( + "A VARIADIC argument cannot be a wildcard at Line: 1, Column: 25".to_string(), + )), + ); +} From b1729d7b0ad0ab8fb201861f5a0ad61aa34fc7c7 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:18:49 +0200 Subject: [PATCH 4/6] Place regression tests beside related syntax cases --- tests/sqlparser_postgres.rs | 1098 +++++++++++++++++------------------ 1 file changed, 549 insertions(+), 549 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c5c89b5669..db9ed5d17e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10456,6 +10456,349 @@ fn parse_quoted_argument_names_in_function_signatures() { ); } +#[test] +fn parse_variadic_function_argument() { + pg().verified_stmt("SELECT concat(VARIADIC ARRAY[1, 2])"); + pg().verified_stmt("SELECT concat_ws('-', VARIADIC ARRAY['a'])"); + pg().verified_stmt( + "SELECT * FROM pg_get_publication_tables(VARIADIC ARRAY['pub'::TEXT]) AS gpt", + ); + + // PostgreSQL accepts the marker on a named final argument too. + let select = pg().verified_only_select("SELECT f(VARIADIC a => ARRAY[1])"); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!( + &vec![FunctionArg::Variadic(Box::new(FunctionArg::ExprNamed { + name: Expr::Identifier(Ident::new("a")), + arg: FunctionArgExpr::Expr(Expr::Array(Array { + elem: vec![Expr::value(number("1"))], + named: true, + })), + operator: FunctionArgOperator::RightArrow, + }))], + args + ), + other => panic!("Expected a function call, found {other:?}"), + } + + // PostgreSQL reserves the keyword, so the marker holds even where an + // unreserved dialect would read `variadic - a` as an expression. + let select = pg().verified_only_select("SELECT concat(VARIADIC -a)"); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!( + &vec![FunctionArg::Variadic(Box::new(FunctionArg::Unnamed( + FunctionArgExpr::Expr(Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Identifier(Ident::new("a"))), + }) + )))], + args + ), + other => panic!("Expected a function call, found {other:?}"), + } + + // Since the keyword is reserved, it never passes a column of that name, + // matching PostgreSQL's own rejection of `SELECT f(variadic)`. + assert_eq!( + ParserError::ParserError("Expected: an expression, found: )".to_string()), + pg().parse_sql_statements("SELECT f(variadic)").unwrap_err() + ); + + // The marker only applies to the final argument. + let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument must be the last function argument".to_string() + ), + dialects + .parse_sql_statements("SELECT concat(VARIADIC a, b)") + .unwrap_err() + ); + + // A comma introducing the table-function `SETTINGS` clause is no argument. + all_dialects_where(|d| d.supports_variadic_function_argument() && d.supports_settings()) + .verified_stmt("SELECT * FROM f(VARIADIC a, SETTINGS x = 1)"); + + // A bare clause keyword is an operand, not the start of that clause. + dialects.verified_stmt("SELECT f(VARIADIC separator)"); + dialects.verified_stmt("SELECT f(VARIADIC returning)"); + + // PostgreSQL's VARIADIC productions require expression operands, so no + // wildcard argument form is valid. + for sql in ["SELECT f(VARIADIC *)", "SELECT f(VARIADIC a => *)"] { + assert_eq!( + ParserError::ParserError("A VARIADIC argument cannot be a wildcard".to_string()), + dialects.parse_sql_statements(sql).unwrap_err(), + "{sql}" + ); + } + dialects.verified_stmt("SELECT f(*)"); + + // Dialects without the marker read `VARIADIC` as an identifier, so the + // argument that follows it has nowhere to go. + let unsupported = all_dialects_except(|d| d.supports_variadic_function_argument()); + assert_eq!( + ParserError::ParserError("Expected: ), found: a".to_string()), + unsupported + .parse_sql_statements("SELECT concat(VARIADIC a)") + .unwrap_err() + ); + + // PostgreSQL has no production pairing the marker with a duplicate + // treatment, so `count(DISTINCT VARIADIC ARRAY[1])` is a syntax error there. + for sql in [ + "SELECT count(DISTINCT VARIADIC ARRAY[1])", + "SELECT count(ALL VARIADIC ARRAY[1])", + ] { + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument cannot be combined with DISTINCT or ALL".to_string() + ), + dialects.parse_sql_statements(sql).unwrap_err(), + "{sql}" + ); + } + + // The rejection points at the duplicate treatment it names, even when + // earlier arguments precede the marker. + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument cannot be combined with DISTINCT or ALL at Line: 1, Column: 14" + .to_string() + ), + Parser::parse_sql( + &PostgreSqlDialect {}, + "SELECT count(DISTINCT a, VARIADIC b)" + ) + .unwrap_err() + ); + + // A column of that name still may, where the keyword is unreserved. + all_dialects_where(|d| { + d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) + }) + .verified_stmt("SELECT count(DISTINCT variadic)"); +} + +/// Dialects that accept the marker without reserving `VARIADIC`, such as +/// `GenericDialect`, must still read `variadic` as an ordinary identifier +/// wherever it stands as an argument on its own. +#[test] +fn parse_variadic_as_identifier_function_argument() { + let dialects = all_dialects_where(|d| { + d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) + }); + for sql in [ + "SELECT f(variadic)", + "SELECT f(variadic, x)", + "SELECT f(x, variadic)", + "SELECT f(variadic + 1)", + "SELECT f(variadic.x)", + "SELECT f(variadic[1])", + "SELECT f(variadic(1))", + "SELECT f(variadic AS x)", + "SELECT f(variadic WHERE x)", + "SELECT f(variadic LIMIT 1)", + "SELECT array_agg(variadic ORDER BY x)", + "SELECT count(variadic) FROM t WHERE variadic IS NULL", + ] { + dialects.verified_stmt(sql); + } + + // Display alone cannot tell `Unnamed(variadic + 1)` from + // `Variadic(Unnamed(+1))` apart from spelling, so assert the shape. + for (sql, expected) in [ + ( + "SELECT f(variadic)", + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new( + "variadic", + )))), + ), + ( + "SELECT f(variadic + 1)", + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("variadic"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::value(number("1"))), + })), + ), + ] { + let select = dialects.verified_only_select(sql); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!(&vec![expected], args), + other => panic!("Expected a function call, found {other:?}"), + } + } + + // The marker still applies when the ordinary reading cannot stand alone. + dialects.verified_stmt("SELECT f(VARIADIC a)"); +} + +/// Where the ordinary reading of the argument stands alone and a clause +/// follows it, the plain reading is kept and the clause is parsed as usual. +#[test] +fn parse_variadic_plain_reading_before_argument_clauses() { + let dialects = all_dialects_where(|d| { + d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) + }); + // A token that cannot extend the expression leaves the marker reading, + // while clause keywords keep the plain reading. + for sql in [ + "SELECT f(VARIADIC 5)", + "SELECT f(variadic IGNORE NULLS)", + "SELECT f(variadic RESPECT NULLS)", + "SELECT f(variadic ON OVERFLOW ERROR)", + "SELECT f(variadic NULL ON NULL)", + "SELECT f(variadic ABSENT ON NULL)", + "SELECT f(variadic HAVING MIN 1)", + "SELECT f(variadic HAVING MAX 1)", + ] { + dialects.verified_stmt(sql); + } + + // The plain argument keeps its identifier shape next to the clause. + let select = dialects.verified_only_select("SELECT f(variadic IGNORE NULLS)"); + match expr_from_projection(only(&select.projection)) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("variadic")) + ))], + args + ); + assert_eq!( + &vec![FunctionArgumentClause::IgnoreOrRespectNulls( + NullTreatment::IgnoreNulls + )], + clauses + ); + } + other => panic!("Expected a function call, found {other:?}"), + } +} + +/// The marker is rejected on non-final arguments in table function argument +/// lists and `LATERAL` function calls as well. +#[test] +fn parse_variadic_not_last_in_table_function_argument_lists() { + let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); + for sql in [ + "SELECT * FROM f(VARIADIC a, b)", + "SELECT * FROM LATERAL f(VARIADIC a, b)", + ] { + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument must be the last function argument".to_string() + ), + dialects.parse_sql_statements(sql).unwrap_err(), + "{sql}" + ); + } +} + +/// A recursion limit exhausted inside the speculative plain reading is +/// propagated rather than treated as a failed plain reading. +#[test] +fn parse_variadic_recursion_limit_in_plain_reading() { + let result = Parser::new(&GenericDialect {}) + .with_recursion_limit(0) + .try_with_sql("VARIADIC 5") + .unwrap() + .parse_function_args(); + assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn parse_pg_publication_tables_view_definition() { + // `pg_get_viewdef('pg_publication_tables'::regclass, true)` on PostgreSQL 18. + let sql = r#" +SELECT p.pubname, + n.nspname AS schemaname, + c.relname AS tablename, + ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg + FROM pg_attribute a + WHERE a.attrelid = gpt.relid AND (a.attnum = ANY (gpt.attrs::smallint[]))) AS attnames, + pg_get_expr(gpt.qual, gpt.relid) AS rowfilter + FROM pg_publication p, + LATERAL pg_get_publication_tables(VARIADIC ARRAY[p.pubname::text]) gpt(pubid, relid, attrs, qual), + pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.oid = gpt.relid +"#; + let mut statements = pg().parse_sql_statements(sql).unwrap(); + assert_eq!(1, statements.len()); + let statement = statements.pop().unwrap(); + let reparsed = pg().parse_sql_statements(&statement.to_string()).unwrap(); + assert_eq!(vec![statement], reparsed); +} + +#[test] +fn variadic_not_last_error_points_to_keyword() { + for (sql, line, column) in [ + ("SELECT f(VARIADIC a, b)", 1, 10), + ("SELECT f(1, VARIADIC a, b)", 1, 13), + ("SELECT f(VARIADIC a, VARIADIC b)", 1, 10), + ("SELECT * FROM f(VARIADIC a, b)", 1, 17), + ("SELECT * FROM LATERAL f(VARIADIC a, b)", 1, 25), + ( + r#"SELECT f( + VARIADIC /* marker */ a, b)"#, + 2, + 3, + ), + ] { + assert_eq!( + Parser::parse_sql(&sqlparser::dialect::PostgreSqlDialect {}, sql), + Err(ParserError::ParserError(format!( + "A VARIADIC argument must be the last function argument at Line: {line}, Column: {column}", + ))), + "{sql}", + ); + } +} + +#[test] +fn variadic_argument_operand_span() { + let dialect = sqlparser::dialect::PostgreSqlDialect {}; + let arg = Parser::new(&dialect) + .try_with_sql("VARIADIC schema.x") + .unwrap() + .parse_function_args() + .unwrap(); + assert_eq!( + sqlparser::ast::Spanned::span(&arg), + sqlparser::tokenizer::Span::new( + sqlparser::tokenizer::Location::new(1, 10), + sqlparser::tokenizer::Location::new(1, 18), + ), + ); +} + +#[test] +fn variadic_table_argument_preserves_operand_errors() { + assert_eq!( + Parser::parse_sql( + &sqlparser::dialect::PostgreSqlDialect {}, + "SELECT * FROM LATERAL f(VARIADIC *)", + ), + Err(ParserError::ParserError( + "A VARIADIC argument cannot be a wildcard at Line: 1, Column: 25".to_string(), + )), + ); +} + #[test] fn parse_alter_table_constraint_check_no_inherit() { match pg_and_generic() @@ -10614,608 +10957,265 @@ fn parse_insert_overriding() { "INSERT INTO t (id) OVERRIDING VALUE VALUES (1)", "Expected: SYSTEM or USER, found: VALUE", ), - ( - "INSERT INTO t (id) OVERRIDING SYSTEM VALUES (1)", - "Expected: VALUE, found: VALUES", - ), - ( - "INSERT INTO t (id) OVERRIDING DEFAULT VALUE VALUES (1)", - "Expected: SYSTEM or USER, found: DEFAULT", - ), - ( - "INSERT INTO t OVERRIDING SYSTEM VALUE DEFAULT VALUES", - "Expected: SELECT, VALUES, or a subquery in the query body, found: DEFAULT", - ), - ] { - assert_eq!( - pg_and_generic().parse_sql_statements(sql), - Err(ParserError::ParserError(err.to_string())), - "{sql}" - ); - } -} - -#[test] -fn parse_insert_by_name_keywords_as_table_and_alias() { - // Without a table name, `BY NAME` is not an INSERT BY NAME clause. PostgreSQL - // treats `BY` as the table name and `NAME` as its implicit table alias. - match pg().verified_stmt("INSERT INTO BY NAME SELECT 1 AS a") { - Statement::Insert(Insert { - table: TableObject::TableName(table), - table_alias: Some(table_alias), - by_name, - .. - }) => { - assert_eq!(table.to_string(), "BY"); - assert_eq!(table_alias.alias.value, "NAME"); - assert!(!by_name); - } - statement => panic!("Expected INSERT statement, got: {statement:?}"), - } -} - -#[test] -fn parse_pg_abs_space_before_negative_operand() { - // `@-` tokenizes as a geometric operator prefix, so displaying PGAbs - // without a space breaks re-parsing of a negative operand. - pg().verified_stmt("SELECT @ -2"); - pg().one_statement_parses_to("SELECT @a", "SELECT @ a"); - let err = pg().parse_sql_statements("SELECT @-2").unwrap_err(); - assert_eq!( - ParserError::TokenizerError( - "Expected a valid binary operator after '@-' at Line: 1, Column: 10".to_string(), - ), - err - ); -} - -#[test] -fn parse_bitwise_not_before_pg_prefix_operators() { - pg().one_statement_parses_to("SELECT ~ @2", "SELECT ~ @ 2"); - pg().verified_stmt("SELECT ~ @ 2"); - pg().one_statement_parses_to("SELECT ~ #x", "SELECT ~ # x"); -} - -#[test] -fn parse_unary_minus_before_pg_prefix_operators() { - pg().one_statement_parses_to("SELECT - ~1", "SELECT - ~ 1"); - pg().verified_stmt("SELECT - ~ 1"); - pg().one_statement_parses_to("SELECT - @2", "SELECT - @ 2"); - pg().verified_stmt("SELECT - @ 2"); - pg().one_statement_parses_to("SELECT - #x", "SELECT - # x"); -} - -#[test] -fn parse_postfix_factorial_spacing() { - pg().verified_stmt("SELECT a!"); - pg().verified_stmt("SELECT 5!"); - pg().verified_stmt("SELECT (a!)!"); - pg().verified_stmt("SELECT a! !"); - pg().verified_stmt("SELECT a! ! !"); - pg().verified_stmt("SELECT a! ! % 2"); - pg().one_statement_parses_to("SELECT a! !%2", "SELECT a! ! % 2"); - pg().one_statement_parses_to("SELECT -a, +b, a! !%2, a", "SELECT -a, +b, a! ! % 2, a"); - - let err = pg().parse_sql_statements("SELECT a!!").unwrap_err(); - assert_eq!( - ParserError::ParserError("Expected: end of statement, found: !!".to_string()), - err - ); -} - -#[test] -fn parse_pg_roots_render_apart_from_operand() { - pg().verified_stmt("SELECT |/ -2"); - pg().verified_stmt("SELECT ||/ -2"); - pg().verified_stmt("SELECT |/ ||/ 2"); -} - -#[test] -fn parse_stage_table_factor_rejected() { - let sql = "SELECT * FROM @stage"; - assert_eq!( - pg().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError("Expected: identifier, found: @".to_string()), - ); -} - -#[test] -fn parse_bitstring_literal_escaping() { - pg_and_generic().verified_stmt("SELECT B''''"); - pg_and_generic().verified_stmt("SELECT B'it''s'"); -} - -#[test] -fn parse_join_using_alias() { - let sql = "SELECT * FROM t1 JOIN t2 USING (id) AS joined_cols"; - let dialect = PostgreSqlDialect {}; - let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); - let statement = parser.parse_statement().unwrap(); - let Statement::Query(query) = statement else { - panic!("Expected a query statement"); - }; - let SetExpr::Select(select) = *query.body else { - panic!("Expected a SELECT query"); - }; - let join = &select.from[0].joins[0]; - - assert_eq!( - JoinOperator::Join(JoinConstraint::UsingAlias( - vec![ObjectName::from(vec![Ident::new("id")])], - Ident::new("joined_cols"), - )), - join.join_operator - ); - assert_eq!( - Span::new(Location::new(1, 33), Location::new(1, 51)), - join.join_operator.span() - ); - assert_eq!( - "SELECT * FROM t1 JOIN t2 USING(id) AS joined_cols", - select.to_string() - ); -} - -#[test] -fn parse_string_literal_alias_rejected() { - for (sql, expected) in [ - ("SELECT 1 'x'", "Expected: end of statement, found: 'x'"), - ( - "SELECT (a) 'x' FROM t", - "Expected: end of statement, found: 'x'", - ), - ( - "SELECT a AS 'x' FROM t", - "Expected: an identifier after AS, found: 'x'", - ), - ( - "SELECT a FROM t 'x'", - "Expected: end of statement, found: 'x'", - ), - ( - "SELECT a FROM t AS 'x'", - "Expected: an identifier after AS, found: 'x'", - ), - ] { - assert_eq!( - pg().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError(expected.to_string()), - "{sql}" - ); - } - pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#); -} - -#[test] -fn parse_custom_type_typed_string() { - assert_eq!( - pg().verified_expr("public.mood 'happy'"), - Expr::TypedString(TypedString { - data_type: DataType::Custom( - ObjectName::from(vec!["public".into(), "mood".into()]), - vec![] - ), - value: Value::SingleQuotedString("happy".to_string()).with_empty_span(), - uses_odbc_syntax: false, - }) - ); - pg().verified_stmt("SELECT mood 'happy'"); - pg().verified_stmt("SELECT count(*) FROM t WHERE m = mood 'sad'"); - pg().verified_stmt("SELECT pg_typeof(mood 'happy')"); - pg().verified_stmt(r#"SELECT "mood" 'happy'"#); - pg().verified_stmt("SELECT status 'on'"); - pg().verified_stmt("SELECT left 'x'"); - pg().verified_stmt("SELECT pg_catalog.coalesce 'x'"); - pg().verified_stmt("SELECT mood E'happy'"); - pg().verified_stmt("SELECT mood $$happy$$"); - - assert!(matches!( - pg().verified_expr("NOT 'a' LIKE 'b'"), - Expr::UnaryOp { - op: UnaryOperator::Not, - .. - } - )); - assert!(matches!( - pg().verified_expr("point '1,2'"), - Expr::TypedString(TypedString { - data_type: DataType::GeometricType(GeometricTypeKind::Point), - .. - }) - )); - assert_eq!( - pg().parse_sql_statements("SELECT pg_typeof(coalesce 'x')") - .unwrap_err(), - ParserError::ParserError("Expected: ), found: 'x'".to_string()) - ); - assert_eq!( - pg().parse_sql_statements("SELECT mood 5").unwrap_err(), - ParserError::ParserError("Expected: end of statement, found: 5".to_string()) - ); -} - -#[test] -fn parse_geometric_keyword_as_column_name() { - pg().verified_stmt("SELECT circle FROM t"); - pg().verified_stmt("SELECT point FROM t"); -} - -#[test] -fn parse_geometric_typed_literal_error_propagates() { - assert_eq!( - pg().parse_sql_statements("SELECT circle( FROM t") - .unwrap_err(), - ParserError::ParserError("Expected: a value, found: (".to_string()) - ); -} - -#[test] -fn parse_create_aggregate_rejects_output_arguments_and_defaults() { - for (args, expected) in [ - ("OUT x INT", "Aggregates cannot have output arguments"), - ("INOUT x INT", "Aggregates cannot have output arguments"), - ( - "x INT DEFAULT 1", - "Aggregate arguments cannot have defaults", + ( + "INSERT INTO t (id) OVERRIDING SYSTEM VALUES (1)", + "Expected: VALUE, found: VALUES", ), - ("x INT = 1", "Aggregate arguments cannot have defaults"), ( - "ORDER BY OUT x INT", - "Aggregates cannot have output arguments", + "INSERT INTO t (id) OVERRIDING DEFAULT VALUE VALUES (1)", + "Expected: SYSTEM or USER, found: DEFAULT", ), ( - "INT ORDER BY x INT DEFAULT 1", - "Aggregate arguments cannot have defaults", + "INSERT INTO t OVERRIDING SYSTEM VALUE DEFAULT VALUES", + "Expected: SELECT, VALUES, or a subquery in the query body, found: DEFAULT", ), ] { - let sql = format!("CREATE AGGREGATE a ({args}) (SFUNC = f, STYPE = INT)"); assert_eq!( - pg_and_generic().parse_sql_statements(&sql).unwrap_err(), - ParserError::ParserError(expected.to_string()), + pg_and_generic().parse_sql_statements(sql), + Err(ParserError::ParserError(err.to_string())), "{sql}" ); } } #[test] -fn parse_variadic_function_argument() { - pg().verified_stmt("SELECT concat(VARIADIC ARRAY[1, 2])"); - pg().verified_stmt("SELECT concat_ws('-', VARIADIC ARRAY['a'])"); - pg().verified_stmt( - "SELECT * FROM pg_get_publication_tables(VARIADIC ARRAY['pub'::TEXT]) AS gpt", - ); - - // PostgreSQL accepts the marker on a named final argument too. - let select = pg().verified_only_select("SELECT f(VARIADIC a => ARRAY[1])"); - match expr_from_projection(only(&select.projection)) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, .. }), - .. - }) => assert_eq!( - &vec![FunctionArg::Variadic(Box::new(FunctionArg::ExprNamed { - name: Expr::Identifier(Ident::new("a")), - arg: FunctionArgExpr::Expr(Expr::Array(Array { - elem: vec![Expr::value(number("1"))], - named: true, - })), - operator: FunctionArgOperator::RightArrow, - }))], - args - ), - other => panic!("Expected a function call, found {other:?}"), - } - - // PostgreSQL reserves the keyword, so the marker holds even where an - // unreserved dialect would read `variadic - a` as an expression. - let select = pg().verified_only_select("SELECT concat(VARIADIC -a)"); - match expr_from_projection(only(&select.projection)) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, .. }), +fn parse_insert_by_name_keywords_as_table_and_alias() { + // Without a table name, `BY NAME` is not an INSERT BY NAME clause. PostgreSQL + // treats `BY` as the table name and `NAME` as its implicit table alias. + match pg().verified_stmt("INSERT INTO BY NAME SELECT 1 AS a") { + Statement::Insert(Insert { + table: TableObject::TableName(table), + table_alias: Some(table_alias), + by_name, .. - }) => assert_eq!( - &vec![FunctionArg::Variadic(Box::new(FunctionArg::Unnamed( - FunctionArgExpr::Expr(Expr::UnaryOp { - op: UnaryOperator::Minus, - expr: Box::new(Expr::Identifier(Ident::new("a"))), - }) - )))], - args - ), - other => panic!("Expected a function call, found {other:?}"), - } - - // Since the keyword is reserved, it never passes a column of that name, - // matching PostgreSQL's own rejection of `SELECT f(variadic)`. - assert_eq!( - ParserError::ParserError("Expected: an expression, found: )".to_string()), - pg().parse_sql_statements("SELECT f(variadic)").unwrap_err() - ); - - // The marker only applies to the final argument. - let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); - assert_eq!( - ParserError::ParserError( - "A VARIADIC argument must be the last function argument".to_string() - ), - dialects - .parse_sql_statements("SELECT concat(VARIADIC a, b)") - .unwrap_err() - ); - - // A comma introducing the table-function `SETTINGS` clause is no argument. - all_dialects_where(|d| d.supports_variadic_function_argument() && d.supports_settings()) - .verified_stmt("SELECT * FROM f(VARIADIC a, SETTINGS x = 1)"); - - // A bare clause keyword is an operand, not the start of that clause. - dialects.verified_stmt("SELECT f(VARIADIC separator)"); - dialects.verified_stmt("SELECT f(VARIADIC returning)"); - - // PostgreSQL's VARIADIC productions require expression operands, so no - // wildcard argument form is valid. - for sql in ["SELECT f(VARIADIC *)", "SELECT f(VARIADIC a => *)"] { - assert_eq!( - ParserError::ParserError("A VARIADIC argument cannot be a wildcard".to_string()), - dialects.parse_sql_statements(sql).unwrap_err(), - "{sql}" - ); - } - dialects.verified_stmt("SELECT f(*)"); - - // Dialects without the marker read `VARIADIC` as an identifier, so the - // argument that follows it has nowhere to go. - let unsupported = all_dialects_except(|d| d.supports_variadic_function_argument()); - assert_eq!( - ParserError::ParserError("Expected: ), found: a".to_string()), - unsupported - .parse_sql_statements("SELECT concat(VARIADIC a)") - .unwrap_err() - ); - - // PostgreSQL has no production pairing the marker with a duplicate - // treatment, so `count(DISTINCT VARIADIC ARRAY[1])` is a syntax error there. - for sql in [ - "SELECT count(DISTINCT VARIADIC ARRAY[1])", - "SELECT count(ALL VARIADIC ARRAY[1])", - ] { - assert_eq!( - ParserError::ParserError( - "A VARIADIC argument cannot be combined with DISTINCT or ALL".to_string() - ), - dialects.parse_sql_statements(sql).unwrap_err(), - "{sql}" - ); + }) => { + assert_eq!(table.to_string(), "BY"); + assert_eq!(table_alias.alias.value, "NAME"); + assert!(!by_name); + } + statement => panic!("Expected INSERT statement, got: {statement:?}"), } +} - // The rejection points at the duplicate treatment it names, even when - // earlier arguments precede the marker. +#[test] +fn parse_pg_abs_space_before_negative_operand() { + // `@-` tokenizes as a geometric operator prefix, so displaying PGAbs + // without a space breaks re-parsing of a negative operand. + pg().verified_stmt("SELECT @ -2"); + pg().one_statement_parses_to("SELECT @a", "SELECT @ a"); + let err = pg().parse_sql_statements("SELECT @-2").unwrap_err(); assert_eq!( - ParserError::ParserError( - "A VARIADIC argument cannot be combined with DISTINCT or ALL at Line: 1, Column: 14" - .to_string() + ParserError::TokenizerError( + "Expected a valid binary operator after '@-' at Line: 1, Column: 10".to_string(), ), - Parser::parse_sql( - &PostgreSqlDialect {}, - "SELECT count(DISTINCT a, VARIADIC b)" - ) - .unwrap_err() + err ); - - // A column of that name still may, where the keyword is unreserved. - all_dialects_where(|d| { - d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) - }) - .verified_stmt("SELECT count(DISTINCT variadic)"); } -/// Dialects that accept the marker without reserving `VARIADIC`, such as -/// `GenericDialect`, must still read `variadic` as an ordinary identifier -/// wherever it stands as an argument on its own. #[test] -fn parse_variadic_as_identifier_function_argument() { - let dialects = all_dialects_where(|d| { - d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) - }); - for sql in [ - "SELECT f(variadic)", - "SELECT f(variadic, x)", - "SELECT f(x, variadic)", - "SELECT f(variadic + 1)", - "SELECT f(variadic.x)", - "SELECT f(variadic[1])", - "SELECT f(variadic(1))", - "SELECT f(variadic AS x)", - "SELECT f(variadic WHERE x)", - "SELECT f(variadic LIMIT 1)", - "SELECT array_agg(variadic ORDER BY x)", - "SELECT count(variadic) FROM t WHERE variadic IS NULL", - ] { - dialects.verified_stmt(sql); - } - - // Display alone cannot tell `Unnamed(variadic + 1)` from - // `Variadic(Unnamed(+1))` apart from spelling, so assert the shape. - for (sql, expected) in [ - ( - "SELECT f(variadic)", - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new( - "variadic", - )))), - ), - ( - "SELECT f(variadic + 1)", - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("variadic"))), - op: BinaryOperator::Plus, - right: Box::new(Expr::value(number("1"))), - })), - ), - ] { - let select = dialects.verified_only_select(sql); - match expr_from_projection(only(&select.projection)) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, .. }), - .. - }) => assert_eq!(&vec![expected], args), - other => panic!("Expected a function call, found {other:?}"), - } - } +fn parse_bitwise_not_before_pg_prefix_operators() { + pg().one_statement_parses_to("SELECT ~ @2", "SELECT ~ @ 2"); + pg().verified_stmt("SELECT ~ @ 2"); + pg().one_statement_parses_to("SELECT ~ #x", "SELECT ~ # x"); +} - // The marker still applies when the ordinary reading cannot stand alone. - dialects.verified_stmt("SELECT f(VARIADIC a)"); +#[test] +fn parse_unary_minus_before_pg_prefix_operators() { + pg().one_statement_parses_to("SELECT - ~1", "SELECT - ~ 1"); + pg().verified_stmt("SELECT - ~ 1"); + pg().one_statement_parses_to("SELECT - @2", "SELECT - @ 2"); + pg().verified_stmt("SELECT - @ 2"); + pg().one_statement_parses_to("SELECT - #x", "SELECT - # x"); } -/// Where the ordinary reading of the argument stands alone and a clause -/// follows it, the plain reading is kept and the clause is parsed as usual. #[test] -fn parse_variadic_plain_reading_before_argument_clauses() { - let dialects = all_dialects_where(|d| { - d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) - }); - // A token that cannot extend the expression leaves the marker reading, - // while clause keywords keep the plain reading. - for sql in [ - "SELECT f(VARIADIC 5)", - "SELECT f(variadic IGNORE NULLS)", - "SELECT f(variadic RESPECT NULLS)", - "SELECT f(variadic ON OVERFLOW ERROR)", - "SELECT f(variadic NULL ON NULL)", - "SELECT f(variadic ABSENT ON NULL)", - "SELECT f(variadic HAVING MIN 1)", - "SELECT f(variadic HAVING MAX 1)", - ] { - dialects.verified_stmt(sql); - } +fn parse_postfix_factorial_spacing() { + pg().verified_stmt("SELECT a!"); + pg().verified_stmt("SELECT 5!"); + pg().verified_stmt("SELECT (a!)!"); + pg().verified_stmt("SELECT a! !"); + pg().verified_stmt("SELECT a! ! !"); + pg().verified_stmt("SELECT a! ! % 2"); + pg().one_statement_parses_to("SELECT a! !%2", "SELECT a! ! % 2"); + pg().one_statement_parses_to("SELECT -a, +b, a! !%2, a", "SELECT -a, +b, a! ! % 2, a"); - // The plain argument keeps its identifier shape next to the clause. - let select = dialects.verified_only_select("SELECT f(variadic IGNORE NULLS)"); - match expr_from_projection(only(&select.projection)) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), - .. - }) => { - assert_eq!( - &vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( - Expr::Identifier(Ident::new("variadic")) - ))], - args - ); - assert_eq!( - &vec![FunctionArgumentClause::IgnoreOrRespectNulls( - NullTreatment::IgnoreNulls - )], - clauses - ); - } - other => panic!("Expected a function call, found {other:?}"), - } + let err = pg().parse_sql_statements("SELECT a!!").unwrap_err(); + assert_eq!( + ParserError::ParserError("Expected: end of statement, found: !!".to_string()), + err + ); } -/// The marker is rejected on non-final arguments in table function argument -/// lists and `LATERAL` function calls as well. #[test] -fn parse_variadic_not_last_in_table_function_argument_lists() { - let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); - for sql in [ - "SELECT * FROM f(VARIADIC a, b)", - "SELECT * FROM LATERAL f(VARIADIC a, b)", - ] { - assert_eq!( - ParserError::ParserError( - "A VARIADIC argument must be the last function argument".to_string() - ), - dialects.parse_sql_statements(sql).unwrap_err(), - "{sql}" - ); - } +fn parse_pg_roots_render_apart_from_operand() { + pg().verified_stmt("SELECT |/ -2"); + pg().verified_stmt("SELECT ||/ -2"); + pg().verified_stmt("SELECT |/ ||/ 2"); } -/// A recursion limit exhausted inside the speculative plain reading is -/// propagated rather than treated as a failed plain reading. #[test] -fn parse_variadic_recursion_limit_in_plain_reading() { - let result = Parser::new(&GenericDialect {}) - .with_recursion_limit(0) - .try_with_sql("VARIADIC 5") - .unwrap() - .parse_function_args(); - assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +fn parse_stage_table_factor_rejected() { + let sql = "SELECT * FROM @stage"; + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: identifier, found: @".to_string()), + ); } #[test] -fn parse_pg_publication_tables_view_definition() { - // `pg_get_viewdef('pg_publication_tables'::regclass, true)` on PostgreSQL 18. - let sql = r#" -SELECT p.pubname, - n.nspname AS schemaname, - c.relname AS tablename, - ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg - FROM pg_attribute a - WHERE a.attrelid = gpt.relid AND (a.attnum = ANY (gpt.attrs::smallint[]))) AS attnames, - pg_get_expr(gpt.qual, gpt.relid) AS rowfilter - FROM pg_publication p, - LATERAL pg_get_publication_tables(VARIADIC ARRAY[p.pubname::text]) gpt(pubid, relid, attrs, qual), - pg_class c - JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.oid = gpt.relid -"#; - let mut statements = pg().parse_sql_statements(sql).unwrap(); - assert_eq!(1, statements.len()); - let statement = statements.pop().unwrap(); - let reparsed = pg().parse_sql_statements(&statement.to_string()).unwrap(); - assert_eq!(vec![statement], reparsed); +fn parse_bitstring_literal_escaping() { + pg_and_generic().verified_stmt("SELECT B''''"); + pg_and_generic().verified_stmt("SELECT B'it''s'"); } #[test] -fn variadic_not_last_error_points_to_keyword() { - for (sql, line, column) in [ - ("SELECT f(VARIADIC a, b)", 1, 10), - ("SELECT f(1, VARIADIC a, b)", 1, 13), - ("SELECT f(VARIADIC a, VARIADIC b)", 1, 10), - ("SELECT * FROM f(VARIADIC a, b)", 1, 17), - ("SELECT * FROM LATERAL f(VARIADIC a, b)", 1, 25), +fn parse_join_using_alias() { + let sql = "SELECT * FROM t1 JOIN t2 USING (id) AS joined_cols"; + let dialect = PostgreSqlDialect {}; + let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); + let statement = parser.parse_statement().unwrap(); + let Statement::Query(query) = statement else { + panic!("Expected a query statement"); + }; + let SetExpr::Select(select) = *query.body else { + panic!("Expected a SELECT query"); + }; + let join = &select.from[0].joins[0]; + + assert_eq!( + JoinOperator::Join(JoinConstraint::UsingAlias( + vec![ObjectName::from(vec![Ident::new("id")])], + Ident::new("joined_cols"), + )), + join.join_operator + ); + assert_eq!( + Span::new(Location::new(1, 33), Location::new(1, 51)), + join.join_operator.span() + ); + assert_eq!( + "SELECT * FROM t1 JOIN t2 USING(id) AS joined_cols", + select.to_string() + ); +} + +#[test] +fn parse_string_literal_alias_rejected() { + for (sql, expected) in [ + ("SELECT 1 'x'", "Expected: end of statement, found: 'x'"), ( - r#"SELECT f( - VARIADIC /* marker */ a, b)"#, - 2, - 3, + "SELECT (a) 'x' FROM t", + "Expected: end of statement, found: 'x'", + ), + ( + "SELECT a AS 'x' FROM t", + "Expected: an identifier after AS, found: 'x'", + ), + ( + "SELECT a FROM t 'x'", + "Expected: end of statement, found: 'x'", + ), + ( + "SELECT a FROM t AS 'x'", + "Expected: an identifier after AS, found: 'x'", ), ] { assert_eq!( - Parser::parse_sql(&sqlparser::dialect::PostgreSqlDialect {}, sql), - Err(ParserError::ParserError(format!( - "A VARIADIC argument must be the last function argument at Line: {line}, Column: {column}", - ))), - "{sql}", + pg().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + "{sql}" ); } + pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#); } #[test] -fn variadic_argument_operand_span() { - let dialect = sqlparser::dialect::PostgreSqlDialect {}; - let arg = Parser::new(&dialect) - .try_with_sql("VARIADIC schema.x") - .unwrap() - .parse_function_args() - .unwrap(); +fn parse_custom_type_typed_string() { assert_eq!( - sqlparser::ast::Spanned::span(&arg), - sqlparser::tokenizer::Span::new( - sqlparser::tokenizer::Location::new(1, 10), - sqlparser::tokenizer::Location::new(1, 18), - ), + pg().verified_expr("public.mood 'happy'"), + Expr::TypedString(TypedString { + data_type: DataType::Custom( + ObjectName::from(vec!["public".into(), "mood".into()]), + vec![] + ), + value: Value::SingleQuotedString("happy".to_string()).with_empty_span(), + uses_odbc_syntax: false, + }) + ); + pg().verified_stmt("SELECT mood 'happy'"); + pg().verified_stmt("SELECT count(*) FROM t WHERE m = mood 'sad'"); + pg().verified_stmt("SELECT pg_typeof(mood 'happy')"); + pg().verified_stmt(r#"SELECT "mood" 'happy'"#); + pg().verified_stmt("SELECT status 'on'"); + pg().verified_stmt("SELECT left 'x'"); + pg().verified_stmt("SELECT pg_catalog.coalesce 'x'"); + pg().verified_stmt("SELECT mood E'happy'"); + pg().verified_stmt("SELECT mood $$happy$$"); + + assert!(matches!( + pg().verified_expr("NOT 'a' LIKE 'b'"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + )); + assert!(matches!( + pg().verified_expr("point '1,2'"), + Expr::TypedString(TypedString { + data_type: DataType::GeometricType(GeometricTypeKind::Point), + .. + }) + )); + assert_eq!( + pg().parse_sql_statements("SELECT pg_typeof(coalesce 'x')") + .unwrap_err(), + ParserError::ParserError("Expected: ), found: 'x'".to_string()) + ); + assert_eq!( + pg().parse_sql_statements("SELECT mood 5").unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: 5".to_string()) ); } #[test] -fn variadic_table_argument_preserves_operand_errors() { +fn parse_geometric_keyword_as_column_name() { + pg().verified_stmt("SELECT circle FROM t"); + pg().verified_stmt("SELECT point FROM t"); +} + +#[test] +fn parse_geometric_typed_literal_error_propagates() { assert_eq!( - Parser::parse_sql( - &sqlparser::dialect::PostgreSqlDialect {}, - "SELECT * FROM LATERAL f(VARIADIC *)", - ), - Err(ParserError::ParserError( - "A VARIADIC argument cannot be a wildcard at Line: 1, Column: 25".to_string(), - )), + pg().parse_sql_statements("SELECT circle( FROM t") + .unwrap_err(), + ParserError::ParserError("Expected: a value, found: (".to_string()) ); } + +#[test] +fn parse_create_aggregate_rejects_output_arguments_and_defaults() { + for (args, expected) in [ + ("OUT x INT", "Aggregates cannot have output arguments"), + ("INOUT x INT", "Aggregates cannot have output arguments"), + ( + "x INT DEFAULT 1", + "Aggregate arguments cannot have defaults", + ), + ("x INT = 1", "Aggregate arguments cannot have defaults"), + ( + "ORDER BY OUT x INT", + "Aggregates cannot have output arguments", + ), + ( + "INT ORDER BY x INT DEFAULT 1", + "Aggregate arguments cannot have defaults", + ), + ] { + let sql = format!("CREATE AGGREGATE a ({args}) (SFUNC = f, STYPE = INT)"); + assert_eq!( + pg_and_generic().parse_sql_statements(&sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + "{sql}" + ); + } +} From bf93f58dc11fd903ffeb577cc5a026fbb84383f3 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:43:08 +0200 Subject: [PATCH 5/6] Add fuzz seeds for pg variadic function arg --- fuzz/fuzz_seeds/generic_2484_variadic_as_identifier.sql | 1 + .../fuzz_seeds/postgres_2484_variadic_function_argument.sql | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 fuzz/fuzz_seeds/generic_2484_variadic_as_identifier.sql create mode 100644 fuzz/fuzz_seeds/postgres_2484_variadic_function_argument.sql diff --git a/fuzz/fuzz_seeds/generic_2484_variadic_as_identifier.sql b/fuzz/fuzz_seeds/generic_2484_variadic_as_identifier.sql new file mode 100644 index 0000000000..66243526b1 --- /dev/null +++ b/fuzz/fuzz_seeds/generic_2484_variadic_as_identifier.sql @@ -0,0 +1 @@ +SELECT f(variadic), f(variadic + 1) FROM t diff --git a/fuzz/fuzz_seeds/postgres_2484_variadic_function_argument.sql b/fuzz/fuzz_seeds/postgres_2484_variadic_function_argument.sql new file mode 100644 index 0000000000..22c00f90b1 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2484_variadic_function_argument.sql @@ -0,0 +1,6 @@ +SELECT concat(VARIADIC ARRAY[1, 2]); +SELECT concat_ws('-', VARIADIC ARRAY['a']); +SELECT * FROM pg_get_publication_tables(VARIADIC ARRAY['pub'::TEXT]) AS gpt; +SELECT f(VARIADIC a => ARRAY[1]); +SELECT concat(VARIADIC -a); +SELECT f(VARIADIC separator) From 86861b4bf53a2744ec8e326ff19727bb57a4d392 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 14:23:46 +0200 Subject: [PATCH 6/6] Simplify variadic argument parsing and tests --- src/ast/mod.rs | 8 +--- src/dialect/mod.rs | 7 +-- src/dialect/postgresql.rs | 2 - src/parser/mod.rs | 73 +++++++----------------------- tests/sqlparser_postgres.rs | 89 +++++++++++++++---------------------- 5 files changed, 54 insertions(+), 125 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 16fae831f3..2a332a61e0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8138,13 +8138,7 @@ pub enum FunctionArg { }, /// An unnamed argument (positional), given by expression or wildcard. Unnamed(FunctionArgExpr), - /// An argument carrying PostgreSQL's `VARIADIC` marker, e.g. - /// `concat(VARIADIC ARRAY['a', 'b'])`. Only the final argument of a call - /// may carry it. - /// - /// Enabled when `Dialect::supports_variadic_function_argument` returns `true`. - /// - /// [PostgreSQL](https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-VARIADIC-FUNCTIONS) + /// A final argument prefixed with PostgreSQL's [`VARIADIC`](https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-VARIADIC-FUNCTIONS) marker. Variadic(Box), } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 291c57810b..4fa6cda8f8 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2127,12 +2127,7 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports the `VARIADIC` marker on the final - /// argument of a function call, e.g. `concat(VARIADIC ARRAY['a', 'b'])`. - /// - /// When this returns false, `VARIADIC` is treated as a regular identifier. - /// - /// [PostgreSQL](https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-VARIADIC-FUNCTIONS) + /// Whether the dialect supports `VARIADIC` on the final function-call argument. fn supports_variadic_function_argument(&self) -> bool { false } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 3e2b9b43bb..556dab7248 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -259,8 +259,6 @@ impl Dialect for PostgreSqlDialect { fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { match kw { Keyword::INTERVAL => false, - // PostgreSQL reserves `VARIADIC`, so inside a call it always - // introduces the argument marker rather than a column of that name. Keyword::VARIADIC => true, _ => RESERVED_FOR_IDENTIFIER.contains(&kw), } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 603d1c19ae..8a783695f7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19494,37 +19494,24 @@ impl<'a> Parser<'a> { } } - /// Parse a single function argument, handling the `VARIADIC` marker as well - /// as named and unnamed variants. - /// - /// Dialects that reserve `VARIADIC`, such as PostgreSQL, always mean the - /// marker. Where the keyword is unreserved, `f(variadic)` passes a column - /// of that name instead, so the ordinary reading is parsed first and kept - /// whenever it stands alone as an argument, and `f(VARIADIC a)` is left as - /// the only reading that takes the marker. + /// Parse a named, positional, or `VARIADIC` function argument. pub fn parse_function_args(&mut self) -> Result { if self.dialect.supports_variadic_function_argument() && self.peek_keyword(Keyword::VARIADIC) { if !self.dialect.is_reserved_for_identifier(Keyword::VARIADIC) { - let plain = self.maybe_parse(|p| { - let arg = p.parse_plain_function_args()?; - if p.peek_ends_function_arg() { - Ok(arg) - } else { - p.expected_ref("end of the function argument", p.peek_token_ref()) + let index = self.index; + if let Some(arg) = self.maybe_parse(|p| p.parse_plain_function_args())? { + if self.peek_ends_function_arg() { + return Ok(arg); } - })?; - if let Some(arg) = plain { - return Ok(arg); + self.index = index; } } let loc = self.peek_token_ref().span.start; self.advance_token(); let arg = self.parse_plain_function_args()?; - // PostgreSQL's VARIADIC productions require expression operands, - // so no wildcard argument form is valid. let carries_wildcard = matches!( &arg, FunctionArg::Unnamed(arg) @@ -19546,20 +19533,22 @@ impl<'a> Parser<'a> { ) -> Result { let loc = self.peek_token_ref().span.start; let arg = self.parse_function_args()?; - if matches!(arg, FunctionArg::Variadic(_)) && first_variadic_location.is_none() { + if let Some(loc) = *first_variadic_location { + return parser_err!( + "A VARIADIC argument must be the last function argument", + loc + ); + } + if matches!(arg, FunctionArg::Variadic(_)) { *first_variadic_location = Some(loc); } Ok(arg) } - /// Returns true when the next token can follow a complete function - /// argument, for example the argument separator, the end of the list, or a - /// clause that [`Parser::parse_function_argument_list`] accepts after the - /// arguments. A clause keyword whose own syntax does not follow is an - /// operand instead, as in `f(VARIADIC separator)`. + /// Whether the next tokens end a plain argument before a delimiter or function clause. fn peek_ends_function_arg(&self) -> bool { let word = match &self.peek_token_ref().token { - Token::Comma | Token::RParen => return true, + Token::Comma | Token::RParen | Token::EOF => return true, Token::Word(word) => word, _ => return false, }; @@ -19568,11 +19557,9 @@ impl<'a> Parser<'a> { match word.keyword { Keyword::ORDER => next_is(Keyword::BY), Keyword::IGNORE | Keyword::RESPECT => next_is(Keyword::NULLS), - // `ON OVERFLOW`, and the `ABSENT ON NULL` / `NULL ON NULL` clauses. Keyword::ON => next_is(Keyword::OVERFLOW), Keyword::ABSENT | Keyword::NULL => next_is(Keyword::ON), Keyword::HAVING => next_is(Keyword::MIN) || next_is(Keyword::MAX), - // These take an expression, a value or a type of their own. Keyword::LIMIT | Keyword::RETURNING | Keyword::SEPARATOR | Keyword::WHERE => { !matches!(next, Token::Comma | Token::RParen) } @@ -19580,31 +19567,7 @@ impl<'a> Parser<'a> { } } - /// Rejects non-final `VARIADIC` arguments at the marker's source location. - fn verify_variadic_argument_is_last( - args: &[FunctionArg], - first_variadic_location: Option, - ) -> Result<(), ParserError> { - let Some((_, preceding)) = args.split_last() else { - return Ok(()); - }; - match first_variadic_location { - Some(loc) - if preceding - .iter() - .any(|arg| matches!(arg, FunctionArg::Variadic(_))) => - { - parser_err!( - "A VARIADIC argument must be the last function argument", - loc - ) - } - _ => Ok(()), - } - } - - /// Parse a single function argument carrying no `VARIADIC` marker, - /// handling named and unnamed variants. + /// Parse a named or positional argument without a `VARIADIC` marker. fn parse_plain_function_args(&mut self) -> Result { // Parse the argument expression once, then check for a named-arg // operator. Parsing it speculatively and re-parsing on the unnamed @@ -19733,7 +19696,6 @@ impl<'a> Parser<'a> { let args = self.parse_comma_separated(|p| { p.parse_function_arg_in_list(&mut first_variadic_location) })?; - Self::verify_variadic_argument_is_last(&args, first_variadic_location)?; self.expect_token(&Token::RParen)?; Ok(args) } @@ -19757,7 +19719,6 @@ impl<'a> Parser<'a> { break None; } }; - Self::verify_variadic_argument_is_last(&args, first_variadic_location)?; self.expect_token(&Token::RParen)?; Ok(TableFunctionArgs { args, settings }) } @@ -19806,9 +19767,7 @@ impl<'a> Parser<'a> { p.parse_function_arg_in_list(&mut first_variadic_location) })? }; - Self::verify_variadic_argument_is_last(&args, first_variadic_location)?; - // PostgreSQL pairs the marker with neither `DISTINCT` nor `ALL`. if duplicate_treatment.is_some() && matches!(args.last(), Some(FunctionArg::Variadic(_))) { return parser_err!( "A VARIADIC argument cannot be combined with DISTINCT or ALL", diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index db9ed5d17e..e8d08d2002 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10464,7 +10464,6 @@ fn parse_variadic_function_argument() { "SELECT * FROM pg_get_publication_tables(VARIADIC ARRAY['pub'::TEXT]) AS gpt", ); - // PostgreSQL accepts the marker on a named final argument too. let select = pg().verified_only_select("SELECT f(VARIADIC a => ARRAY[1])"); match expr_from_projection(only(&select.projection)) { Expr::Function(Function { @@ -10484,8 +10483,6 @@ fn parse_variadic_function_argument() { other => panic!("Expected a function call, found {other:?}"), } - // PostgreSQL reserves the keyword, so the marker holds even where an - // unreserved dialect would read `variadic - a` as an expression. let select = pg().verified_only_select("SELECT concat(VARIADIC -a)"); match expr_from_projection(only(&select.projection)) { Expr::Function(Function { @@ -10503,14 +10500,11 @@ fn parse_variadic_function_argument() { other => panic!("Expected a function call, found {other:?}"), } - // Since the keyword is reserved, it never passes a column of that name, - // matching PostgreSQL's own rejection of `SELECT f(variadic)`. assert_eq!( ParserError::ParserError("Expected: an expression, found: )".to_string()), pg().parse_sql_statements("SELECT f(variadic)").unwrap_err() ); - // The marker only applies to the final argument. let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); assert_eq!( ParserError::ParserError( @@ -10521,16 +10515,12 @@ fn parse_variadic_function_argument() { .unwrap_err() ); - // A comma introducing the table-function `SETTINGS` clause is no argument. all_dialects_where(|d| d.supports_variadic_function_argument() && d.supports_settings()) .verified_stmt("SELECT * FROM f(VARIADIC a, SETTINGS x = 1)"); - // A bare clause keyword is an operand, not the start of that clause. dialects.verified_stmt("SELECT f(VARIADIC separator)"); dialects.verified_stmt("SELECT f(VARIADIC returning)"); - // PostgreSQL's VARIADIC productions require expression operands, so no - // wildcard argument form is valid. for sql in ["SELECT f(VARIADIC *)", "SELECT f(VARIADIC a => *)"] { assert_eq!( ParserError::ParserError("A VARIADIC argument cannot be a wildcard".to_string()), @@ -10540,8 +10530,6 @@ fn parse_variadic_function_argument() { } dialects.verified_stmt("SELECT f(*)"); - // Dialects without the marker read `VARIADIC` as an identifier, so the - // argument that follows it has nowhere to go. let unsupported = all_dialects_except(|d| d.supports_variadic_function_argument()); assert_eq!( ParserError::ParserError("Expected: ), found: a".to_string()), @@ -10550,8 +10538,6 @@ fn parse_variadic_function_argument() { .unwrap_err() ); - // PostgreSQL has no production pairing the marker with a duplicate - // treatment, so `count(DISTINCT VARIADIC ARRAY[1])` is a syntax error there. for sql in [ "SELECT count(DISTINCT VARIADIC ARRAY[1])", "SELECT count(ALL VARIADIC ARRAY[1])", @@ -10565,8 +10551,6 @@ fn parse_variadic_function_argument() { ); } - // The rejection points at the duplicate treatment it names, even when - // earlier arguments precede the marker. assert_eq!( ParserError::ParserError( "A VARIADIC argument cannot be combined with DISTINCT or ALL at Line: 1, Column: 14" @@ -10579,26 +10563,20 @@ fn parse_variadic_function_argument() { .unwrap_err() ); - // A column of that name still may, where the keyword is unreserved. all_dialects_where(|d| { d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) }) .verified_stmt("SELECT count(DISTINCT variadic)"); } -/// Dialects that accept the marker without reserving `VARIADIC`, such as -/// `GenericDialect`, must still read `variadic` as an ordinary identifier -/// wherever it stands as an argument on its own. #[test] fn parse_variadic_as_identifier_function_argument() { let dialects = all_dialects_where(|d| { d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) }); for sql in [ - "SELECT f(variadic)", "SELECT f(variadic, x)", "SELECT f(x, variadic)", - "SELECT f(variadic + 1)", "SELECT f(variadic.x)", "SELECT f(variadic[1])", "SELECT f(variadic(1))", @@ -10611,8 +10589,7 @@ fn parse_variadic_as_identifier_function_argument() { dialects.verified_stmt(sql); } - // Display alone cannot tell `Unnamed(variadic + 1)` from - // `Variadic(Unnamed(+1))` apart from spelling, so assert the shape. + // `Display` cannot distinguish a column expression from a `VARIADIC` marker. for (sql, expected) in [ ( "SELECT f(variadic)", @@ -10639,19 +10616,14 @@ fn parse_variadic_as_identifier_function_argument() { } } - // The marker still applies when the ordinary reading cannot stand alone. dialects.verified_stmt("SELECT f(VARIADIC a)"); } -/// Where the ordinary reading of the argument stands alone and a clause -/// follows it, the plain reading is kept and the clause is parsed as usual. #[test] fn parse_variadic_plain_reading_before_argument_clauses() { let dialects = all_dialects_where(|d| { d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) }); - // A token that cannot extend the expression leaves the marker reading, - // while clause keywords keep the plain reading. for sql in [ "SELECT f(VARIADIC 5)", "SELECT f(variadic IGNORE NULLS)", @@ -10665,7 +10637,6 @@ fn parse_variadic_plain_reading_before_argument_clauses() { dialects.verified_stmt(sql); } - // The plain argument keeps its identifier shape next to the clause. let select = dialects.verified_only_select("SELECT f(variadic IGNORE NULLS)"); match expr_from_projection(only(&select.projection)) { Expr::Function(Function { @@ -10689,27 +10660,6 @@ fn parse_variadic_plain_reading_before_argument_clauses() { } } -/// The marker is rejected on non-final arguments in table function argument -/// lists and `LATERAL` function calls as well. -#[test] -fn parse_variadic_not_last_in_table_function_argument_lists() { - let dialects = all_dialects_where(|d| d.supports_variadic_function_argument()); - for sql in [ - "SELECT * FROM f(VARIADIC a, b)", - "SELECT * FROM LATERAL f(VARIADIC a, b)", - ] { - assert_eq!( - ParserError::ParserError( - "A VARIADIC argument must be the last function argument".to_string() - ), - dialects.parse_sql_statements(sql).unwrap_err(), - "{sql}" - ); - } -} - -/// A recursion limit exhausted inside the speculative plain reading is -/// propagated rather than treated as a failed plain reading. #[test] fn parse_variadic_recursion_limit_in_plain_reading() { let result = Parser::new(&GenericDialect {}) @@ -10722,7 +10672,6 @@ fn parse_variadic_recursion_limit_in_plain_reading() { #[test] fn parse_pg_publication_tables_view_definition() { - // `pg_get_viewdef('pg_publication_tables'::regclass, true)` on PostgreSQL 18. let sql = r#" SELECT p.pubname, n.nspname AS schemaname, @@ -10760,7 +10709,8 @@ fn variadic_not_last_error_points_to_keyword() { ), ] { assert_eq!( - Parser::parse_sql(&sqlparser::dialect::PostgreSqlDialect {}, sql), + all_dialects_where(|d| d.supports_variadic_function_argument()) + .run_parser_method(sql, |p| p.parse_statements()), Err(ParserError::ParserError(format!( "A VARIADIC argument must be the last function argument at Line: {line}, Column: {column}", ))), @@ -11219,3 +11169,36 @@ fn parse_create_aggregate_rejects_output_arguments_and_defaults() { ); } } + +#[test] +fn parse_variadic_identifier_and_named_arguments_at_eof() { + let dialect = GenericDialect {}; + for sql in ["variadic", "variadic => 1"] { + let expected = Parser::new(&dialect) + .try_with_sql(&format!("{sql})")) + .unwrap() + .parse_function_args(); + let actual = Parser::new(&dialect) + .try_with_sql(sql) + .unwrap() + .parse_function_args(); + assert_eq!(actual, expected, "{sql}"); + } +} + +#[test] +fn parse_variadic_binary_argument_at_eof() { + assert_eq!( + Parser::new(&GenericDialect {}) + .try_with_sql("variadic + 1") + .unwrap() + .parse_function_args(), + Ok(FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("variadic"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::value(number("1"))), + } + ))) + ); +}