Postgres: Scope colon and AS-named function arguments to their functions
diff --git a/derive/src/dialect.rs b/derive/src/dialect.rs index af17067..aadfaef 100644 --- a/derive/src/dialect.rs +++ b/derive/src/dialect.rs
@@ -229,7 +229,9 @@ const _: () = { use ::core::iter::Peekable; use ::core::str::Chars; - use sqlparser::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement}; + use sqlparser::ast::{ + ColumnOption, Expr, GranteesType, Ident, ObjectName, ObjectNamePart, Statement, + }; use sqlparser::dialect::{Dialect, Precedence}; use sqlparser::keywords::Keyword; use sqlparser::parser::{Parser, ParserError};
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 2beb565..cbb50bc 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs
@@ -132,7 +132,7 @@ #[cfg(feature = "derive-dialect")] pub use sqlparser_derive::derive_dialect; -use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement}; +use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectName, ObjectNamePart, Statement}; pub use crate::keywords; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -481,6 +481,14 @@ false } + /// Returns true if `fn_name` supports named arguments of the form + /// `FUN(a : '1', b : '2')`. Defaults to + /// [`Dialect::supports_named_fn_args_with_colon_operator`], ignoring `fn_name`. + fn supports_named_fn_args_with_colon_operator_for(&self, fn_name: Option<&ObjectName>) -> bool { + let _ = fn_name; + self.supports_named_fn_args_with_colon_operator() + } + /// Returns true if the dialect supports named arguments of the form `FUN(a := '1', b := '2')`. fn supports_named_fn_args_with_assignment_operator(&self) -> bool { false @@ -1934,6 +1942,13 @@ false } + /// Returns true if `fn_name` supports `FUN(a AS x)`-style aliased arguments. + /// Defaults to [`Dialect::supports_aliased_function_args`], ignoring `fn_name`. + fn supports_aliased_function_args_for(&self, fn_name: Option<&ObjectName>) -> bool { + let _ = fn_name; + self.supports_aliased_function_args() + } + /// Returns true if the dialect supports `USING <format>` in `CREATE TABLE`. /// /// Example:
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 3bec6ce..bc49f4b 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs
@@ -28,6 +28,7 @@ // limitations under the License. use log::debug; +use crate::ast::ObjectName; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -261,8 +262,8 @@ /// ```sql /// SELECT json_object('a': 'b') /// ``` - fn supports_named_fn_args_with_colon_operator(&self) -> bool { - true + fn supports_named_fn_args_with_colon_operator_for(&self, fn_name: Option<&ObjectName>) -> bool { + fn_name.is_some_and(|name| Parser::is_simple_unquoted_object_name(name, "json_object")) } /// See <https://www.postgresql.org/docs/current/functions-json.html> @@ -357,8 +358,12 @@ true } - fn supports_aliased_function_args(&self) -> bool { - true + fn supports_aliased_function_args_for(&self, fn_name: Option<&ObjectName>) -> bool { + fn_name.is_some_and(|name| { + ["XMLFOREST", "XMLELEMENT", "XMLTABLE"] + .iter() + .any(|f| Parser::is_simple_unquoted_object_name(name, f)) + }) } /// Postgres supports query optimizer hints via the `pg_hint_plan` extension,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1a46c6e..fb4f51a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs
@@ -1745,7 +1745,7 @@ /// Returns true if the given [ObjectName] is a single unquoted /// identifier matching `expected` (case-insensitive). - fn is_simple_unquoted_object_name(name: &ObjectName, expected: &str) -> bool { + pub(crate) fn is_simple_unquoted_object_name(name: &ObjectName, expected: &str) -> bool { if let [ObjectNamePart::Identifier(ident)] = name.0.as_slice() { ident.quote_style.is_none() && ident.value.eq_ignore_ascii_case(expected) } else { @@ -2585,7 +2585,7 @@ { self.parse_xmlparse_argument_list()? } else { - self.parse_function_argument_list()? + self.parse_function_argument_list(&name)? }; let mut parameters = FunctionArguments::None; // ClickHouse aggregations support parametric functions like `HISTOGRAM(0.5, 0.6)(x, y)` @@ -2594,7 +2594,7 @@ && self.consume_token(&Token::LParen) { parameters = FunctionArguments::List(args); - args = self.parse_function_argument_list()?; + args = self.parse_function_argument_list(&name)?; } let within_group = if self.parse_keywords(&[Keyword::WITHIN, Keyword::GROUP]) { @@ -2673,7 +2673,7 @@ /// Parse time-related function `name` possibly followed by `(...)` arguments. pub fn parse_time_functions(&mut self, name: ObjectName) -> Result<Expr, ParserError> { let args = if self.consume_token(&Token::LParen) { - FunctionArguments::List(self.parse_function_argument_list()?) + FunctionArguments::List(self.parse_function_argument_list(&name)?) } else { FunctionArguments::None }; @@ -19027,6 +19027,15 @@ /// Parse a single function argument, handling named and unnamed variants. pub fn parse_function_args(&mut self) -> Result<FunctionArg, ParserError> { + self.parse_function_args_for(None) + } + + /// Like [`Parser::parse_function_args`], but `fn_name`, when known, scopes + /// dialect-specific named-argument syntax to the functions that define it. + fn parse_function_args_for( + &mut self, + fn_name: Option<&ObjectName>, + ) -> Result<FunctionArg, ParserError> { // 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 (…`. @@ -19035,7 +19044,7 @@ // A wildcard is never a named-arg name; only the unnamed form applies. if !matches!(expr, Expr::Wildcard(_) | Expr::QualifiedWildcard(..)) { if let Some(operator) = - self.maybe_parse(|p| p.parse_function_named_arg_operator())? + self.maybe_parse(|p| p.parse_function_named_arg_operator(fn_name))? { let arg = self.parse_wildcard_expr()?.into(); return Ok(FunctionArg::ExprNamed { @@ -19047,13 +19056,13 @@ } let arg_expr = self.function_arg_expr_from_wildcard(expr)?; return Ok(FunctionArg::Unnamed( - self.maybe_parse_aliased_function_arg(arg_expr)?, + self.maybe_parse_aliased_function_arg(arg_expr, fn_name)?, )); } let arg = self.maybe_parse(|p| { let name = p.parse_identifier()?; - let operator = p.parse_function_named_arg_operator()?; + let operator = p.parse_function_named_arg_operator(fn_name)?; let arg = p.parse_wildcard_expr()?.into(); Ok(FunctionArg::Named { name, @@ -19067,7 +19076,7 @@ let wildcard_expr = self.parse_wildcard_expr()?; let arg_expr = self.function_arg_expr_from_wildcard(wildcard_expr)?; Ok(FunctionArg::Unnamed( - self.maybe_parse_aliased_function_arg(arg_expr)?, + self.maybe_parse_aliased_function_arg(arg_expr, fn_name)?, )) } @@ -19101,10 +19110,11 @@ fn maybe_parse_aliased_function_arg( &mut self, arg_expr: FunctionArgExpr, + fn_name: Option<&ObjectName>, ) -> Result<FunctionArgExpr, ParserError> { Ok(match arg_expr { FunctionArgExpr::Expr(expr) - if self.dialect.supports_aliased_function_args() + if self.dialect.supports_aliased_function_args_for(fn_name) && self.parse_keyword(Keyword::AS) => { FunctionArgExpr::Expr(Expr::Named { @@ -19116,7 +19126,10 @@ }) } - fn parse_function_named_arg_operator(&mut self) -> Result<FunctionArgOperator, ParserError> { + fn parse_function_named_arg_operator( + &mut self, + fn_name: Option<&ObjectName>, + ) -> Result<FunctionArgOperator, ParserError> { if self.parse_keyword(Keyword::VALUE) { return Ok(FunctionArgOperator::Value); } @@ -19135,7 +19148,11 @@ { Ok(FunctionArgOperator::Assignment) } - Token::Colon if self.dialect.supports_named_fn_args_with_colon_operator() => { + Token::Colon + if self + .dialect + .supports_named_fn_args_with_colon_operator_for(fn_name) => + { Ok(FunctionArgOperator::Colon) } _ => { @@ -19185,7 +19202,10 @@ /// FIRST_VALUE(x ORDER BY 1,2,3); /// FIRST_VALUE(x IGNORE NULL); /// ``` - fn parse_function_argument_list(&mut self) -> Result<FunctionArgumentList, ParserError> { + fn parse_function_argument_list( + &mut self, + fn_name: &ObjectName, + ) -> Result<FunctionArgumentList, ParserError> { let mut clauses = vec![]; // Handle clauses that may exist with an empty argument list @@ -19209,7 +19229,7 @@ } let duplicate_treatment = self.parse_duplicate_treatment()?; - let args = self.parse_comma_separated(Parser::parse_function_args)?; + let args = self.parse_comma_separated(|p| p.parse_function_args_for(Some(fn_name)))?; 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 fb99851..09809e3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs
@@ -4145,6 +4145,25 @@ } #[test] +fn parse_named_fn_args_scoped_to_their_functions() { + assert_eq!( + pg().parse_sql_statements("SELECT abs(a:b) FROM t") + .unwrap_err() + .to_string(), + "sql parser error: Expected: ), found: :" + ); + assert_eq!( + pg().parse_sql_statements("SELECT abs(a AS x) FROM t") + .unwrap_err() + .to_string(), + "sql parser error: Expected: ), found: AS" + ); + + pg().verified_expr("JSON_OBJECT('a' : 'b')"); + pg_and_generic().verified_only_select("SELECT XMLELEMENT(a AS x, b)"); +} + +#[test] fn parse_xmlparse() { // The parser only distinguishes the two modes, so the corpus covers those // plus a non-literal argument.