Skip to content

Commit 6d4776b

Browse files
Enhancing Trailing Comma Option (apache#1212)
1 parent a0f511c commit 6d4776b

7 files changed

Lines changed: 118 additions & 12 deletions

File tree

src/dialect/bigquery.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ impl Dialect for BigQueryDialect {
2222
ch == '`'
2323
}
2424

25+
fn supports_projection_trailing_commas(&self) -> bool {
26+
true
27+
}
28+
2529
fn is_identifier_start(&self, ch: char) -> bool {
2630
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_'
2731
}

src/dialect/duckdb.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ pub struct DuckDbDialect;
1818

1919
// In most cases the redshift dialect is identical to [`PostgresSqlDialect`].
2020
impl Dialect for DuckDbDialect {
21+
fn supports_trailing_commas(&self) -> bool {
22+
true
23+
}
24+
2125
fn is_identifier_start(&self, ch: char) -> bool {
2226
ch.is_alphabetic() || ch == '_'
2327
}

src/dialect/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,14 @@ pub trait Dialect: Debug + Any {
251251
// return None to fall back to the default behavior
252252
None
253253
}
254+
/// Does the dialect support trailing commas around the query?
255+
fn supports_trailing_commas(&self) -> bool {
256+
false
257+
}
258+
/// Does the dialect support trailing commas in the projection list?
259+
fn supports_projection_trailing_commas(&self) -> bool {
260+
self.supports_trailing_commas()
261+
}
254262
/// Dialect-specific infix parser override
255263
fn parse_infix(
256264
&self,

src/dialect/snowflake.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ impl Dialect for SnowflakeDialect {
3838
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_'
3939
}
4040

41+
fn supports_projection_trailing_commas(&self) -> bool {
42+
true
43+
}
44+
4145
fn is_identifier_part(&self, ch: char) -> bool {
4246
ch.is_ascii_lowercase()
4347
|| ch.is_ascii_uppercase()

src/parser/mod.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ impl<'a> Parser<'a> {
305305
state: ParserState::Normal,
306306
dialect,
307307
recursion_counter: RecursionCounter::new(DEFAULT_REMAINING_DEPTH),
308-
options: ParserOptions::default(),
308+
options: ParserOptions::new().with_trailing_commas(dialect.supports_trailing_commas()),
309309
}
310310
}
311311

@@ -3225,7 +3225,7 @@ impl<'a> Parser<'a> {
32253225
// This pattern could be captured better with RAII type semantics, but it's quite a bit of
32263226
// code to add for just one case, so we'll just do it manually here.
32273227
let old_value = self.options.trailing_commas;
3228-
self.options.trailing_commas |= dialect_of!(self is BigQueryDialect | SnowflakeDialect);
3228+
self.options.trailing_commas |= self.dialect.supports_projection_trailing_commas();
32293229

32303230
let ret = self.parse_comma_separated(|p| p.parse_select_item());
32313231
self.options.trailing_commas = old_value;
@@ -5413,12 +5413,17 @@ impl<'a> Parser<'a> {
54135413
} else {
54145414
return self.expected("column name or constraint definition", self.peek_token());
54155415
}
5416+
54165417
let comma = self.consume_token(&Token::Comma);
5417-
if self.consume_token(&Token::RParen) {
5418-
// allow a trailing comma, even though it's not in standard
5419-
break;
5420-
} else if !comma {
5418+
let rparen = self.peek_token().token == Token::RParen;
5419+
5420+
if !comma && !rparen {
54215421
return self.expected("',' or ')' after column definition", self.peek_token());
5422+
};
5423+
5424+
if rparen && (!comma || self.options.trailing_commas) {
5425+
let _ = self.consume_token(&Token::RParen);
5426+
break;
54225427
}
54235428
}
54245429

@@ -9411,6 +9416,9 @@ impl<'a> Parser<'a> {
94119416
with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES),
94129417
}
94139418
} else {
9419+
let old_value = self.options.trailing_commas;
9420+
self.options.trailing_commas = false;
9421+
94149422
let (actions, err): (Vec<_>, Vec<_>) = self
94159423
.parse_comma_separated(Parser::parse_grant_permission)?
94169424
.into_iter()
@@ -9434,6 +9442,8 @@ impl<'a> Parser<'a> {
94349442
})
94359443
.partition(Result::is_ok);
94369444

9445+
self.options.trailing_commas = old_value;
9446+
94379447
if !err.is_empty() {
94389448
let errors: Vec<Keyword> = err.into_iter().filter_map(|x| x.err()).collect();
94399449
return Err(ParserError::ParserError(format!(
@@ -9939,6 +9949,12 @@ impl<'a> Parser<'a> {
99399949
Expr::Wildcard => Ok(SelectItem::Wildcard(
99409950
self.parse_wildcard_additional_options()?,
99419951
)),
9952+
Expr::Identifier(v) if v.value.to_lowercase() == "from" => {
9953+
parser_err!(
9954+
format!("Expected an expression, found: {}", v),
9955+
self.peek_token().location
9956+
)
9957+
}
99429958
expr => self
99439959
.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS)
99449960
.map(|alias| match alias {

tests/sqlparser_common.rs

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3552,8 +3552,13 @@ fn parse_create_table_clone() {
35523552

35533553
#[test]
35543554
fn parse_create_table_trailing_comma() {
3555-
let sql = "CREATE TABLE foo (bar int,)";
3556-
all_dialects().one_statement_parses_to(sql, "CREATE TABLE foo (bar INT)");
3555+
let dialect = TestedDialects {
3556+
dialects: vec![Box::new(DuckDbDialect {})],
3557+
options: None,
3558+
};
3559+
3560+
let sql = "CREATE TABLE foo (bar int,);";
3561+
dialect.one_statement_parses_to(sql, "CREATE TABLE foo (bar INT)");
35573562
}
35583563

35593564
#[test]
@@ -4418,7 +4423,7 @@ fn parse_window_clause() {
44184423
ORDER BY C3";
44194424
verified_only_select(sql);
44204425

4421-
let sql = "SELECT from mytable WINDOW window1 AS window2";
4426+
let sql = "SELECT * from mytable WINDOW window1 AS window2";
44224427
let dialects = all_dialects_except(|d| d.is::<BigQueryDialect>() || d.is::<GenericDialect>());
44234428
let res = dialects.parse_sql_statements(sql);
44244429
assert_eq!(
@@ -8846,9 +8851,11 @@ fn parse_non_latin_identifiers() {
88468851

88478852
#[test]
88488853
fn parse_trailing_comma() {
8854+
// At the moment, Duck DB is the only dialect that allows
8855+
// trailing commas anywhere in the query
88498856
let trailing_commas = TestedDialects {
8850-
dialects: vec![Box::new(GenericDialect {})],
8851-
options: Some(ParserOptions::new().with_trailing_commas(true)),
8857+
dialects: vec![Box::new(DuckDbDialect {})],
8858+
options: None,
88528859
};
88538860

88548861
trailing_commas.one_statement_parses_to(
@@ -8866,11 +8873,74 @@ fn parse_trailing_comma() {
88668873
"SELECT DISTINCT ON (album_id) name FROM track",
88678874
);
88688875

8876+
trailing_commas.one_statement_parses_to(
8877+
"CREATE TABLE employees (name text, age int,)",
8878+
"CREATE TABLE employees (name TEXT, age INT)",
8879+
);
8880+
88698881
trailing_commas.verified_stmt("SELECT album_id, name FROM track");
88708882

88718883
trailing_commas.verified_stmt("SELECT * FROM track ORDER BY milliseconds");
88728884

88738885
trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track");
8886+
8887+
// doesn't allow any trailing commas
8888+
let trailing_commas = TestedDialects {
8889+
dialects: vec![Box::new(GenericDialect {})],
8890+
options: None,
8891+
};
8892+
8893+
assert_eq!(
8894+
trailing_commas
8895+
.parse_sql_statements("SELECT name, age, from employees;")
8896+
.unwrap_err(),
8897+
ParserError::ParserError("Expected an expression, found: from".to_string())
8898+
);
8899+
8900+
assert_eq!(
8901+
trailing_commas
8902+
.parse_sql_statements("CREATE TABLE employees (name text, age int,)")
8903+
.unwrap_err(),
8904+
ParserError::ParserError(
8905+
"Expected column name or constraint definition, found: )".to_string()
8906+
)
8907+
);
8908+
}
8909+
8910+
#[test]
8911+
fn parse_projection_trailing_comma() {
8912+
// Some dialects allow trailing commas only in the projection
8913+
let trailing_commas = TestedDialects {
8914+
dialects: vec![Box::new(SnowflakeDialect {}), Box::new(BigQueryDialect {})],
8915+
options: None,
8916+
};
8917+
8918+
trailing_commas.one_statement_parses_to(
8919+
"SELECT album_id, name, FROM track",
8920+
"SELECT album_id, name FROM track",
8921+
);
8922+
8923+
trailing_commas.verified_stmt("SELECT album_id, name FROM track");
8924+
8925+
trailing_commas.verified_stmt("SELECT * FROM track ORDER BY milliseconds");
8926+
8927+
trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track");
8928+
8929+
assert_eq!(
8930+
trailing_commas
8931+
.parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,")
8932+
.unwrap_err(),
8933+
ParserError::ParserError("Expected an expression:, found: EOF".to_string())
8934+
);
8935+
8936+
assert_eq!(
8937+
trailing_commas
8938+
.parse_sql_statements("CREATE TABLE employees (name text, age int,)")
8939+
.unwrap_err(),
8940+
ParserError::ParserError(
8941+
"Expected column name or constraint definition, found: )".to_string()
8942+
),
8943+
);
88748944
}
88758945

88768946
#[test]

tests/sqlparser_postgres.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3701,7 +3701,7 @@ fn parse_create_table_with_alias() {
37013701
int2_col INT2,
37023702
float8_col FLOAT8,
37033703
float4_col FLOAT4,
3704-
bool_col BOOL,
3704+
bool_col BOOL
37053705
);";
37063706
match pg_and_generic().one_statement_parses_to(sql, "") {
37073707
Statement::CreateTable(CreateTable {

0 commit comments

Comments
 (0)