Skip to content

Commit 560faf2

Browse files
DuckDB, MySQL, Spark, Databricks, Hive, ClickHouse: Accept string literal aliases only where each engine does
1 parent d32825d commit 560faf2

10 files changed

Lines changed: 177 additions & 13 deletions

File tree

‎src/dialect/clickhouse.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ impl Dialect for ClickHouseDialect {
2828
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_'
2929
}
3030

31+
/// Aliases are identifiers, quoted with `"` or a backtick, see
32+
/// <https://clickhouse.com/docs/reference/syntax#identifiers>
33+
fn supports_string_literal_column_alias(&self, _explicit: bool) -> bool {
34+
false
35+
}
36+
37+
/// See <https://clickhouse.com/docs/reference/syntax#identifiers>
38+
fn supports_string_literal_table_alias(&self, _explicit: bool) -> bool {
39+
false
40+
}
41+
3142
fn is_identifier_part(&self, ch: char) -> bool {
3243
self.is_identifier_start(ch) || ch.is_ascii_digit()
3344
}

‎src/dialect/databricks.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ impl Dialect for DatabricksDialect {
3131
matches!(ch, '`')
3232
}
3333

34+
/// Aliases are identifiers, see `column_alias` in
35+
/// <https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-qry-select.html> and
36+
/// <https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-identifiers.html>
37+
fn supports_string_literal_column_alias(&self, _explicit: bool) -> bool {
38+
false
39+
}
40+
41+
/// Aliases are identifiers, see
42+
/// <https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-identifiers.html>
43+
fn supports_string_literal_table_alias(&self, _explicit: bool) -> bool {
44+
false
45+
}
46+
3447
/// See <https://docs.databricks.com/en/sql/language-manual/sql-ref-identifiers.html>
3548
fn identifier_quote_style(&self, _identifier: &str) -> Option<char> {
3649
Some('`')

‎src/dialect/duckdb.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ impl Dialect for DuckDbDialect {
2828
true
2929
}
3030

31+
fn supports_string_literal_column_alias(&self, explicit: bool) -> bool {
32+
explicit
33+
}
34+
35+
fn supports_string_literal_table_alias(&self, explicit: bool) -> bool {
36+
explicit
37+
}
38+
3139
fn supports_trailing_commas(&self) -> bool {
3240
true
3341
}

‎src/dialect/hive.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ impl Dialect for HiveDialect {
3131
true
3232
}
3333

34+
/// The alias is an `identifier`, see `selectItem` in
35+
/// <https://github.com/apache/hive/blob/master/parser/src/java/org/apache/hadoop/hive/ql/parse/SelectClauseParser.g>
36+
fn supports_string_literal_column_alias(&self, _explicit: bool) -> bool {
37+
false
38+
}
39+
40+
/// The alias is an `identifier`, see `tableSource` in
41+
/// <https://github.com/apache/hive/blob/master/parser/src/java/org/apache/hadoop/hive/ql/parse/FromClauseParser.g>
42+
fn supports_string_literal_table_alias(&self, _explicit: bool) -> bool {
43+
false
44+
}
45+
3446
/// See <https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-RulesforColumnNames>
3547
fn identifier_quote_style(&self, _identifier: &str) -> Option<char> {
3648
Some('`')

‎src/dialect/mod.rs‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,11 +1537,20 @@ pub trait Dialect: Debug + Any {
15371537
explicit || self.is_table_alias(kw, parser)
15381538
}
15391539

1540-
/// Returns true if a single-quoted string can be an alias, as in `SELECT 1 AS 'x'`.
1540+
/// Returns true if a single-quoted string can name a select item, as in `SELECT 1 AS 'x'`.
1541+
/// `explicit` is true when the alias follows `AS`.
15411542
///
15421543
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/problems-with-alias.html),
15431544
/// [SQLite](https://www.sqlite.org/lang_keywords.html)
1544-
fn supports_string_literal_alias(&self) -> bool {
1545+
fn supports_string_literal_column_alias(&self, _explicit: bool) -> bool {
1546+
true
1547+
}
1548+
1549+
/// Returns true if a single-quoted string can name a table, as in `FROM t AS 'x'`.
1550+
/// `explicit` is true when the alias follows `AS`.
1551+
///
1552+
/// [SQLite](https://www.sqlite.org/lang_keywords.html)
1553+
fn supports_string_literal_table_alias(&self, _explicit: bool) -> bool {
15451554
true
15461555
}
15471556

‎src/dialect/mysql.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ const RESERVED_FOR_TABLE_ALIAS_MYSQL: &[Keyword] = &[
4141
pub struct MySqlDialect {}
4242

4343
impl Dialect for MySqlDialect {
44+
/// Only select-list aliases may use string quoting, see
45+
/// <https://dev.mysql.com/doc/refman/8.4/en/problems-with-alias.html>
46+
fn supports_string_literal_table_alias(&self, _explicit: bool) -> bool {
47+
false
48+
}
49+
4450
fn is_identifier_start(&self, ch: char) -> bool {
4551
// See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html.
4652
// Identifiers which begin with a digit are recognized while tokenizing numbers,

‎src/dialect/postgresql.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,13 @@ impl Dialect for PostgreSqlDialect {
116116

117117
/// Aliases are identifiers, see `output_name` and `alias` in
118118
/// <https://www.postgresql.org/docs/current/sql-select.html>
119-
fn supports_string_literal_alias(&self) -> bool {
119+
fn supports_string_literal_column_alias(&self, _explicit: bool) -> bool {
120+
false
121+
}
122+
123+
/// Aliases are identifiers, see `alias` in
124+
/// <https://www.postgresql.org/docs/current/sql-select.html>
125+
fn supports_string_literal_table_alias(&self, _explicit: bool) -> bool {
120126
false
121127
}
122128

‎src/dialect/spark.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ impl Dialect for SparkSqlDialect {
3636
matches!(ch, '`')
3737
}
3838

39+
/// Aliases are identifiers, see `named_expression` in
40+
/// <https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select.html> and
41+
/// <https://spark.apache.org/docs/latest/sql-ref-identifier.html>
42+
fn supports_string_literal_column_alias(&self, _explicit: bool) -> bool {
43+
false
44+
}
45+
46+
/// Aliases are identifiers, see <https://spark.apache.org/docs/latest/sql-ref-identifier.html>
47+
fn supports_string_literal_table_alias(&self, _explicit: bool) -> bool {
48+
false
49+
}
50+
3951
/// See <https://spark.apache.org/docs/latest/sql-ref-identifier.html>
4052
fn identifier_quote_style(&self, _identifier: &str) -> Option<char> {
4153
Some('`')

‎src/parser/mod.rs‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,13 @@ pub struct Parser<'a> {
366366
failed_in_subquery_positions: BTreeMap<usize, Option<usize>>,
367367
}
368368

369+
/// Whether an alias names a select item or a table.
370+
#[derive(Debug, Clone, Copy)]
371+
enum AliasKind {
372+
Column,
373+
Table,
374+
}
375+
369376
/// Copy marker for a [`ParserError`] cached by the `parse_prefix` failure
370377
/// memoization, so the caches hold no strings.
371378
#[derive(Debug, Clone, Copy)]
@@ -1414,7 +1421,7 @@ impl<'a> Parser<'a> {
14141421
fn validator(explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
14151422
explicit || !&[Keyword::ASC, Keyword::DESC, Keyword::GROUP].contains(kw)
14161423
}
1417-
let alias = self.parse_optional_alias_inner(None, validator)?;
1424+
let alias = self.parse_optional_alias_inner(None, AliasKind::Column, validator)?;
14181425
let order_by = OrderByOptions {
14191426
sort: self.parse_optional_order_by_sort(),
14201427
nulls_first: None,
@@ -13605,7 +13612,7 @@ impl<'a> Parser<'a> {
1360513612
fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
1360613613
parser.dialect.is_select_item_alias(explicit, kw, parser)
1360713614
}
13608-
self.parse_optional_alias_inner(None, validator)
13615+
self.parse_optional_alias_inner(None, AliasKind::Column, validator)
1360913616
}
1361013617

1361113618
/// Optionally parses an alias for a table like in `... FROM generate_series(1, 10) AS t (col)`.
@@ -13616,7 +13623,7 @@ impl<'a> Parser<'a> {
1361613623
parser.dialect.is_table_factor_alias(explicit, kw, parser)
1361713624
}
1361813625
let explicit = self.peek_keyword(Keyword::AS);
13619-
match self.parse_optional_alias_inner(None, validator)? {
13626+
match self.parse_optional_alias_inner(None, AliasKind::Table, validator)? {
1362013627
Some(name) => {
1362113628
let columns = self.parse_table_alias_column_defs()?;
1362213629
let at = if self.dialect.supports_partiql() && self.parse_keyword(Keyword::AT) {
@@ -13704,7 +13711,7 @@ impl<'a> Parser<'a> {
1370413711
fn validator(_explicit: bool, _kw: &Keyword, _parser: &mut Parser) -> bool {
1370513712
false
1370613713
}
13707-
self.parse_optional_alias_inner(Some(reserved_kwds), validator)
13714+
self.parse_optional_alias_inner(Some(reserved_kwds), AliasKind::Column, validator)
1370813715
}
1370913716

1371013717
/// Parses an optional alias after a SQL element such as a select list item
@@ -13716,6 +13723,7 @@ impl<'a> Parser<'a> {
1371613723
fn parse_optional_alias_inner<F>(
1371713724
&mut self,
1371813725
reserved_kwds: Option<&[Keyword]>,
13726+
kind: AliasKind,
1371913727
validator: F,
1372013728
) -> Result<Option<Ident>, ParserError>
1372113729
where
@@ -13739,7 +13747,14 @@ impl<'a> Parser<'a> {
1373913747
Token::Word(w) if validator(after_as, &w.keyword, self) => {
1374013748
Ok(Some(w.into_ident(next_token.span)))
1374113749
}
13742-
Token::SingleQuotedString(s) if self.dialect.supports_string_literal_alias() => {
13750+
Token::SingleQuotedString(s)
13751+
if match kind {
13752+
AliasKind::Column => {
13753+
self.dialect.supports_string_literal_column_alias(after_as)
13754+
}
13755+
AliasKind::Table => self.dialect.supports_string_literal_table_alias(after_as),
13756+
} =>
13757+
{
1374313758
Ok(Some(Ident::with_quote('\'', s)))
1374413759
}
1374513760
Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))),
@@ -17883,7 +17898,7 @@ impl<'a> Parser<'a> {
1788317898
// ~ for a PIVOT aggregate function the alias must not be a "FOR"; in any dialect
1788417899
kw != &Keyword::FOR && parser.dialect.is_select_item_alias(explicit, kw, parser)
1788517900
}
17886-
self.parse_optional_alias_inner(None, validator)?
17901+
self.parse_optional_alias_inner(None, AliasKind::Column, validator)?
1788717902
};
1788817903
Ok(ExprWithAlias { expr, alias })
1788917904
}

‎tests/sqlparser_common.rs‎

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20750,8 +20750,80 @@ fn parse_cte_materialization() {
2075020750

2075120751
#[test]
2075220752
fn parse_string_literal_alias() {
20753-
let dialects = all_dialects_where(|d| d.supports_string_literal_alias());
20754-
dialects.one_statement_parses_to("SELECT 1 'x'", "SELECT 1 AS 'x'");
20755-
dialects.verified_stmt("SELECT 1 AS 'x'");
20756-
dialects.verified_stmt("SELECT a FROM t AS 'y'");
20753+
for (sql, canonical, table, explicit, error) in [
20754+
(
20755+
"SELECT 1 AS 'x'",
20756+
"SELECT 1 AS 'x'",
20757+
false,
20758+
true,
20759+
"Expected: an identifier after AS, found: 'x'",
20760+
),
20761+
(
20762+
"SELECT 1 'x'",
20763+
"SELECT 1 AS 'x'",
20764+
false,
20765+
false,
20766+
"Expected: end of statement, found: 'x'",
20767+
),
20768+
(
20769+
"SELECT a FROM t AS 'y'",
20770+
"SELECT a FROM t AS 'y'",
20771+
true,
20772+
true,
20773+
"Expected: an identifier after AS, found: 'y'",
20774+
),
20775+
(
20776+
"SELECT a FROM t 'y'",
20777+
"SELECT a FROM t 'y'",
20778+
true,
20779+
false,
20780+
"Expected: end of statement, found: 'y'",
20781+
),
20782+
] {
20783+
let supported = |d: &dyn Dialect| {
20784+
if table {
20785+
d.supports_string_literal_table_alias(explicit)
20786+
} else {
20787+
d.supports_string_literal_column_alias(explicit)
20788+
}
20789+
};
20790+
all_dialects_where(supported).one_statement_parses_to(sql, canonical);
20791+
assert_eq!(
20792+
all_dialects_where(|d| !supported(d)).parse_sql_statements(sql),
20793+
Err(ParserError::ParserError(error.to_string())),
20794+
"{sql}"
20795+
);
20796+
}
20797+
}
20798+
20799+
#[test]
20800+
fn parse_string_literal_alias_per_engine() {
20801+
let forms = [
20802+
"SELECT 1 AS 'x'",
20803+
"SELECT 1 'x'",
20804+
"SELECT a FROM t AS 'y'",
20805+
"SELECT a FROM t 'y'",
20806+
];
20807+
let dialects: [(Box<dyn Dialect>, [bool; 4]); 8] = [
20808+
(Box::new(PostgreSqlDialect {}), [false, false, false, false]),
20809+
(Box::new(SQLiteDialect {}), [true, true, true, true]),
20810+
(Box::new(DuckDbDialect {}), [true, false, true, false]),
20811+
(Box::new(MySqlDialect {}), [true, true, false, false]),
20812+
(
20813+
Box::new(sqlparser::dialect::SparkSqlDialect {}),
20814+
[false, false, false, false],
20815+
),
20816+
(Box::new(DatabricksDialect {}), [false, false, false, false]),
20817+
(Box::new(HiveDialect {}), [false, false, false, false]),
20818+
(Box::new(ClickHouseDialect {}), [false, false, false, false]),
20819+
];
20820+
for (dialect, accepted) in dialects {
20821+
for (sql, accepted) in forms.iter().zip(accepted) {
20822+
assert_eq!(
20823+
Parser::parse_sql(dialect.as_ref(), sql).is_ok(),
20824+
accepted,
20825+
"{dialect:?}: {sql}"
20826+
);
20827+
}
20828+
}
2075720829
}

0 commit comments

Comments
 (0)