Skip to content

Commit 1f3d514

Browse files
committed
Merge remote-tracking branch 'apache/main' into Nyrox/main
2 parents b2b8795 + 62fa860 commit 1f3d514

23 files changed

+475
-60
lines changed

examples/cli.rs

+19-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
#![warn(clippy::all)]
1919

20-
/// A small command-line app to run the parser.
21-
/// Run with `cargo run --example cli`
20+
//! A small command-line app to run the parser.
21+
//! Run with `cargo run --example cli`
22+
2223
use std::fs;
24+
use std::io::{stdin, Read};
2325

2426
use simple_logger::SimpleLogger;
2527
use sqlparser::dialect::*;
@@ -38,6 +40,9 @@ $ cargo run --example cli FILENAME.sql [--dialectname]
3840
To print the parse results as JSON:
3941
$ cargo run --feature json_example --example cli FILENAME.sql [--dialectname]
4042
43+
To read from stdin instead of a file:
44+
$ cargo run --example cli - [--dialectname]
45+
4146
"#,
4247
);
4348

@@ -57,9 +62,18 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname]
5762
s => panic!("Unexpected parameter: {s}"),
5863
};
5964

60-
println!("Parsing from file '{}' using {:?}", &filename, dialect);
61-
let contents = fs::read_to_string(&filename)
62-
.unwrap_or_else(|_| panic!("Unable to read the file {}", &filename));
65+
let contents = if filename == "-" {
66+
println!("Parsing from stdin using {:?}", dialect);
67+
let mut buf = Vec::new();
68+
stdin()
69+
.read_to_end(&mut buf)
70+
.expect("failed to read from stdin");
71+
String::from_utf8(buf).expect("stdin content wasn't valid utf8")
72+
} else {
73+
println!("Parsing from file '{}' using {:?}", &filename, dialect);
74+
fs::read_to_string(&filename)
75+
.unwrap_or_else(|_| panic!("Unable to read the file {}", &filename))
76+
};
6377
let without_bom = if contents.chars().next().unwrap() as u64 != 0xfeff {
6478
contents.as_str()
6579
} else {

src/ast/mod.rs

+11
Original file line numberDiff line numberDiff line change
@@ -3406,6 +3406,13 @@ pub enum Statement {
34063406
/// See Postgres <https://www.postgresql.org/docs/current/sql-listen.html>
34073407
LISTEN { channel: Ident },
34083408
/// ```sql
3409+
/// UNLISTEN
3410+
/// ```
3411+
/// stop listening for a notification
3412+
///
3413+
/// See Postgres <https://www.postgresql.org/docs/current/sql-unlisten.html>
3414+
UNLISTEN { channel: Ident },
3415+
/// ```sql
34093416
/// NOTIFY channel [ , payload ]
34103417
/// ```
34113418
/// send a notification event together with an optional “payload” string to channel
@@ -5014,6 +5021,10 @@ impl fmt::Display for Statement {
50145021
write!(f, "LISTEN {channel}")?;
50155022
Ok(())
50165023
}
5024+
Statement::UNLISTEN { channel } => {
5025+
write!(f, "UNLISTEN {channel}")?;
5026+
Ok(())
5027+
}
50175028
Statement::NOTIFY { channel, payload } => {
50185029
write!(f, "NOTIFY {channel}")?;
50195030
if let Some(payload) = payload {

src/ast/query.rs

+6
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,8 @@ pub enum TableFactor {
999999
with_ordinality: bool,
10001000
/// [Partition selection](https://dev.mysql.com/doc/refman/8.0/en/partitioning-selection.html), supported by MySQL.
10011001
partitions: Vec<Ident>,
1002+
/// Optional PartiQL JsonPath: <https://partiql.org/dql/from.html>
1003+
json_path: Option<JsonPath>,
10021004
},
10031005
Derived {
10041006
lateral: bool,
@@ -1400,8 +1402,12 @@ impl fmt::Display for TableFactor {
14001402
version,
14011403
partitions,
14021404
with_ordinality,
1405+
json_path,
14031406
} => {
14041407
write!(f, "{name}")?;
1408+
if let Some(json_path) = json_path {
1409+
write!(f, "{json_path}")?;
1410+
}
14051411
if !partitions.is_empty() {
14061412
write!(f, "PARTITION ({})", display_comma_separated(partitions))?;
14071413
}

src/ast/spans.rs

+3
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ impl Spanned for Statement {
371371
.chain(core::iter::once(query.span()))
372372
.chain(with_options.iter().map(|i| i.span())),
373373
),
374+
// These statements need to be implemented
374375
Statement::AlterRole { .. } => Span::empty(),
375376
Statement::AttachDatabase { .. } => Span::empty(),
376377
Statement::AttachDuckDBDatabase { .. } => Span::empty(),
@@ -441,6 +442,7 @@ impl Spanned for Statement {
441442
Statement::LISTEN { .. } => Span::empty(),
442443
Statement::NOTIFY { .. } => Span::empty(),
443444
Statement::LoadData { .. } => Span::empty(),
445+
Statement::UNLISTEN { .. } => Span::empty(),
444446
}
445447
}
446448
}
@@ -1636,6 +1638,7 @@ impl Spanned for TableFactor {
16361638
version: _,
16371639
with_ordinality: _,
16381640
partitions: _,
1641+
json_path: _,
16391642
} => union_spans(
16401643
name.0
16411644
.iter()

src/dialect/mod.rs

+8-7
Original file line numberDiff line numberDiff line change
@@ -633,13 +633,8 @@ pub trait Dialect: Debug + Any {
633633
false
634634
}
635635

636-
/// Returns true if the dialect supports the `LISTEN` statement
637-
fn supports_listen(&self) -> bool {
638-
false
639-
}
640-
641-
/// Returns true if the dialect supports the `NOTIFY` statement
642-
fn supports_notify(&self) -> bool {
636+
/// Returns true if the dialect supports the `LISTEN`, `UNLISTEN` and `NOTIFY` statements
637+
fn supports_listen_notify(&self) -> bool {
643638
false
644639
}
645640

@@ -680,6 +675,12 @@ pub trait Dialect: Debug + Any {
680675
fn supports_create_table_select(&self) -> bool {
681676
false
682677
}
678+
679+
/// Returns true if the dialect supports PartiQL for querying semi-structured data
680+
/// <https://partiql.org/index.html>
681+
fn supports_partiql(&self) -> bool {
682+
false
683+
}
683684
}
684685

685686
/// This represents the operators for which precedence must be defined

src/dialect/postgresql.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,9 @@ impl Dialect for PostgreSqlDialect {
191191
}
192192

193193
/// see <https://www.postgresql.org/docs/current/sql-listen.html>
194-
fn supports_listen(&self) -> bool {
195-
true
196-
}
197-
194+
/// see <https://www.postgresql.org/docs/current/sql-unlisten.html>
198195
/// see <https://www.postgresql.org/docs/current/sql-notify.html>
199-
fn supports_notify(&self) -> bool {
196+
fn supports_listen_notify(&self) -> bool {
200197
true
201198
}
202199

@@ -209,6 +206,11 @@ impl Dialect for PostgreSqlDialect {
209206
fn supports_comment_on(&self) -> bool {
210207
true
211208
}
209+
210+
/// See <https://www.postgresql.org/docs/current/sql-load.html>
211+
fn supports_load_extension(&self) -> bool {
212+
true
213+
}
212214
}
213215

214216
pub fn parse_create(parser: &mut Parser) -> Option<Result<Statement, ParserError>> {

src/dialect/redshift.rs

+5
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,9 @@ impl Dialect for RedshiftSqlDialect {
7474
fn supports_top_before_distinct(&self) -> bool {
7575
true
7676
}
77+
78+
/// Redshift supports PartiQL: <https://docs.aws.amazon.com/redshift/latest/dg/super-overview.html>
79+
fn supports_partiql(&self) -> bool {
80+
true
81+
}
7782
}

src/keywords.rs

+1
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ define_keywords!(
799799
UNION,
800800
UNIQUE,
801801
UNKNOWN,
802+
UNLISTEN,
802803
UNLOAD,
803804
UNLOCK,
804805
UNLOGGED,

src/parser/mod.rs

+62-20
Original file line numberDiff line numberDiff line change
@@ -533,10 +533,11 @@ impl<'a> Parser<'a> {
533533
Keyword::EXECUTE | Keyword::EXEC => self.parse_execute(),
534534
Keyword::PREPARE => self.parse_prepare(),
535535
Keyword::MERGE => self.parse_merge(),
536-
// `LISTEN` and `NOTIFY` are Postgres-specific
536+
// `LISTEN`, `UNLISTEN` and `NOTIFY` are Postgres-specific
537537
// syntaxes. They are used for Postgres statement.
538-
Keyword::LISTEN if self.dialect.supports_listen() => self.parse_listen(),
539-
Keyword::NOTIFY if self.dialect.supports_notify() => self.parse_notify(),
538+
Keyword::LISTEN if self.dialect.supports_listen_notify() => self.parse_listen(),
539+
Keyword::UNLISTEN if self.dialect.supports_listen_notify() => self.parse_unlisten(),
540+
Keyword::NOTIFY if self.dialect.supports_listen_notify() => self.parse_notify(),
540541
// `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html
541542
Keyword::PRAGMA => self.parse_pragma(),
542543
Keyword::UNLOAD => self.parse_unload(),
@@ -1003,6 +1004,21 @@ impl<'a> Parser<'a> {
10031004
Ok(Statement::LISTEN { channel })
10041005
}
10051006

1007+
pub fn parse_unlisten(&mut self) -> Result<Statement, ParserError> {
1008+
let channel = if self.consume_token(&Token::Mul) {
1009+
Ident::new(Expr::Wildcard(AttachedToken::empty()).to_string())
1010+
} else {
1011+
match self.parse_identifier(false) {
1012+
Ok(expr) => expr,
1013+
_ => {
1014+
self.prev_token();
1015+
return self.expected("wildcard or identifier", self.peek_token());
1016+
}
1017+
}
1018+
};
1019+
Ok(Statement::UNLISTEN { channel })
1020+
}
1021+
10061022
pub fn parse_notify(&mut self) -> Result<Statement, ParserError> {
10071023
let channel = self.parse_identifier(false)?;
10081024
let payload = if self.consume_token(&Token::Comma) {
@@ -2927,7 +2943,7 @@ impl<'a> Parser<'a> {
29272943
} else if Token::LBracket == tok {
29282944
if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) {
29292945
self.parse_subscript(expr)
2930-
} else if dialect_of!(self is SnowflakeDialect) {
2946+
} else if dialect_of!(self is SnowflakeDialect) || self.dialect.supports_partiql() {
29312947
self.prev_token();
29322948
self.parse_json_access(expr)
29332949
} else {
@@ -3063,6 +3079,14 @@ impl<'a> Parser<'a> {
30633079
}
30643080

30653081
fn parse_json_access(&mut self, expr: Expr) -> Result<Expr, ParserError> {
3082+
let path = self.parse_json_path()?;
3083+
Ok(Expr::JsonAccess {
3084+
value: Box::new(expr),
3085+
path,
3086+
})
3087+
}
3088+
3089+
fn parse_json_path(&mut self) -> Result<JsonPath, ParserError> {
30663090
let mut path = Vec::new();
30673091
loop {
30683092
match self.next_token().token {
@@ -3086,10 +3110,7 @@ impl<'a> Parser<'a> {
30863110
}
30873111

30883112
debug_assert!(!path.is_empty());
3089-
Ok(Expr::JsonAccess {
3090-
value: Box::new(expr),
3091-
path: JsonPath { path },
3092-
})
3113+
Ok(JsonPath { path })
30933114
}
30943115

30953116
pub fn parse_map_access(&mut self, expr: Expr) -> Result<Expr, ParserError> {
@@ -3522,16 +3543,11 @@ impl<'a> Parser<'a> {
35223543
// e.g. `SELECT 1, 2, FROM t`
35233544
// https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#trailing_commas
35243545
// https://docs.snowflake.com/en/release-notes/2024/8_11#select-supports-trailing-commas
3525-
//
3526-
// This pattern could be captured better with RAII type semantics, but it's quite a bit of
3527-
// code to add for just one case, so we'll just do it manually here.
3528-
let old_value = self.options.trailing_commas;
3529-
self.options.trailing_commas |= self.dialect.supports_projection_trailing_commas();
35303546

3531-
let ret = self.parse_comma_separated(|p| p.parse_select_item());
3532-
self.options.trailing_commas = old_value;
3547+
let trailing_commas =
3548+
self.options.trailing_commas | self.dialect.supports_projection_trailing_commas();
35333549

3534-
ret
3550+
self.parse_comma_separated_with_trailing_commas(|p| p.parse_select_item(), trailing_commas)
35353551
}
35363552

35373553
pub fn parse_actions_list(&mut self) -> Result<Vec<ParsedAction>, ParserError> {
@@ -3558,11 +3574,12 @@ impl<'a> Parser<'a> {
35583574
}
35593575

35603576
/// Parse the comma of a comma-separated syntax element.
3577+
/// Allows for control over trailing commas
35613578
/// Returns true if there is a next element
3562-
fn is_parse_comma_separated_end(&mut self) -> bool {
3579+
fn is_parse_comma_separated_end_with_trailing_commas(&mut self, trailing_commas: bool) -> bool {
35633580
if !self.consume_token(&Token::Comma) {
35643581
true
3565-
} else if self.options.trailing_commas {
3582+
} else if trailing_commas {
35663583
let token = self.peek_token().token;
35673584
match token {
35683585
Token::Word(ref kw)
@@ -3580,15 +3597,34 @@ impl<'a> Parser<'a> {
35803597
}
35813598
}
35823599

3600+
/// Parse the comma of a comma-separated syntax element.
3601+
/// Returns true if there is a next element
3602+
fn is_parse_comma_separated_end(&mut self) -> bool {
3603+
self.is_parse_comma_separated_end_with_trailing_commas(self.options.trailing_commas)
3604+
}
3605+
35833606
/// Parse a comma-separated list of 1+ items accepted by `F`
3584-
pub fn parse_comma_separated<T, F>(&mut self, mut f: F) -> Result<Vec<T>, ParserError>
3607+
pub fn parse_comma_separated<T, F>(&mut self, f: F) -> Result<Vec<T>, ParserError>
3608+
where
3609+
F: FnMut(&mut Parser<'a>) -> Result<T, ParserError>,
3610+
{
3611+
self.parse_comma_separated_with_trailing_commas(f, self.options.trailing_commas)
3612+
}
3613+
3614+
/// Parse a comma-separated list of 1+ items accepted by `F`
3615+
/// Allows for control over trailing commas
3616+
fn parse_comma_separated_with_trailing_commas<T, F>(
3617+
&mut self,
3618+
mut f: F,
3619+
trailing_commas: bool,
3620+
) -> Result<Vec<T>, ParserError>
35853621
where
35863622
F: FnMut(&mut Parser<'a>) -> Result<T, ParserError>,
35873623
{
35883624
let mut values = vec![];
35893625
loop {
35903626
values.push(f(self)?);
3591-
if self.is_parse_comma_separated_end() {
3627+
if self.is_parse_comma_separated_end_with_trailing_commas(trailing_commas) {
35923628
break;
35933629
}
35943630
}
@@ -10326,6 +10362,11 @@ impl<'a> Parser<'a> {
1032610362
} else {
1032710363
let name = self.parse_object_name(true)?;
1032810364

10365+
let json_path = match self.peek_token().token {
10366+
Token::LBracket if self.dialect.supports_partiql() => Some(self.parse_json_path()?),
10367+
_ => None,
10368+
};
10369+
1032910370
let partitions: Vec<Ident> = if dialect_of!(self is MySqlDialect | GenericDialect)
1033010371
&& self.parse_keyword(Keyword::PARTITION)
1033110372
{
@@ -10368,6 +10409,7 @@ impl<'a> Parser<'a> {
1036810409
version,
1036910410
partitions,
1037010411
with_ordinality,
10412+
json_path,
1037110413
};
1037210414

1037310415
while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) {

src/test_utils.rs

+2
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ pub fn table(name: impl Into<String>) -> TableFactor {
345345
version: None,
346346
partitions: vec![],
347347
with_ordinality: false,
348+
json_path: None,
348349
}
349350
}
350351

@@ -360,6 +361,7 @@ pub fn table_with_alias(name: impl Into<String>, alias: impl Into<String>) -> Ta
360361
version: None,
361362
partitions: vec![],
362363
with_ordinality: false,
364+
json_path: None,
363365
}
364366
}
365367

src/tokenizer.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -786,8 +786,9 @@ impl<'a> Tokenizer<'a> {
786786
}
787787
Ok(Some(Token::Whitespace(Whitespace::Newline)))
788788
}
789-
// BigQuery uses b or B for byte string literal
790-
b @ 'B' | b @ 'b' if dialect_of!(self is BigQueryDialect | GenericDialect) => {
789+
// BigQuery and MySQL use b or B for byte string literal, Postgres for bit strings
790+
b @ 'B' | b @ 'b' if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | MySqlDialect | GenericDialect) =>
791+
{
791792
chars.next(); // consume
792793
match chars.peek() {
793794
Some('\'') => {

0 commit comments

Comments
 (0)