diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index f6a6682ec9..9c1dc08b3a 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -49,6 +49,10 @@ impl Dialect for GenericDialect { true } + fn supports_literal_prefix_in_qualified_name(&self) -> bool { + true + } + fn supports_partition_by_after_order_by(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3ce71eb0be..a2920e61eb 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -291,6 +291,14 @@ pub trait Dialect: Debug + Any { true } + /// Treats literal prefixes immediately after `.` as unquoted identifiers. + /// + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/identifiers.html), + /// [MariaDB](https://mariadb.com/docs/server/reference/sql-structure/sql-language-structure/identifier-names) + fn supports_literal_prefix_in_qualified_name(&self) -> bool { + false + } + /// Determine whether the dialect strips the backslash when escaping LIKE wildcards (%, _). /// /// [MySQL] has a special case when escaping single quoted strings which leaves these unescaped diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 4c8b407f37..c5330d8eb5 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -92,6 +92,10 @@ impl Dialect for MySqlDialect { true } + fn supports_literal_prefix_in_qualified_name(&self) -> bool { + true + } + fn supports_bitwise_shift_operators(&self) -> bool { true } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 728a508cee..b6dafe12e9 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1064,6 +1064,12 @@ impl<'a> Tokenizer<'a> { ) -> Result, TokenizerError> { match chars.peek() { Some(&ch) => match ch { + ch if prev_token == Some(&Token::Period) + && ch.is_ascii_alphabetic() + && self.dialect.supports_literal_prefix_in_qualified_name() => + { + self.tokenize_identifier_or_keyword([ch], chars) + } ' ' => self.consume_and_return(chars, Token::Whitespace(Whitespace::Space)), '\t' => self.consume_and_return(chars, Token::Whitespace(Whitespace::Tab)), '\n' => self.consume_and_return(chars, Token::Whitespace(Whitespace::Newline)), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 87fb61dc17..8f665b25c2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21022,3 +21022,65 @@ fn parse_window_spec_keyword_base_window_name() { "SELECT count(*) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t", ); } + +#[test] +fn parse_qualified_column_named_like_a_literal_prefix() { + let dialects = all_dialects_where(|d| d.supports_literal_prefix_in_qualified_name()); + for (prefix, value) in [ + ("x", "ff"), + ("X", "ff"), + ("b", "01"), + ("B", "01"), + ("n", "nat"), + ("N", "nat"), + ("r", "raw"), + ("R", "raw"), + ("e", "es"), + ("E", "es"), + ] { + dialects.one_statement_parses_to( + &format!("SELECT t.{prefix}'{value}' FROM t"), + &format!("SELECT t.{prefix} AS '{value}' FROM t"), + ); + } + dialects.one_statement_parses_to("SELECT t.u&'uni' FROM t", "SELECT t.u & 'uni' FROM t"); +} + +#[test] +fn tokenize_qualified_literal_prefix_without_capability() { + all_dialects_where(|d| !d.supports_literal_prefix_in_qualified_name()).tokenizes_to( + "t.x'ff'", + vec![ + Token::make_word("t", None), + Token::Period, + Token::HexStringLiteral("ff".to_string()), + ], + ); +} + +#[test] +fn parse_qualified_literal_prefix_requires_adjacent_period() { + let dialects = all_dialects_where(|d| d.supports_literal_prefix_in_qualified_name()); + for sql in [ + "SELECT t. x'ff' FROM t", + "SELECT t./* comment */x'ff' FROM t", + ] { + assert_eq!( + dialects.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: an identifier or a '*' after '.', found: X'ff'".to_string() + )), + "{sql}" + ); + } + for (sql, token) in [ + ("x'ff'", Token::HexStringLiteral("ff".to_string())), + ( + "b'01'", + Token::SingleQuotedByteStringLiteral("01".to_string()), + ), + ("n'nat'", Token::NationalStringLiteral("nat".to_string())), + ] { + dialects.tokenizes_to(sql, vec![token]); + } +}