From 775114241585501547c1323e18c40d22755a93b5 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 257ba6ac77..8df63b64a6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8165,6 +8165,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 { @@ -8181,6 +8189,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 704b583b8b..26aa2fc28f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2271,6 +2271,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 6745bf8d11..57652a316b 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -365,6 +365,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 f296a2b452..9981fbc6c3 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2187,6 +2187,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 9d15a87c97..7be796a1b0 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), } } @@ -579,6 +581,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 58ab4ca112..354e4c6935 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19719,8 +19719,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 (…`. @@ -19845,6 +19937,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) } @@ -19867,6 +19960,7 @@ impl<'a> Parser<'a> { break None; } }; + Self::verify_variadic_argument_is_last(&args)?; self.expect_token(&Token::RParen)?; Ok(TableFunctionArgs { args, settings }) } @@ -19902,6 +19996,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() @@ -19911,6 +20006,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 7fb4bb23fe..fd5a383583 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -28,6 +28,7 @@ use sqlparser::dialect::{ Dialect, GenericDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, }; +use sqlparser::keywords::Keyword; use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::{Location, Span, Token, Tokenizer}; use test_utils::*; @@ -11671,3 +11672,214 @@ fn parse_drop_group_is_dialect_gated() { )) ); } + +#[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 265601cc43c163da2332f89f70a92d9b2fa47602 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 354e4c6935..237fe8eb18 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19746,16 +19746,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 fd5a383583..e3e946b25b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11860,6 +11860,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 50d4bf00356fe34cf25553182519cff071a937ff 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 237fe8eb18..4477ce5e35 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19765,6 +19765,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 @@ -19793,21 +19805,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(()), } } @@ -19937,8 +19954,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) } @@ -19952,16 +19972,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 }) } @@ -19999,15 +20020,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 e3e946b25b..3b712266a7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11960,3 +11960,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 268c8c3c53c60ef73cbda22e8e270414ee5afce4 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 | 1592 +++++++++++++++++------------------ 1 file changed, 796 insertions(+), 796 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3b712266a7..7333dc0ab6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10470,6 +10470,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() @@ -11069,458 +11412,144 @@ fn parse_create_foreign_data_wrapper_clause_order() { pg_and_generic().verified_stmt(sql); } } - -#[test] -fn operator_function_display_preserves_empty_operator_types() { - for (op_types, expected) in [(None, "FUNCTION 1 f"), (Some(vec![]), "FUNCTION 1 () f")] { - let item = OperatorFamilyItem::Function { - support_number: 1, - op_types, - function_name: ObjectName::from(vec![Ident::new("f")]), - argument_types: vec![], - }; - assert_eq!(item.to_string(), expected); - } -} - -#[test] -fn parse_alter_operator_family_order_by_followed_by_search() { - let sql = r#"ALTER OPERATOR FAMILY my_ops USING gist ADD OPERATOR 1 < (INT4, INT4) FOR ORDER BY sort_schema."sort family", OPERATOR 2 > (INT4, INT4) FOR SEARCH"#; - let Statement::AlterOperatorFamily(AlterOperatorFamily { - operation: AlterOperatorFamilyOperation::Add { items }, - .. - }) = pg_and_generic().verified_stmt(sql) - else { - panic!("Expected ALTER OPERATOR FAMILY"); - }; - assert_eq!( - items, - vec![ - OperatorFamilyItem::Operator { - strategy_number: 1, - operator_name: ObjectName::from(vec![Ident::new("<")]), - op_types: vec![DataType::Int4(None), DataType::Int4(None)], - purpose: Some(OperatorPurpose::ForOrderBy { - sort_family: ObjectName::from(vec![ - Ident::new("sort_schema"), - Ident::with_quote('"', "sort family"), - ]), - }), - }, - OperatorFamilyItem::Operator { - strategy_number: 2, - operator_name: ObjectName::from(vec![Ident::new(">")]), - op_types: vec![DataType::Int4(None), DataType::Int4(None)], - purpose: Some(OperatorPurpose::ForSearch), - }, - ] - ); -} - -#[test] -fn parse_role_signed_connection_limit() { - let minus_one = Expr::UnaryOp { - op: UnaryOperator::Minus, - expr: Box::new(Expr::value(number("1"))), - }; - - match pg().verified_stmt("CREATE ROLE a CONNECTION LIMIT -1") { - Statement::CreateRole(create_role) => { - assert_eq!(create_role.connection_limit.as_ref(), Some(&minus_one)); - } - _ => unreachable!(), - } - pg().verified_stmt("CREATE ROLE a CONNECTION LIMIT +5"); - - assert_eq!( - pg().one_statement_parses_to( - "ALTER ROLE a CONNECTION LIMIT -1", - "ALTER ROLE a WITH CONNECTION LIMIT -1", - ), - Statement::AlterRole { - name: Ident::new("a"), - operation: AlterRoleOperation::WithOptions { - options: vec![RoleOption::ConnectionLimit(minus_one)], - }, - } - ); - pg().verified_stmt("ALTER ROLE a WITH CONNECTION LIMIT +5"); - - assert_eq!( - pg().parse_sql_statements("CREATE ROLE a CONNECTION LIMIT -'x'") - .unwrap_err() - .to_string(), - "sql parser error: Expected: literal number, found: 'x'" - ); - assert_eq!( - pg().parse_sql_statements("ALTER ROLE a CONNECTION LIMIT -'x'") - .unwrap_err() - .to_string(), - "sql parser error: Expected: option, found: CONNECTION" - ); -} - -#[test] -fn parse_create_table_column_unique_nulls_distinct() { - let Statement::CreateTable(CreateTable { columns, .. }) = pg_and_generic().verified_stmt( - "CREATE TABLE t (u INT UNIQUE NULLS NOT DISTINCT, v INT UNIQUE NULLS DISTINCT, w INT UNIQUE)", - ) else { - panic!("Expected CREATE TABLE"); - }; - let nulls_distinct: Vec<_> = columns - .iter() - .map(|column| match &column.options[..] { - [ColumnOptionDef { - option: ColumnOption::Unique(constraint), - .. - }] => &constraint.nulls_distinct, - options => panic!("Expected one UNIQUE option, got {options:?}"), - }) - .collect(); - assert_eq!( - nulls_distinct, - vec![ - &NullsDistinctOption::NotDistinct, - &NullsDistinctOption::Distinct, - &NullsDistinctOption::None, - ] - ); - - pg_and_generic().verified_stmt( - "CREATE TABLE t (u INT CONSTRAINT k UNIQUE NULLS NOT DISTINCT DEFERRABLE NOT NULL)", - ); - - assert_eq!( - pg_and_generic() - .parse_sql_statements("CREATE TABLE t (u INT UNIQUE NULLS NOT)") - .unwrap_err(), - ParserError::ParserError("Expected: DISTINCT, found: )".to_string()) - ); -} - -#[test] -fn display_column_unique_nulls_distinct_propagates_formatter_errors() { - use core::fmt::{self, Write}; - - struct RejectingWriter; - - impl Write for RejectingWriter { - fn write_str(&mut self, value: &str) -> fmt::Result { - if value.contains("NULLS") { - Err(fmt::Error) - } else { - Ok(()) - } - } - } - - let stmt = pg_and_generic().verified_stmt("CREATE TABLE t (u INT UNIQUE NULLS NOT DISTINCT)"); - assert_eq!(write!(RejectingWriter, "{stmt}"), Err(fmt::Error)); -} - -#[test] -fn parse_alter_table_attach_partition_of() { - let sql = - "ALTER TABLE ONLY public.root ATTACH PARTITION public.part FOR VALUES FROM (0) TO (10)"; - let Statement::AlterTable(alter) = pg().verified_stmt(sql) else { - unreachable!() - }; - assert_eq!( - alter.operations, - vec![AlterTableOperation::AttachPartitionOf { - partition: ObjectName::from(vec![Ident::new("public"), Ident::new("part")]), - for_values: ForValues::From { - from: vec![PartitionBoundValue::Expr(Expr::value(number("0")))], - to: vec![PartitionBoundValue::Expr(Expr::value(number("10")))], - }, - }] - ); - - pg().verified_stmt("ALTER TABLE root ATTACH PARTITION part DEFAULT"); - pg().verified_stmt("ALTER TABLE root ATTACH PARTITION part FOR VALUES IN (1, 2)"); - // As written by pg_dump. - pg().one_statement_parses_to( - "ALTER TABLE ONLY public.hroot ATTACH PARTITION public.part4 FOR VALUES WITH (modulus 4, remainder 0)", - "ALTER TABLE ONLY public.hroot ATTACH PARTITION public.part4 FOR VALUES WITH (MODULUS 4, REMAINDER 0)", - ); - - for (sql, expected) in [ - ( - "ALTER TABLE root ATTACH PARTITION part", - "Expected: FOR, found: EOF", - ), - ( - "ALTER TABLE root ATTACH PARTITION", - "Expected: identifier, found: EOF", - ), - ( - "ALTER TABLE root ATTACH PARTITION part FOR VALUES IN ()", - "Expected: at least one value, found: )", - ), - ( - "ALTER TABLE root ATTACH PARTITION part FOR VALUES", - "Expected: IN, FROM, or WITH after FOR VALUES, found: EOF", - ), - ] { - assert_eq!( - pg().parse_sql_statements(sql).unwrap_err().to_string(), - format!("sql parser error: {expected}"), - "{sql}" - ); - } -} - -#[test] -fn parse_drop_foreign_table() { - match pg_and_generic().verified_stmt("DROP FOREIGN TABLE IF EXISTS ft, public.other CASCADE") { - Statement::Drop { - object_type, - if_exists, - names, - cascade, - restrict, - .. - } => { - assert_eq!(object_type, ObjectType::ForeignTable); - assert!(if_exists); - assert_eq!( - names, - vec![ - ObjectName::from(vec![Ident::new("ft")]), - ObjectName::from(vec![Ident::new("public"), Ident::new("other")]), - ] - ); - assert!(cascade); - assert!(!restrict); - } - _ => unreachable!(), + +#[test] +fn operator_function_display_preserves_empty_operator_types() { + for (op_types, expected) in [(None, "FUNCTION 1 f"), (Some(vec![]), "FUNCTION 1 () f")] { + let item = OperatorFamilyItem::Function { + support_number: 1, + op_types, + function_name: ObjectName::from(vec![Ident::new("f")]), + argument_types: vec![], + }; + assert_eq!(item.to_string(), expected); } - pg_and_generic().verified_stmt("DROP FOREIGN TABLE ft RESTRICT"); +} +#[test] +fn parse_alter_operator_family_order_by_followed_by_search() { + let sql = r#"ALTER OPERATOR FAMILY my_ops USING gist ADD OPERATOR 1 < (INT4, INT4) FOR ORDER BY sort_schema."sort family", OPERATOR 2 > (INT4, INT4) FOR SEARCH"#; + let Statement::AlterOperatorFamily(AlterOperatorFamily { + operation: AlterOperatorFamilyOperation::Add { items }, + .. + }) = pg_and_generic().verified_stmt(sql) + else { + panic!("Expected ALTER OPERATOR FAMILY"); + }; assert_eq!( - pg_and_generic() - .parse_sql_statements("DROP FOREIGN ft") - .unwrap_err() - .to_string(), - "sql parser error: Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: FOREIGN" + items, + vec![ + OperatorFamilyItem::Operator { + strategy_number: 1, + operator_name: ObjectName::from(vec![Ident::new("<")]), + op_types: vec![DataType::Int4(None), DataType::Int4(None)], + purpose: Some(OperatorPurpose::ForOrderBy { + sort_family: ObjectName::from(vec![ + Ident::new("sort_schema"), + Ident::with_quote('"', "sort family"), + ]), + }), + }, + OperatorFamilyItem::Operator { + strategy_number: 2, + operator_name: ObjectName::from(vec![Ident::new(">")]), + op_types: vec![DataType::Int4(None), DataType::Int4(None)], + purpose: Some(OperatorPurpose::ForSearch), + }, + ] ); - for (sql, expected) in [ - ("DROP FOREIGN TABLE", "Expected: identifier, found: EOF"), - ( - "DROP FOREIGN TABLE ft CASCADE RESTRICT", - "Cannot specify both CASCADE and RESTRICT in DROP", - ), - ] { - assert_eq!( - pg_and_generic() - .parse_sql_statements(sql) - .unwrap_err() - .to_string(), - format!("sql parser error: {expected}"), - "{sql}" - ); - } } #[test] -fn parse_comment_on_foreign_table() { - match pg_and_generic().verified_stmt("COMMENT ON FOREIGN TABLE public.ft IS 'remote orders'") { - Statement::Comment { - object_type, - object_name, - comment, - if_exists, - } => { - assert_eq!(object_type, CommentObject::ForeignTable); - assert_eq!( - object_name, - ObjectName::from(vec![Ident::new("public"), Ident::new("ft")]) - ); - assert_eq!(comment, Some("remote orders".to_string())); - assert!(!if_exists); +fn parse_role_signed_connection_limit() { + let minus_one = Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::value(number("1"))), + }; + + match pg().verified_stmt("CREATE ROLE a CONNECTION LIMIT -1") { + Statement::CreateRole(create_role) => { + assert_eq!(create_role.connection_limit.as_ref(), Some(&minus_one)); } _ => unreachable!(), } - pg_and_generic().verified_stmt("COMMENT ON FOREIGN TABLE ft IS NULL"); + pg().verified_stmt("CREATE ROLE a CONNECTION LIMIT +5"); assert_eq!( - pg_and_generic() - .parse_sql_statements("COMMENT ON FOREIGN ft IS 'x'") - .unwrap_err() - .to_string(), - "sql parser error: Expected: TABLE, found: ft" + pg().one_statement_parses_to( + "ALTER ROLE a CONNECTION LIMIT -1", + "ALTER ROLE a WITH CONNECTION LIMIT -1", + ), + Statement::AlterRole { + name: Ident::new("a"), + operation: AlterRoleOperation::WithOptions { + options: vec![RoleOption::ConnectionLimit(minus_one)], + }, + } ); + pg().verified_stmt("ALTER ROLE a WITH CONNECTION LIMIT +5"); + assert_eq!( - pg_and_generic() - .parse_sql_statements("COMMENT ON FOREIGN TABLE ft 'x'") + pg().parse_sql_statements("CREATE ROLE a CONNECTION LIMIT -'x'") .unwrap_err() .to_string(), - "sql parser error: Expected: IS, found: 'x'" + "sql parser error: Expected: literal number, found: 'x'" ); assert_eq!( - pg_and_generic() - .parse_sql_statements("COMMENT ON FOREIGN TABLE") + pg().parse_sql_statements("ALTER ROLE a CONNECTION LIMIT -'x'") .unwrap_err() .to_string(), - "sql parser error: Expected: identifier, found: EOF" + "sql parser error: Expected: option, found: CONNECTION" ); } #[test] -fn parse_drop_foreign_table_rejects_modifiers() { - for sql in [ - "DROP TEMPORARY FOREIGN TABLE ft", - "DROP FOREIGN TABLE ft PURGE", - "DROP FOREIGN TABLE ft ON t", - ] { - for dialect in all_dialects().dialects { - assert!( - Parser::parse_sql(dialect.as_ref(), sql).is_err(), - "{sql} with {dialect:?}" - ); - } - } - for sql in ["DROP FOREIGN TABLE ft PURGE", "DROP FOREIGN TABLE ft ON t"] { - assert_eq!( - pg_and_generic() - .parse_sql_statements(sql) - .unwrap_err() - .to_string(), - "sql parser error: Cannot specify TEMPORARY, PURGE, or ON in DROP FOREIGN TABLE", - "{sql}" - ); - } -} - -#[test] -fn parse_non_decimal_integer_literals() { - let sql = "SELECT 0x1F, 0X1F, 0o17, 0O17, 0b101, 0B101, 0x_1F, 0xFF_FF, 0xFFFFFFFFFFFFFFFFFFFF"; - let canonical = if cfg!(feature = "bigdecimal") { - "SELECT 31, 31, 15, 15, 5, 5, 31, 65535, 1208925819614629174706175" - } else { - sql +fn parse_create_table_column_unique_nulls_distinct() { + let Statement::CreateTable(CreateTable { columns, .. }) = pg_and_generic().verified_stmt( + "CREATE TABLE t (u INT UNIQUE NULLS NOT DISTINCT, v INT UNIQUE NULLS DISTINCT, w INT UNIQUE)", + ) else { + panic!("Expected CREATE TABLE"); }; - let select = pg().verified_only_select_with_canonical(sql, canonical); - for item in &select.projection { - assert!( - matches!( - item, - SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { - value: Value::Number(_, false), - .. - })) - ), - "{item:?}" - ); - } - + let nulls_distinct: Vec<_> = columns + .iter() + .map(|column| match &column.options[..] { + [ColumnOptionDef { + option: ColumnOption::Unique(constraint), + .. + }] => &constraint.nulls_distinct, + options => panic!("Expected one UNIQUE option, got {options:?}"), + }) + .collect(); assert_eq!( - expr_from_projection(&pg().verified_only_select("SELECT X'1F'").projection[0]), - &Expr::Value(Value::HexStringLiteral("1F".to_string()).with_empty_span()) + nulls_distinct, + vec![ + &NullsDistinctOption::NotDistinct, + &NullsDistinctOption::Distinct, + &NullsDistinctOption::None, + ] ); - for (sql, message) in [ - ("SELECT 0x", "Expected a hexadecimal digit after '0x'"), - ("SELECT 0Xg", "Expected a hexadecimal digit after '0X'"), - ("SELECT 0o8", "Expected an octal digit after '0o'"), - ("SELECT 0b2", "Expected a binary digit after '0b'"), - ("SELECT 0x_", "Expected a hexadecimal digit after '0x'"), - ("SELECT 0x__1", "Unexpected character '_'"), - ("SELECT 0x1_", "Unexpected character '_'"), - ] { - let err = pg().parse_sql_statements(sql).unwrap_err().to_string(); - assert!(err.contains(message), "{sql}: {err}"); - } -} - -#[cfg(feature = "bigdecimal")] -#[test] -fn parse_non_decimal_integer_token_with_invalid_digits() { - let err = Parser::new(&PostgreSqlDialect {}) - .with_tokens(vec![Token::Number("0xZZ".to_string(), false)]) - .parse_value() - .unwrap_err(); - assert_eq!( - err, - ParserError::ParserError("Could not parse '0xZZ' as an integer".to_string()) + pg_and_generic().verified_stmt( + "CREATE TABLE t (u INT CONSTRAINT k UNIQUE NULLS NOT DISTINCT DEFERRABLE NOT NULL)", ); -} - -#[test] -fn parse_create_function_begin_atomic() { - let sql = "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1; END"; - pg_and_generic().verified_stmt(sql); - match pg_and_generic().run_parser_method(sql, |parser| parser.parse_statement().unwrap()) { - Statement::CreateFunction(CreateFunction { - function_body: Some(CreateFunctionBody::BeginAtomic(body)), - .. - }) => { - assert_eq!( - body.statements, - vec![pg_and_generic().verified_stmt("SELECT 1")] - ); - assert_eq!( - body.span(), - Span::new(Location::new(1, 46), Location::new(1, 72)) - ); - } - stmt => panic!("unexpected statement: {stmt:?}"), - } - - for sql in [ - "CREATE FUNCTION f(a INT) RETURNS INT LANGUAGE sql IMMUTABLE BEGIN ATOMIC SELECT a; RETURN a + 1; END", - "CREATE OR REPLACE FUNCTION f(a INT) RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT CASE WHEN a > 0 THEN 1 END; END", - "CREATE FUNCTION f() RETURNS VOID LANGUAGE sql BEGIN ATOMIC END", - ] { - pg_and_generic().verified_stmt(sql); - } - pg_and_generic().one_statement_parses_to( - r#"CREATE OR REPLACE FUNCTION public.f(a integer) - RETURNS integer - LANGUAGE sql -BEGIN ATOMIC - INSERT INTO t VALUES (a); - RETURN a; -END"#, - "CREATE OR REPLACE FUNCTION public.f(a INTEGER) RETURNS INTEGER LANGUAGE sql BEGIN ATOMIC INSERT INTO t VALUES (a); RETURN a; END", + assert_eq!( + pg_and_generic() + .parse_sql_statements("CREATE TABLE t (u INT UNIQUE NULLS NOT)") + .unwrap_err(), + ParserError::ParserError("Expected: DISTINCT, found: )".to_string()) ); - - for (sql, err) in [ - ( - "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1;", - "Expected: END, found: EOF", - ), - ( - "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1 END", - "Expected: ;, found: END", - ), - ( - "CREATE FUNCTION f() RETURNS INT LANGUAGE sql RETURN 1 BEGIN ATOMIC SELECT 1; END", - "BEGIN ATOMIC specified more than once", - ), - ( - "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN SELECT 1; END", - "Expected: end of statement, found: BEGIN", - ), - ] { - assert_eq!( - pg_and_generic().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError(err.to_string()), - "{sql}" - ); - } } #[test] -fn display_create_function_begin_atomic_propagates_write_errors() { +fn display_column_unique_nulls_distinct_propagates_formatter_errors() { use core::fmt::{self, Write}; - struct RejectingWriter(&'static str); + struct RejectingWriter; impl Write for RejectingWriter { fn write_str(&mut self, value: &str) -> fmt::Result { - if value == self.0 { + if value.contains("NULLS") { Err(fmt::Error) } else { Ok(()) @@ -11528,95 +11557,56 @@ fn display_create_function_begin_atomic_propagates_write_errors() { } } - let stmt = pg_and_generic() - .verified_stmt("CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1; END"); - for rejected in [" BEGIN ATOMIC", " ", ";", " END"] { - let mut writer = RejectingWriter(rejected); - assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); - } -} - -#[test] -fn parse_named_argument_with_assignment_operator() { - let select = pg().verified_only_select("SELECT f(x := 1, y => 2)"); - let Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, .. }), - .. - }) = expr_from_projection(only(&select.projection)) - else { - panic!("expected a function call"); - }; - assert_eq!( - args, - &[ - FunctionArg::ExprNamed { - name: Expr::Identifier(Ident::new("x")), - arg: FunctionArgExpr::Expr(Expr::value(number("1"))), - operator: FunctionArgOperator::Assignment, - }, - FunctionArg::ExprNamed { - name: Expr::Identifier(Ident::new("y")), - arg: FunctionArgExpr::Expr(Expr::value(number("2"))), - operator: FunctionArgOperator::RightArrow, - }, - ] - ); - - assert_eq!( - pg().parse_sql_statements("SELECT x := 1") - .unwrap_err() - .to_string(), - "sql parser error: Expected: end of statement, found: :=" - ); + let stmt = pg_and_generic().verified_stmt("CREATE TABLE t (u INT UNIQUE NULLS NOT DISTINCT)"); + assert_eq!(write!(RejectingWriter, "{stmt}"), Err(fmt::Error)); } #[test] -fn parse_create_foreign_table_inherits() { - let sql = "CREATE FOREIGN TABLE child (id INT) INHERITS (parent, public.other) SERVER s OPTIONS (schema_name 'public')"; - let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { +fn parse_alter_table_attach_partition_of() { + let sql = + "ALTER TABLE ONLY public.root ATTACH PARTITION public.part FOR VALUES FROM (0) TO (10)"; + let Statement::AlterTable(alter) = pg().verified_stmt(sql) else { unreachable!() }; assert_eq!( - stmt.inherits, - Some(vec![ - ObjectName::from(vec![Ident::new("parent")]), - ObjectName::from(vec![Ident::new("public"), Ident::new("other")]), - ]) + alter.operations, + vec![AlterTableOperation::AttachPartitionOf { + partition: ObjectName::from(vec![Ident::new("public"), Ident::new("part")]), + for_values: ForValues::From { + from: vec![PartitionBoundValue::Expr(Expr::value(number("0")))], + to: vec![PartitionBoundValue::Expr(Expr::value(number("10")))], + }, + }] ); + pg().verified_stmt("ALTER TABLE root ATTACH PARTITION part DEFAULT"); + pg().verified_stmt("ALTER TABLE root ATTACH PARTITION part FOR VALUES IN (1, 2)"); // As written by pg_dump. - pg_and_generic().one_statement_parses_to( - "CREATE FOREIGN TABLE public.child (\n)\nINHERITS (public.parent)\nSERVER s", - "CREATE FOREIGN TABLE public.child () INHERITS (public.parent) SERVER s", + pg().one_statement_parses_to( + "ALTER TABLE ONLY public.hroot ATTACH PARTITION public.part4 FOR VALUES WITH (modulus 4, remainder 0)", + "ALTER TABLE ONLY public.hroot ATTACH PARTITION public.part4 FOR VALUES WITH (MODULUS 4, REMAINDER 0)", ); for (sql, expected) in [ ( - "CREATE FOREIGN TABLE child INHERITS (parent) SERVER s", - "Expected: '(' before the column list of CREATE FOREIGN TABLE, found: INHERITS", - ), - ( - "CREATE FOREIGN TABLE child () INHERITS () SERVER s", - "Expected: identifier, found: )", + "ALTER TABLE root ATTACH PARTITION part", + "Expected: FOR, found: EOF", ), ( - "CREATE FOREIGN TABLE child () INHERITS parent SERVER s", - "Expected: a list of columns in parentheses, found: parent", + "ALTER TABLE root ATTACH PARTITION", + "Expected: identifier, found: EOF", ), ( - "CREATE FOREIGN TABLE child () INHERITS (parent) OPTIONS (schema_name 'public')", - "Expected: SERVER, found: OPTIONS", + "ALTER TABLE root ATTACH PARTITION part FOR VALUES IN ()", + "Expected: at least one value, found: )", ), ( - "CREATE FOREIGN TABLE child () SERVER s INHERITS (parent)", - "Expected: end of statement, found: INHERITS", + "ALTER TABLE root ATTACH PARTITION part FOR VALUES", + "Expected: IN, FROM, or WITH after FOR VALUES, found: EOF", ), ] { assert_eq!( - pg_and_generic() - .parse_sql_statements(sql) - .unwrap_err() - .to_string(), + pg().parse_sql_statements(sql).unwrap_err().to_string(), format!("sql parser error: {expected}"), "{sql}" ); @@ -11624,394 +11614,404 @@ fn parse_create_foreign_table_inherits() { } #[test] -fn parse_create_foreign_table_inherits_span() { - let sql = "CREATE FOREIGN TABLE child (id INT) INHERITS (parent, public.other) SERVER s OPTIONS (schema_name 'public')"; - let mut parser = Parser::new(&PostgreSqlDialect {}) - .try_with_sql(sql) - .unwrap(); - let Statement::CreateForeignTable(stmt) = parser.parse_statement().unwrap() else { - unreachable!() - }; - assert_eq!( - stmt.span(), - Span::new(Location::new(1, 22), Location::new(1, 107)), - ); -} - -#[test] -fn display_create_foreign_table_inherits_propagates_formatter_errors() { - use core::fmt::{self, Write}; - - struct RejectingWriter(&'static str); - - impl Write for RejectingWriter { - fn write_str(&mut self, value: &str) -> fmt::Result { - if value == self.0 { - Err(fmt::Error) - } else { - Ok(()) - } - } - } - - let stmt = - pg_and_generic().verified_stmt("CREATE FOREIGN TABLE child () INHERITS (parent) SERVER s"); - for rejected in [")", " INHERITS (", " SERVER "] { - let mut writer = RejectingWriter(rejected); - assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); - } -} - -#[test] -fn parse_drop_group_is_dialect_gated() { - assert_eq!( - all_dialects_where(|d| !d.supports_drop_group()) - .parse_sql_statements("DROP GROUP staff"), - Err(ParserError::ParserError( - "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: GROUP".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, .. }), +fn parse_drop_foreign_table() { + match pg_and_generic().verified_stmt("DROP FOREIGN TABLE IF EXISTS ft, public.other CASCADE") { + Statement::Drop { + object_type, + if_exists, + names, + cascade, + restrict, .. - }) => 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:?}"), + } => { + assert_eq!(object_type, ObjectType::ForeignTable); + assert!(if_exists); + assert_eq!( + names, + vec![ + ObjectName::from(vec![Ident::new("ft")]), + ObjectName::from(vec![Ident::new("public"), Ident::new("other")]), + ] + ); + assert!(cascade); + assert!(!restrict); + } + _ => unreachable!(), } + pg_and_generic().verified_stmt("DROP FOREIGN TABLE ft RESTRICT"); - // 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)") + pg_and_generic() + .parse_sql_statements("DROP FOREIGN ft") .unwrap_err() + .to_string(), + "sql parser error: Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: FOREIGN" ); - - // 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 => *)"] { + for (sql, expected) in [ + ("DROP FOREIGN TABLE", "Expected: identifier, found: EOF"), + ( + "DROP FOREIGN TABLE ft CASCADE RESTRICT", + "Cannot specify both CASCADE and RESTRICT in DROP", + ), + ] { assert_eq!( - ParserError::ParserError("A VARIADIC argument cannot be a wildcard".to_string()), - dialects.parse_sql_statements(sql).unwrap_err(), + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), "{sql}" ); } - dialects.verified_stmt("SELECT f(*)"); +} + +#[test] +fn parse_comment_on_foreign_table() { + match pg_and_generic().verified_stmt("COMMENT ON FOREIGN TABLE public.ft IS 'remote orders'") { + Statement::Comment { + object_type, + object_name, + comment, + if_exists, + } => { + assert_eq!(object_type, CommentObject::ForeignTable); + assert_eq!( + object_name, + ObjectName::from(vec![Ident::new("public"), Ident::new("ft")]) + ); + assert_eq!(comment, Some("remote orders".to_string())); + assert!(!if_exists); + } + _ => unreachable!(), + } + pg_and_generic().verified_stmt("COMMENT ON FOREIGN TABLE ft IS NULL"); - // 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)") + pg_and_generic() + .parse_sql_statements("COMMENT ON FOREIGN ft IS 'x'") + .unwrap_err() + .to_string(), + "sql parser error: Expected: TABLE, found: ft" + ); + assert_eq!( + pg_and_generic() + .parse_sql_statements("COMMENT ON FOREIGN TABLE ft 'x'") + .unwrap_err() + .to_string(), + "sql parser error: Expected: IS, found: 'x'" + ); + assert_eq!( + pg_and_generic() + .parse_sql_statements("COMMENT ON FOREIGN TABLE") .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" ); +} - // PostgreSQL has no production pairing the marker with a duplicate - // treatment, so `count(DISTINCT VARIADIC ARRAY[1])` is a syntax error there. +#[test] +fn parse_drop_foreign_table_rejects_modifiers() { for sql in [ - "SELECT count(DISTINCT VARIADIC ARRAY[1])", - "SELECT count(ALL VARIADIC ARRAY[1])", + "DROP TEMPORARY FOREIGN TABLE ft", + "DROP FOREIGN TABLE ft PURGE", + "DROP FOREIGN TABLE ft ON t", ] { + for dialect in all_dialects().dialects { + assert!( + Parser::parse_sql(dialect.as_ref(), sql).is_err(), + "{sql} with {dialect:?}" + ); + } + } + for sql in ["DROP FOREIGN TABLE ft PURGE", "DROP FOREIGN TABLE ft ON t"] { assert_eq!( - ParserError::ParserError( - "A VARIADIC argument cannot be combined with DISTINCT or ALL".to_string() - ), - dialects.parse_sql_statements(sql).unwrap_err(), + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + "sql parser error: Cannot specify TEMPORARY, PURGE, or ON in DROP FOREIGN TABLE", "{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); +fn parse_non_decimal_integer_literals() { + let sql = "SELECT 0x1F, 0X1F, 0o17, 0O17, 0b101, 0B101, 0x_1F, 0xFF_FF, 0xFFFFFFFFFFFFFFFFFFFF"; + let canonical = if cfg!(feature = "bigdecimal") { + "SELECT 31, 31, 15, 15, 5, 5, 31, 65535, 1208925819614629174706175" + } else { + sql + }; + let select = pg().verified_only_select_with_canonical(sql, canonical); + for item in &select.projection { + assert!( + matches!( + item, + SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { + value: Value::Number(_, false), + .. + })) + ), + "{item:?}" + ); } - // 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"))), - })), - ), + assert_eq!( + expr_from_projection(&pg().verified_only_select("SELECT X'1F'").projection[0]), + &Expr::Value(Value::HexStringLiteral("1F".to_string()).with_empty_span()) + ); + + for (sql, message) in [ + ("SELECT 0x", "Expected a hexadecimal digit after '0x'"), + ("SELECT 0Xg", "Expected a hexadecimal digit after '0X'"), + ("SELECT 0o8", "Expected an octal digit after '0o'"), + ("SELECT 0b2", "Expected a binary digit after '0b'"), + ("SELECT 0x_", "Expected a hexadecimal digit after '0x'"), + ("SELECT 0x__1", "Unexpected character '_'"), + ("SELECT 0x1_", "Unexpected character '_'"), ] { - 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:?}"), - } + let err = pg().parse_sql_statements(sql).unwrap_err().to_string(); + assert!(err.contains(message), "{sql}: {err}"); } - - // 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. +#[cfg(feature = "bigdecimal")] #[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_non_decimal_integer_token_with_invalid_digits() { + let err = Parser::new(&PostgreSqlDialect {}) + .with_tokens(vec![Token::Number("0xZZ".to_string(), false)]) + .parse_value() + .unwrap_err(); + assert_eq!( + err, + ParserError::ParserError("Could not parse '0xZZ' as an integer".to_string()) + ); +} - // 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, .. }), +#[test] +fn parse_create_function_begin_atomic() { + let sql = "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1; END"; + pg_and_generic().verified_stmt(sql); + match pg_and_generic().run_parser_method(sql, |parser| parser.parse_statement().unwrap()) { + Statement::CreateFunction(CreateFunction { + function_body: Some(CreateFunctionBody::BeginAtomic(body)), .. }) => { assert_eq!( - &vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( - Expr::Identifier(Ident::new("variadic")) - ))], - args + body.statements, + vec![pg_and_generic().verified_stmt("SELECT 1")] ); assert_eq!( - &vec![FunctionArgumentClause::IgnoreOrRespectNulls( - NullTreatment::IgnoreNulls - )], - clauses + body.span(), + Span::new(Location::new(1, 46), Location::new(1, 72)) ); } - other => panic!("Expected a function call, found {other:?}"), + stmt => panic!("unexpected statement: {stmt:?}"), } -} -/// 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)", + "CREATE FUNCTION f(a INT) RETURNS INT LANGUAGE sql IMMUTABLE BEGIN ATOMIC SELECT a; RETURN a + 1; END", + "CREATE OR REPLACE FUNCTION f(a INT) RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT CASE WHEN a > 0 THEN 1 END; END", + "CREATE FUNCTION f() RETURNS VOID LANGUAGE sql BEGIN ATOMIC END", + ] { + pg_and_generic().verified_stmt(sql); + } + + pg_and_generic().one_statement_parses_to( + r#"CREATE OR REPLACE FUNCTION public.f(a integer) + RETURNS integer + LANGUAGE sql +BEGIN ATOMIC + INSERT INTO t VALUES (a); + RETURN a; +END"#, + "CREATE OR REPLACE FUNCTION public.f(a INTEGER) RETURNS INTEGER LANGUAGE sql BEGIN ATOMIC INSERT INTO t VALUES (a); RETURN a; END", + ); + + for (sql, err) in [ + ( + "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1;", + "Expected: END, found: EOF", + ), + ( + "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1 END", + "Expected: ;, found: END", + ), + ( + "CREATE FUNCTION f() RETURNS INT LANGUAGE sql RETURN 1 BEGIN ATOMIC SELECT 1; END", + "BEGIN ATOMIC specified more than once", + ), + ( + "CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN SELECT 1; END", + "Expected: end of statement, found: BEGIN", + ), ] { assert_eq!( - ParserError::ParserError( - "A VARIADIC argument must be the last function argument".to_string() - ), - dialects.parse_sql_statements(sql).unwrap_err(), + pg_and_generic().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(err.to_string()), "{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)); +fn display_create_function_begin_atomic_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value == self.0 { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let stmt = pg_and_generic() + .verified_stmt("CREATE FUNCTION f() RETURNS INT LANGUAGE sql BEGIN ATOMIC SELECT 1; END"); + for rejected in [" BEGIN ATOMIC", " ", ";", " END"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); + } } #[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_named_argument_with_assignment_operator() { + let select = pg().verified_only_select("SELECT f(x := 1, y => 2)"); + let Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) = expr_from_projection(only(&select.projection)) + else { + panic!("expected a function call"); + }; + assert_eq!( + args, + &[ + FunctionArg::ExprNamed { + name: Expr::Identifier(Ident::new("x")), + arg: FunctionArgExpr::Expr(Expr::value(number("1"))), + operator: FunctionArgOperator::Assignment, + }, + FunctionArg::ExprNamed { + name: Expr::Identifier(Ident::new("y")), + arg: FunctionArgExpr::Expr(Expr::value(number("2"))), + operator: FunctionArgOperator::RightArrow, + }, + ] + ); + + assert_eq!( + pg().parse_sql_statements("SELECT x := 1") + .unwrap_err() + .to_string(), + "sql parser error: Expected: end of statement, found: :=" + ); } #[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_create_foreign_table_inherits() { + let sql = "CREATE FOREIGN TABLE child (id INT) INHERITS (parent, public.other) SERVER s OPTIONS (schema_name 'public')"; + let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt.inherits, + Some(vec![ + ObjectName::from(vec![Ident::new("parent")]), + ObjectName::from(vec![Ident::new("public"), Ident::new("other")]), + ]) + ); + + // As written by pg_dump. + pg_and_generic().one_statement_parses_to( + "CREATE FOREIGN TABLE public.child (\n)\nINHERITS (public.parent)\nSERVER s", + "CREATE FOREIGN TABLE public.child () INHERITS (public.parent) SERVER s", + ); + + for (sql, expected) in [ ( - r#"SELECT f( - VARIADIC /* marker */ a, b)"#, - 2, - 3, + "CREATE FOREIGN TABLE child INHERITS (parent) SERVER s", + "Expected: '(' before the column list of CREATE FOREIGN TABLE, found: INHERITS", + ), + ( + "CREATE FOREIGN TABLE child () INHERITS () SERVER s", + "Expected: identifier, found: )", + ), + ( + "CREATE FOREIGN TABLE child () INHERITS parent SERVER s", + "Expected: a list of columns in parentheses, found: parent", + ), + ( + "CREATE FOREIGN TABLE child () INHERITS (parent) OPTIONS (schema_name 'public')", + "Expected: SERVER, found: OPTIONS", + ), + ( + "CREATE FOREIGN TABLE child () SERVER s INHERITS (parent)", + "Expected: end of statement, found: INHERITS", ), ] { 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_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{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() +fn parse_create_foreign_table_inherits_span() { + let sql = "CREATE FOREIGN TABLE child (id INT) INHERITS (parent, public.other) SERVER s OPTIONS (schema_name 'public')"; + let mut parser = Parser::new(&PostgreSqlDialect {}) + .try_with_sql(sql) .unwrap(); + let Statement::CreateForeignTable(stmt) = parser.parse_statement().unwrap() else { + unreachable!() + }; assert_eq!( - sqlparser::ast::Spanned::span(&arg), - sqlparser::tokenizer::Span::new( - sqlparser::tokenizer::Location::new(1, 10), - sqlparser::tokenizer::Location::new(1, 18), - ), + stmt.span(), + Span::new(Location::new(1, 22), Location::new(1, 107)), ); } #[test] -fn variadic_table_argument_preserves_operand_errors() { +fn display_create_foreign_table_inherits_propagates_formatter_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value == self.0 { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let stmt = + pg_and_generic().verified_stmt("CREATE FOREIGN TABLE child () INHERITS (parent) SERVER s"); + for rejected in [")", " INHERITS (", " SERVER "] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); + } +} + +#[test] +fn parse_drop_group_is_dialect_gated() { assert_eq!( - Parser::parse_sql( - &sqlparser::dialect::PostgreSqlDialect {}, - "SELECT * FROM LATERAL f(VARIADIC *)", - ), + all_dialects_where(|d| !d.supports_drop_group()) + .parse_sql_statements("DROP GROUP staff"), Err(ParserError::ParserError( - "A VARIADIC argument cannot be a wildcard at Line: 1, Column: 25".to_string(), - )), + "Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, GROUP, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: GROUP".to_string() + )) ); } From 620eef10325f3b40a5edf2046d48faf2a32ae849 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 d9c36d4e816956dcfe54c30b596b2e0375af17b9 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 8df63b64a6..987d978c54 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8165,13 +8165,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 9981fbc6c3..37835cc41c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2187,12 +2187,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 7be796a1b0..14b2385054 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 4477ce5e35..83d17c8ab2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19719,37 +19719,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) @@ -19771,20 +19758,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, }; @@ -19793,11 +19782,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) } @@ -19805,31 +19792,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 @@ -19958,7 +19921,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) } @@ -19982,7 +19944,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 }) } @@ -20031,9 +19992,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 7333dc0ab6..cbaa1e1ed5 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10478,7 +10478,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 { @@ -10498,8 +10497,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 { @@ -10517,14 +10514,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( @@ -10535,16 +10529,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()), @@ -10554,8 +10544,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()), @@ -10564,8 +10552,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])", @@ -10579,8 +10565,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" @@ -10593,26 +10577,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))", @@ -10625,8 +10603,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)", @@ -10653,19 +10630,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)", @@ -10679,7 +10651,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 { @@ -10703,27 +10674,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 {}) @@ -10736,7 +10686,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, @@ -10774,7 +10723,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}", ))), @@ -11361,6 +11311,22 @@ fn parse_drop_group_rejects_drop_behaviors() { } } +#[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 foreign_data_wrapper_display_and_span() { let ident = |value: &str, start: u64, end: u64| Ident { @@ -11413,6 +11379,23 @@ fn parse_create_foreign_data_wrapper_clause_order() { } } +#[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"))), + } + ))) + ); +} + #[test] fn operator_function_display_preserves_empty_operator_types() { for (op_types, expected) in [(None, "FUNCTION 1 f"), (Some(vec![]), "FUNCTION 1 () f")] {