Skip to content

Commit 40abd90

Browse files
Merge branch 'main' into not-null-and-notnull-support
2 parents 375fe8c + bc2c4e2 commit 40abd90

17 files changed

Lines changed: 414 additions & 18 deletions

src/ast/query.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ pub struct Select {
321321
pub top_before_distinct: bool,
322322
/// projection expressions
323323
pub projection: Vec<SelectItem>,
324+
/// Excluded columns from the projection expression which are not specified
325+
/// directly after a wildcard.
326+
///
327+
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html)
328+
pub exclude: Option<ExcludeSelectItem>,
324329
/// INTO
325330
pub into: Option<SelectInto>,
326331
/// FROM
@@ -401,6 +406,10 @@ impl fmt::Display for Select {
401406
indented_list(f, &self.projection)?;
402407
}
403408

409+
if let Some(exclude) = &self.exclude {
410+
write!(f, " {exclude}")?;
411+
}
412+
404413
if let Some(ref into) = self.into {
405414
f.write_str(" ")?;
406415
into.fmt(f)?;

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,6 +2220,7 @@ impl Spanned for Select {
22202220
distinct: _, // todo
22212221
top: _, // todo, mysql specific
22222222
projection,
2223+
exclude: _,
22232224
into,
22242225
from,
22252226
lateral_views,

src/dialect/duckdb.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ impl Dialect for DuckDbDialect {
9595
true
9696
}
9797

98+
fn supports_select_wildcard_exclude(&self) -> bool {
99+
true
100+
}
101+
98102
/// DuckDB supports `NOTNULL` as an alias for `IS NOT NULL`,
99103
/// see DuckDB Comparisons <https://duckdb.org/docs/stable/sql/expressions/comparison_operators#between-and-is-not-null>
100104
fn supports_notnull_operator(&self) -> bool {

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,8 @@ impl Dialect for GenericDialect {
179179
fn supports_filter_during_aggregation(&self) -> bool {
180180
true
181181
}
182+
183+
fn supports_select_wildcard_exclude(&self) -> bool {
184+
true
185+
}
182186
}

src/dialect/mod.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,26 @@ pub trait Dialect: Debug + Any {
570570
false
571571
}
572572

573+
/// Returns true if the dialect supports an exclude option
574+
/// following a wildcard in the projection section. For example:
575+
/// `SELECT * EXCLUDE col1 FROM tbl`.
576+
///
577+
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html)
578+
/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/select)
579+
fn supports_select_wildcard_exclude(&self) -> bool {
580+
false
581+
}
582+
583+
/// Returns true if the dialect supports an exclude option
584+
/// as the last item in the projection section, not necessarily
585+
/// after a wildcard. For example:
586+
/// `SELECT *, c1, c2 EXCLUDE c3 FROM tbl`
587+
///
588+
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html)
589+
fn supports_select_exclude(&self) -> bool {
590+
false
591+
}
592+
573593
/// Dialect-specific infix parser override
574594
///
575595
/// This method is called to parse the next infix expression.
@@ -998,11 +1018,17 @@ pub trait Dialect: Debug + Any {
9981018
explicit || self.is_column_alias(kw, parser)
9991019
}
10001020

1021+
/// Returns true if the specified keyword should be parsed as a table identifier.
1022+
/// See [keywords::RESERVED_FOR_TABLE_ALIAS]
1023+
fn is_table_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
1024+
!keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw)
1025+
}
1026+
10011027
/// Returns true if the specified keyword should be parsed as a table factor alias.
10021028
/// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided
10031029
/// to enable looking ahead if needed.
1004-
fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
1005-
explicit || !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw)
1030+
fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
1031+
explicit || self.is_table_alias(kw, parser)
10061032
}
10071033

10081034
/// Returns true if this dialect supports querying historical table data

src/dialect/redshift.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,12 @@ impl Dialect for RedshiftSqlDialect {
131131
fn supports_string_literal_backslash_escape(&self) -> bool {
132132
true
133133
}
134+
135+
fn supports_select_wildcard_exclude(&self) -> bool {
136+
true
137+
}
138+
139+
fn supports_select_exclude(&self) -> bool {
140+
true
141+
}
134142
}

src/dialect/snowflake.rs

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,11 @@ impl Dialect for SnowflakeDialect {
318318
}
319319

320320
// `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT`
321-
// which would give it a different meanings, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias
322-
Keyword::FETCH
323-
if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) =>
321+
// which would give it a different meanings, for example:
322+
// `SELECT 1 FETCH FIRST 10 ROWS` - not an alias
323+
// `SELECT 1 FETCH 10` - not an alias
324+
Keyword::FETCH if parser.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]).is_some()
325+
|| matches!(parser.peek_token().token, Token::Number(_, _)) =>
324326
{
325327
false
326328
}
@@ -345,6 +347,86 @@ impl Dialect for SnowflakeDialect {
345347
}
346348
}
347349

350+
fn is_table_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool {
351+
match kw {
352+
// The following keywords can be considered an alias as long as
353+
// they are not followed by other tokens that may change their meaning
354+
Keyword::LIMIT
355+
| Keyword::RETURNING
356+
| Keyword::INNER
357+
| Keyword::USING
358+
| Keyword::PIVOT
359+
| Keyword::UNPIVOT
360+
| Keyword::EXCEPT
361+
| Keyword::MATCH_RECOGNIZE
362+
| Keyword::OFFSET
363+
if !matches!(parser.peek_token_ref().token, Token::SemiColon | Token::EOF) =>
364+
{
365+
false
366+
}
367+
368+
// `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT`
369+
// which would give it a different meanings, for example:
370+
// `SELECT * FROM tbl FETCH FIRST 10 ROWS` - not an alias
371+
// `SELECT * FROM tbl FETCH 10` - not an alias
372+
Keyword::FETCH
373+
if parser
374+
.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT])
375+
.is_some()
376+
|| matches!(parser.peek_token().token, Token::Number(_, _)) =>
377+
{
378+
false
379+
}
380+
381+
// All sorts of join-related keywords can be considered aliases unless additional
382+
// keywords change their meaning.
383+
Keyword::RIGHT | Keyword::LEFT | Keyword::SEMI | Keyword::ANTI
384+
if parser
385+
.peek_one_of_keywords(&[Keyword::JOIN, Keyword::OUTER])
386+
.is_some() =>
387+
{
388+
false
389+
}
390+
Keyword::GLOBAL if parser.peek_keyword(Keyword::FULL) => false,
391+
392+
// Reserved keywords by the Snowflake dialect, which seem to be less strictive
393+
// than what is listed in `keywords::RESERVED_FOR_TABLE_ALIAS`. The following
394+
// keywords were tested with the this statement: `SELECT <KW>.* FROM tbl <KW>`.
395+
Keyword::WITH
396+
| Keyword::ORDER
397+
| Keyword::SELECT
398+
| Keyword::WHERE
399+
| Keyword::GROUP
400+
| Keyword::HAVING
401+
| Keyword::LATERAL
402+
| Keyword::UNION
403+
| Keyword::INTERSECT
404+
| Keyword::MINUS
405+
| Keyword::ON
406+
| Keyword::JOIN
407+
| Keyword::INNER
408+
| Keyword::CROSS
409+
| Keyword::FULL
410+
| Keyword::LEFT
411+
| Keyword::RIGHT
412+
| Keyword::NATURAL
413+
| Keyword::USING
414+
| Keyword::ASOF
415+
| Keyword::MATCH_CONDITION
416+
| Keyword::SET
417+
| Keyword::QUALIFY
418+
| Keyword::FOR
419+
| Keyword::START
420+
| Keyword::CONNECT
421+
| Keyword::SAMPLE
422+
| Keyword::TABLESAMPLE
423+
| Keyword::FROM => false,
424+
425+
// Any other word is considered an alias
426+
_ => true,
427+
}
428+
}
429+
348430
/// See: <https://docs.snowflake.com/en/sql-reference/constructs/at-before>
349431
fn supports_timestamp_versioning(&self) -> bool {
350432
true
@@ -384,6 +466,10 @@ impl Dialect for SnowflakeDialect {
384466
fn supports_select_expr_star(&self) -> bool {
385467
true
386468
}
469+
470+
fn supports_select_wildcard_exclude(&self) -> bool {
471+
true
472+
}
387473
}
388474

389475
fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
11201120
Keyword::FETCH,
11211121
Keyword::UNION,
11221122
Keyword::EXCEPT,
1123+
Keyword::EXCLUDE,
11231124
Keyword::INTERSECT,
11241125
Keyword::MINUS,
11251126
Keyword::CLUSTER,

src/parser/mod.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,17 @@ pub struct ParserOptions {
222222
/// Controls how literal values are unescaped. See
223223
/// [`Tokenizer::with_unescape`] for more details.
224224
pub unescape: bool,
225+
/// Controls if the parser expects a semi-colon token
226+
/// between statements. Default is `true`.
227+
pub require_semicolon_stmt_delimiter: bool,
225228
}
226229

227230
impl Default for ParserOptions {
228231
fn default() -> Self {
229232
Self {
230233
trailing_commas: false,
231234
unescape: true,
235+
require_semicolon_stmt_delimiter: true,
232236
}
233237
}
234238
}
@@ -470,6 +474,10 @@ impl<'a> Parser<'a> {
470474
expecting_statement_delimiter = false;
471475
}
472476

477+
if !self.options.require_semicolon_stmt_delimiter {
478+
expecting_statement_delimiter = false;
479+
}
480+
473481
match self.peek_token().token {
474482
Token::EOF => break,
475483

@@ -11762,6 +11770,7 @@ impl<'a> Parser<'a> {
1176211770
top: None,
1176311771
top_before_distinct: false,
1176411772
projection: vec![],
11773+
exclude: None,
1176511774
into: None,
1176611775
from,
1176711776
lateral_views: vec![],
@@ -11804,6 +11813,12 @@ impl<'a> Parser<'a> {
1180411813
self.parse_projection()?
1180511814
};
1180611815

11816+
let exclude = if self.dialect.supports_select_exclude() {
11817+
self.parse_optional_select_item_exclude()?
11818+
} else {
11819+
None
11820+
};
11821+
1180711822
let into = if self.parse_keyword(Keyword::INTO) {
1180811823
Some(self.parse_select_into()?)
1180911824
} else {
@@ -11937,6 +11952,7 @@ impl<'a> Parser<'a> {
1193711952
top,
1193811953
top_before_distinct,
1193911954
projection,
11955+
exclude,
1194011956
into,
1194111957
from,
1194211958
lateral_views,
@@ -15074,8 +15090,7 @@ impl<'a> Parser<'a> {
1507415090
} else {
1507515091
None
1507615092
};
15077-
let opt_exclude = if opt_ilike.is_none()
15078-
&& dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect)
15093+
let opt_exclude = if opt_ilike.is_none() && self.dialect.supports_select_wildcard_exclude()
1507915094
{
1508015095
self.parse_optional_select_item_exclude()?
1508115096
} else {

src/test_utils.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@ pub fn all_dialects() -> TestedDialects {
294294
])
295295
}
296296

297+
// Returns all available dialects with the specified parser options
298+
pub fn all_dialects_with_options(options: ParserOptions) -> TestedDialects {
299+
TestedDialects::new_with_options(all_dialects().dialects, options)
300+
}
301+
297302
/// Returns all dialects matching the given predicate.
298303
pub fn all_dialects_where<F>(predicate: F) -> TestedDialects
299304
where

0 commit comments

Comments
 (0)