You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/08/24 07:57:44 UTC
(datafusion-sqlparser-rs) 01/01: Keep the quoting and span of a function argument name (#2447)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-2447-e436249679dfc43872c3afbedb5ec120064f2491
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit 2f1c4869f5809567472b0ac663a42bb5efa28ec8
Author: Luca Cappelletti <ca...@gmail.com>
AuthorDate: Mon Aug 24 07:57:19 2026 +0000
Keep the quoting and span of a function argument name (#2447)
---
src/parser/mod.rs | 16 ++---
tests/sqlparser_mssql.rs | 16 +++++
tests/sqlparser_postgres.rs | 144 +++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 166 insertions(+), 10 deletions(-)
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 272de79d..a20f79db 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -6268,14 +6268,14 @@ impl<'a> Parser<'a> {
}
if let Some(next_data_type) = self.maybe_parse(parse_data_type_no_default)? {
- let token = self.token_at(data_type_idx);
+ let token = self.token_at(data_type_idx).clone();
// We ensure that the token is a `Word` token, and not other special tokens.
- if !matches!(token.token, Token::Word(_)) {
- return self.expected("a name or type", token.clone());
+ match token.token {
+ Token::Word(word) => name = Some(word.into_ident(token.span)),
+ _ => return self.expected("a name or type", token),
}
- name = Some(Ident::new(token.to_string()));
data_type = next_data_type;
}
@@ -6332,12 +6332,12 @@ impl<'a> Parser<'a> {
}
if let Some(next_data_type) = self.maybe_parse(parse_data_type_for_aggregate_arg)? {
- let token = self.token_at(data_type_idx);
- if !matches!(token.token, Token::Word(_)) {
- return self.expected("a name or type", token.clone());
+ let token = self.token_at(data_type_idx).clone();
+ match token.token {
+ Token::Word(word) => name = Some(word.into_ident(token.span)),
+ _ => return self.expected("a name or type", token),
}
- name = Some(Ident::new(token.to_string()));
data_type = next_data_type;
}
diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs
index 3faf56f0..4510f953 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -2925,3 +2925,19 @@ fn parse_mssql_money_constants() {
expr_from_projection(only(&select.projection)),
);
}
+
+#[test]
+fn parse_bracket_quoted_function_argument_name() {
+ let Statement::DropFunction(drop) = ms().verified_stmt("DROP FUNCTION f([Role] INT)") else {
+ panic!("expected a DROP FUNCTION statement");
+ };
+ assert_eq!(
+ drop.func_desc[0].args,
+ Some(vec![OperateFunctionArg {
+ mode: None,
+ name: Some(Ident::with_quote('[', "Role")),
+ data_type: Int(None),
+ default_expr: None,
+ }])
+ );
+}
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index c36dd9a9..9b1460cd 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -25,8 +25,8 @@ mod test_utils;
use helpers::attached_token::AttachedToken;
use sqlparser::ast::*;
use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
-use sqlparser::parser::ParserError;
-use sqlparser::tokenizer::Span;
+use sqlparser::parser::{Parser, ParserError};
+use sqlparser::tokenizer::{Location, Span};
use test_utils::*;
#[test]
@@ -9678,3 +9678,143 @@ fn parse_right_deep_join_chain() {
// NATURAL JOIN followed by a constrained join must stay left-associative.
pg().verified_stmt("SELECT * FROM t0 NATURAL JOIN t1 INNER JOIN t2 ON true");
}
+
+#[test]
+fn parse_quoted_function_argument_name() {
+ let statement = pg_and_generic().verified_stmt(
+ r#"CREATE FUNCTION is_member("Role" TEXT) RETURNS BOOLEAN LANGUAGE SQL AS 'SELECT true'"#,
+ );
+ let Statement::CreateFunction(function) = statement else {
+ panic!("expected a CREATE FUNCTION statement");
+ };
+ assert_eq!(
+ function.args,
+ Some(vec![OperateFunctionArg {
+ mode: None,
+ name: Some(Ident::with_quote('"', "Role")),
+ data_type: DataType::Text,
+ default_expr: None,
+ }])
+ );
+
+ // An embedded quote is doubled in the input and belongs to the name once.
+ let statement = pg_and_generic().verified_stmt(
+ r#"CREATE FUNCTION f("we""ird" INT) RETURNS BOOLEAN LANGUAGE SQL RETURN true"#,
+ );
+ let Statement::CreateFunction(function) = statement else {
+ panic!("expected a CREATE FUNCTION statement");
+ };
+ assert_eq!(
+ function.args,
+ Some(vec![OperateFunctionArg {
+ mode: None,
+ name: Some(Ident::with_quote('"', r#"we"ird"#)),
+ data_type: DataType::Int(None),
+ default_expr: None,
+ }])
+ );
+
+ // A name outside ASCII is quoted for the same reason and survives the same way.
+ pg_and_generic().verified_stmt(
+ r#"CREATE FUNCTION f("RĂ´le" TEXT) RETURNS BOOLEAN LANGUAGE SQL RETURN true"#,
+ );
+}
+
+#[test]
+fn parse_quoted_function_argument_name_span() {
+ let sql =
+ r#"CREATE FUNCTION is_member("Role" TEXT) RETURNS BOOLEAN LANGUAGE SQL AS 'SELECT true'"#;
+ // Parsed directly rather than through the test helpers, which tokenize
+ // without locations.
+ let mut statements = Parser::parse_sql(&PostgreSqlDialect {}, sql).unwrap();
+ let Some(Statement::CreateFunction(function)) = statements.pop() else {
+ panic!("expected a CREATE FUNCTION statement");
+ };
+ let name = function.args.as_ref().unwrap()[0].name.as_ref().unwrap();
+ assert_eq!(
+ name.span,
+ Span::new(Location::new(1, 27), Location::new(1, 33)),
+ "the span covers the quoted name in the input"
+ );
+}
+
+#[test]
+fn parse_function_argument_modes_and_defaults_keep_quoted_names() {
+ let sql = r#"CREATE FUNCTION f(IN "A" INT = 1, OUT "B" TEXT, INOUT "C" BOOLEAN, VARIADIC "D" INT[]) RETURNS INT LANGUAGE SQL AS 'x'"#;
+ let Statement::CreateFunction(function) = pg_and_generic().verified_stmt(sql) else {
+ panic!("expected a CREATE FUNCTION statement");
+ };
+ assert_eq!(
+ function.args,
+ Some(vec![
+ OperateFunctionArg {
+ mode: Some(ArgMode::In),
+ name: Some(Ident::with_quote('"', "A")),
+ data_type: DataType::Int(None),
+ default_expr: Some(Expr::Value(
+ Value::Number("1".parse().unwrap(), false).with_empty_span()
+ )),
+ },
+ OperateFunctionArg {
+ mode: Some(ArgMode::Out),
+ name: Some(Ident::with_quote('"', "B")),
+ data_type: DataType::Text,
+ default_expr: None,
+ },
+ OperateFunctionArg {
+ mode: Some(ArgMode::InOut),
+ name: Some(Ident::with_quote('"', "C")),
+ data_type: DataType::Boolean,
+ default_expr: None,
+ },
+ OperateFunctionArg {
+ mode: Some(ArgMode::Variadic),
+ name: Some(Ident::with_quote('"', "D")),
+ data_type: DataType::Array(ArrayElemTypeDef::SquareBracket(
+ Box::new(DataType::Int(None)),
+ None
+ )),
+ default_expr: None,
+ },
+ ])
+ );
+
+ // The `DEFAULT` spelling of the same argument list renders as `=`.
+ pg_and_generic().one_statement_parses_to(
+ r#"CREATE FUNCTION f("A" INT DEFAULT 1) RETURNS INT LANGUAGE SQL AS 'x'"#,
+ r#"CREATE FUNCTION f("A" INT = 1) RETURNS INT LANGUAGE SQL AS 'x'"#,
+ );
+}
+
+#[test]
+fn parse_quoted_argument_names_in_function_signatures() {
+ pg_and_generic().verified_stmt(r#"DROP FUNCTION f("Role" TEXT)"#);
+ pg_and_generic().verified_stmt(r#"DROP PROCEDURE p("Role" TEXT)"#);
+ pg_and_generic().verified_stmt(r#"ALTER FUNCTION f("Role" TEXT) RENAME TO g"#);
+ pg_and_generic().verified_stmt(r#"ALTER AGGREGATE my_agg("Role" TEXT) RENAME TO other_agg"#);
+
+ // An aggregate's `ORDER BY` arguments are parsed by the same reader as its
+ // direct ones.
+ let sql = r#"ALTER AGGREGATE my_agg("A" INT ORDER BY "B" INT) OWNER TO some_role"#;
+ let Statement::AlterFunction(alter) = pg_and_generic().verified_stmt(sql) else {
+ panic!("expected an ALTER AGGREGATE statement");
+ };
+ assert_eq!(
+ alter.function.args,
+ Some(vec![OperateFunctionArg {
+ mode: None,
+ name: Some(Ident::with_quote('"', "A")),
+ data_type: DataType::Int(None),
+ default_expr: None,
+ }])
+ );
+ assert_eq!(
+ alter.aggregate_order_by,
+ Some(vec![OperateFunctionArg {
+ mode: None,
+ name: Some(Ident::with_quote('"', "B")),
+ data_type: DataType::Int(None),
+ default_expr: None,
+ }])
+ );
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org