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) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e798524c9..2a332a61e0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8138,6 +8138,8 @@ pub enum FunctionArg { }, /// An unnamed argument (positional), given by expression or wildcard. Unnamed(FunctionArgExpr), + /// A final argument prefixed with PostgreSQL's [`VARIADIC`](https://www.postgresql.org/docs/current/xfunc-sql.html#XFUNC-SQL-VARIADIC-FUNCTIONS) marker. + Variadic(Box), } impl fmt::Display for FunctionArg { @@ -8154,6 +8156,7 @@ impl fmt::Display for FunctionArg { operator, } => fmt_named_function_arg(f, name, operator, arg), FunctionArg::Unnamed(unnamed_arg) => write!(f, "{unnamed_arg}"), + FunctionArg::Variadic(arg) => write!(f, "VARIADIC {arg}"), } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7638d7d6c0..90a467d113 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2215,6 +2215,7 @@ impl Spanned for FunctionArg { arg, operator: _, } => name.span().union(&arg.span()), + FunctionArg::Variadic(arg) => arg.span(), } } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 6aae6acaea..dd551857f5 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -357,6 +357,10 @@ impl Dialect for GenericDialect { true } + fn supports_variadic_function_argument(&self) -> bool { + true + } + fn supports_alter_column_position(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c867b76f61..4fa6cda8f8 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2127,6 +2127,11 @@ pub trait Dialect: Debug + Any { false } + /// Whether the dialect supports `VARIADIC` on the final function-call argument. + fn supports_variadic_function_argument(&self) -> bool { + false + } + /// Returns true if the dialect supports `USING ` in `CREATE TABLE`. /// /// Example: diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9e5257b30f..556dab7248 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -257,10 +257,10 @@ 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, + Keyword::VARIADIC => true, + _ => RESERVED_FOR_IDENTIFIER.contains(&kw), } } @@ -553,6 +553,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_variadic_function_argument(&self) -> bool { + true + } + /// Postgres supports query optimizer hints via the `pg_hint_plan` extension, /// using the same comment-prefixed-with-`+` syntax as MySQL and Oracle. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..8a783695f7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19494,8 +19494,81 @@ impl<'a> Parser<'a> { } } - /// Parse a single function argument, handling named and unnamed variants. + /// 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 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); + } + self.index = index; + } + } + + let loc = self.peek_token_ref().span.start; + self.advance_token(); + let arg = self.parse_plain_function_args()?; + let carries_wildcard = matches!( + &arg, + FunctionArg::Unnamed(arg) + | FunctionArg::Named { arg, .. } + | FunctionArg::ExprNamed { arg, .. } + if !matches!(arg, FunctionArgExpr::Expr(_)) + ); + 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() + } + + 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 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) + } + + /// 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 | Token::EOF => 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), + Keyword::ON => next_is(Keyword::OVERFLOW), + Keyword::ABSENT | Keyword::NULL => next_is(Keyword::ON), + Keyword::HAVING => next_is(Keyword::MIN) || next_is(Keyword::MAX), + Keyword::LIMIT | Keyword::RETURNING | Keyword::SEPARATOR | Keyword::WHERE => { + !matches!(next, Token::Comma | Token::RParen) + } + _ => false, + } + } + + /// 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 // path is O(2^depth) on nested calls like `CAST(CASE (CAST(CASE (…`. @@ -19619,7 +19692,10 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::RParen) { Ok(vec![]) } else { - let args = self.parse_comma_separated(Parser::parse_function_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.expect_token(&Token::RParen)?; Ok(args) } @@ -19633,11 +19709,12 @@ 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; } @@ -19677,16 +19754,27 @@ 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) + })? }; + if duplicate_treatment.is_some() && matches!(args.last(), Some(FunctionArg::Variadic(_))) { + return parser_err!( + "A VARIADIC argument cannot be combined with DISTINCT or ALL", + loc + ); + } + if self.parse_keyword(Keyword::WHERE) { clauses.push(FunctionArgumentClause::Where(self.parse_expr()?)); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index aa9252125b..e8d08d2002 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -27,6 +27,7 @@ use sqlparser::ast::*; use sqlparser::dialect::{ Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, }; +use sqlparser::keywords::Keyword; use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::{Location, Span, Token, Tokenizer}; use test_utils::*; @@ -10455,6 +10456,299 @@ 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", + ); + + 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:?}"), + } + + 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:?}"), + } + + assert_eq!( + ParserError::ParserError("Expected: an expression, found: )".to_string()), + pg().parse_sql_statements("SELECT f(variadic)").unwrap_err() + ); + + 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() + ); + + all_dialects_where(|d| d.supports_variadic_function_argument() && d.supports_settings()) + .verified_stmt("SELECT * FROM f(VARIADIC a, SETTINGS x = 1)"); + + dialects.verified_stmt("SELECT f(VARIADIC separator)"); + dialects.verified_stmt("SELECT f(VARIADIC returning)"); + + 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(*)"); + + 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() + ); + + for sql in [ + "SELECT count(DISTINCT VARIADIC ARRAY[1])", + "SELECT count(ALL VARIADIC ARRAY[1])", + ] { + assert_eq!( + ParserError::ParserError( + "A VARIADIC argument cannot be combined with DISTINCT or ALL".to_string() + ), + dialects.parse_sql_statements(sql).unwrap_err(), + "{sql}" + ); + } + + assert_eq!( + 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() + ); + + all_dialects_where(|d| { + d.supports_variadic_function_argument() && !d.is_reserved_for_identifier(Keyword::VARIADIC) + }) + .verified_stmt("SELECT count(DISTINCT variadic)"); +} + +#[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, x)", + "SELECT f(x, variadic)", + "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` cannot distinguish a column expression from a `VARIADIC` marker. + 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:?}"), + } + } + + dialects.verified_stmt("SELECT f(VARIADIC a)"); +} + +#[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) + }); + 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); + } + + 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:?}"), + } +} + +#[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() { + 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!( + 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}", + ))), + "{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() @@ -10875,3 +11169,36 @@ fn parse_create_aggregate_rejects_output_arguments_and_defaults() { ); } } + +#[test] +fn parse_variadic_identifier_and_named_arguments_at_eof() { + let dialect = GenericDialect {}; + for sql in ["variadic", "variadic => 1"] { + let expected = Parser::new(&dialect) + .try_with_sql(&format!("{sql})")) + .unwrap() + .parse_function_args(); + let actual = Parser::new(&dialect) + .try_with_sql(sql) + .unwrap() + .parse_function_args(); + assert_eq!(actual, expected, "{sql}"); + } +} + +#[test] +fn parse_variadic_binary_argument_at_eof() { + assert_eq!( + Parser::new(&GenericDialect {}) + .try_with_sql("variadic + 1") + .unwrap() + .parse_function_args(), + Ok(FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("variadic"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::value(number("1"))), + } + ))) + ); +}