From ed5324dbff39564c50b0b64727a384003df57958 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:14:52 +0200 Subject: [PATCH 1/3] `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 eb5bc6cc75..55e433a306 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8116,6 +8116,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 { @@ -8132,6 +8140,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 8267c71798..b4ff07dbae 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2209,6 +2209,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 f6a6682ec9..440074a0e5 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -333,6 +333,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 3ce71eb0be..c7f2331afa 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2027,6 +2027,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 c060f2a8c1..af9f498362 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -253,10 +253,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), } } @@ -541,6 +543,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 276b252ed2..4896ca96b9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19229,8 +19229,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 (…`. @@ -19355,6 +19447,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) } @@ -19377,6 +19470,7 @@ impl<'a> Parser<'a> { break None; } }; + Self::verify_variadic_argument_is_last(&args)?; self.expect_token(&Token::RParen)?; Ok(TableFunctionArgs { args, settings }) } @@ -19412,8 +19506,18 @@ impl<'a> Parser<'a> { }); } + let loc = self.peek_token_ref().span.start; let duplicate_treatment = self.parse_duplicate_treatment()?; let args = 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 0a3866d29f..b912bf714e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -25,6 +25,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect}; +use sqlparser::keywords::Keyword; use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::{Location, Span}; use test_utils::*; @@ -10265,3 +10266,214 @@ fn parse_geometric_typed_literal_error_propagates() { ParserError::ParserError("Expected: a value, found: (".to_string()) ); } + +#[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 11ce4a5ceddffb5c3ac408420e9bb3a6bffa0413 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 12:51:21 +0200 Subject: [PATCH 2/3] 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 4896ca96b9..cc02f349e9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19256,16 +19256,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 b912bf714e..b6662b2235 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10454,6 +10454,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 6f9c01727cf5356700b359d21b65a33f0f1b04b6 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 17:55:59 +0200 Subject: [PATCH 3/3] 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 cc02f349e9..3b89407c97 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19275,6 +19275,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 @@ -19303,21 +19315,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(()), } } @@ -19447,8 +19464,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) } @@ -19462,16 +19482,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 }) } @@ -19509,8 +19530,11 @@ impl<'a> Parser<'a> { let loc = self.peek_token_ref().span.start; let duplicate_treatment = self.parse_duplicate_treatment()?; - 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)?; // 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 b6662b2235..3eff4c8638 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10554,3 +10554,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(), + )), + ); +}