diff --git a/fuzz/fuzz_seeds/hive_2589_digit_prefixed_field.sql b/fuzz/fuzz_seeds/hive_2589_digit_prefixed_field.sql new file mode 100644 index 0000000000..4375daef43 --- /dev/null +++ b/fuzz/fuzz_seeds/hive_2589_digit_prefixed_field.sql @@ -0,0 +1 @@ +SELECT [dbo]. 1te_sk = 1 diff --git a/fuzz/fuzz_seeds/mssql_2709_alter_role_with_name.sql b/fuzz/fuzz_seeds/mssql_2709_alter_role_with_name.sql new file mode 100644 index 0000000000..d563c54909 --- /dev/null +++ b/fuzz/fuzz_seeds/mssql_2709_alter_role_with_name.sql @@ -0,0 +1 @@ +ALTER ROLE old_name WITH NAME = new_name diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 96b340714c..cf8e9d955d 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -147,6 +147,8 @@ pub enum AlterRoleOperation { RenameRole { /// Role name to rename. role_name: Ident, + /// Whether the MS SQL `WITH NAME =` form was used instead of `RENAME TO`. + use_with_name: bool, }, /// MS SQL Server /// @@ -194,8 +196,15 @@ pub enum AlterRoleOperation { impl fmt::Display for AlterRoleOperation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - AlterRoleOperation::RenameRole { role_name } => { - write!(f, "RENAME TO {role_name}") + AlterRoleOperation::RenameRole { + role_name, + use_with_name, + } => { + if *use_with_name { + write!(f, "WITH NAME = {role_name}") + } else { + write!(f, "RENAME TO {role_name}") + } } AlterRoleOperation::AddMember { member_name } => { write!(f, "ADD MEMBER {member_name}") diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb5bc6cc75..16383358fa 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1503,10 +1503,19 @@ const fn is_number_expr(expr: &Expr) -> bool { matches!(expr, Expr::Value(v) if matches!(v.value, Value::Number(_, _))) } +/// Whether a leading `.` would merge into `expr` when the displayed SQL is +/// re-lexed: `.1` lexes as a number in front of a numeric literal or an +/// unquoted identifier starting with a digit (Hive allows such words). +fn dot_merges_with_expr(expr: &Expr) -> bool { + is_number_expr(expr) + || matches!(expr, Expr::Identifier(ident) if ident.quote_style.is_none() + && ident.value.starts_with(|c: char| c.is_ascii_digit())) +} + impl fmt::Display for AccessExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AccessExpr::Dot(expr) if is_number_expr(expr) => write!(f, " . {expr}"), + AccessExpr::Dot(expr) if dot_merges_with_expr(expr) => write!(f, " . {expr}"), AccessExpr::Dot(expr) => write!(f, ".{expr}"), AccessExpr::Subscript(subscript) => write!(f, "[{subscript}]"), } diff --git a/src/parser/alter.rs b/src/parser/alter.rs index 4000eb26ba..a1bda44f6e 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -356,7 +356,10 @@ impl Parser<'_> { } else if self.parse_keywords(&[Keyword::WITH, Keyword::NAME]) { if self.consume_token(&Token::Eq) { let role_name = self.parse_identifier()?; - AlterRoleOperation::RenameRole { role_name } + AlterRoleOperation::RenameRole { + role_name, + use_with_name: true, + } } else { return self.expected_ref("= after WITH NAME ", self.peek_token_ref()); } @@ -383,7 +386,10 @@ impl Parser<'_> { let operation = if self.parse_keyword(Keyword::RENAME) { if self.parse_keyword(Keyword::TO) { let role_name = self.parse_identifier()?; - AlterRoleOperation::RenameRole { role_name } + AlterRoleOperation::RenameRole { + role_name, + use_with_name: false, + } } else { return self.expected_ref("TO after RENAME", self.peek_token_ref()); } diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 21e76e23b6..d6cfb1c12d 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -569,6 +569,16 @@ fn parse_create_table_with_map_column_comment() { ); } +#[test] +fn test_field_access_on_digit_prefixed_identifier() { + // The space before `1te_sk` keeps `.1` from being lexed as a number, + // which previously reparsed as a different statement. See + // https://github.com/apache/datafusion-sqlparser-rs/issues/2589 + hive().one_statement_parses_to("SELECT [dbo]. 1te_sk = 1", "SELECT [dbo] . 1te_sk = 1"); + hive().verified_stmt("SELECT [dbo] . 1te_sk"); + hive().verified_stmt("SELECT dbo.1te_sk"); +} + fn hive() -> TestedDialects { TestedDialects::new(vec![Box::new(HiveDialect {})]) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9bf015fc00..872d15c586 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -812,7 +812,8 @@ fn parse_alter_role() { value: "new_name".into(), quote_style: None, span: Span::empty(), - } + }, + use_with_name: true, }, }] ); @@ -3013,3 +3014,11 @@ fn parse_mssql_declare_comma_list_display() { ms().verified_stmt("DECLARE @a INT = 1, @b TEXT = 'x'"); ms().verified_stmt("DECLARE @foo CURSOR, @bar INT"); } + +#[test] +fn mssql_alter_role_with_name_roundtrip() { + // `WITH NAME =` is the only rename form T-SQL accepts; the shared + // RenameRole variant must not render as `RENAME TO`. + ms().verified_stmt("ALTER ROLE old_name WITH NAME = new_name"); + ms().verified_stmt("ALTER ROLE [old_name] WITH NAME = [new_name]"); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0a3866d29f..def403c1f4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4562,7 +4562,8 @@ fn parse_alter_role() { value: "new_name".into(), quote_style: None, span: Span::empty(), - } + }, + use_with_name: false, }, } ); @@ -4774,6 +4775,7 @@ fn parse_alter_user() { name: Ident::new("old_name"), operation: AlterRoleOperation::RenameRole { role_name: Ident::new("new_name"), + use_with_name: false, }, } );