From 495d0a02d5680c93a8886fde7cec4d9998c9b595 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 2 Oct 2023 13:10:56 +0200 Subject: [PATCH 01/11] Add support for ATTACH DATABASE (#989) --- src/ast/mod.rs | 18 ++++++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 13 +++++++++++++ tests/sqlparser_sqlite.rs | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb8830bb1..48274f68c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1429,6 +1429,16 @@ pub enum Statement { name: Ident, operation: AlterRoleOperation, }, + /// ATTACH DATABASE 'path/to/file' AS alias + /// (SQLite-specific) + AttachDatabase { + /// The name to bind to the newly attached database + schema_name: Ident, + /// An expression that indicates the path to the database file + database_file_name: Expr, + /// true if the syntax is 'ATTACH DATABASE', false if it's just 'ATTACH' + database: bool, + }, /// DROP Drop { /// The type of the object to drop: TABLE, VIEW, etc. @@ -1969,6 +1979,14 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::AttachDatabase { + schema_name, + database_file_name, + database, + } => { + let keyword = if *database { "DATABASE " } else { "" }; + write!(f, "ATTACH {keyword}{database_file_name} AS {schema_name}") + } Statement::Analyze { table_name, partitions, diff --git a/src/keywords.rs b/src/keywords.rs index ad0526ccd..eee961350 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -95,6 +95,7 @@ define_keywords!( ASYMMETRIC, AT, ATOMIC, + ATTACH, AUTHORIZATION, AUTOINCREMENT, AUTO_INCREMENT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ba8f5784f..49cd24899 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -456,6 +456,7 @@ impl<'a> Parser<'a> { Ok(Statement::Query(Box::new(self.parse_query()?))) } Keyword::TRUNCATE => Ok(self.parse_truncate()?), + Keyword::ATTACH => Ok(self.parse_attach_database()?), Keyword::MSCK => Ok(self.parse_msck()?), Keyword::CREATE => Ok(self.parse_create()?), Keyword::CACHE => Ok(self.parse_cache_table()?), @@ -543,6 +544,18 @@ impl<'a> Parser<'a> { }) } + pub fn parse_attach_database(&mut self) -> Result { + let database = self.parse_keyword(Keyword::DATABASE); + let database_file_name = self.parse_expr()?; + self.expect_keyword(Keyword::AS)?; + let schema_name = self.parse_identifier()?; + Ok(Statement::AttachDatabase { + database, + schema_name, + database_file_name, + }) + } + pub fn parse_analyze(&mut self) -> Result { self.expect_keyword(Keyword::TABLE)?; let table_name = self.parse_object_name()?; diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index fd7a22461..c4e69d530 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -259,6 +259,24 @@ fn parse_create_table_with_strict() { } } +#[test] +fn parse_attach_database() { + let sql = "ATTACH DATABASE 'test.db' AS test"; + let verified_stmt = sqlite().verified_stmt(sql); + assert_eq!(sql, format!("{}", verified_stmt)); + match verified_stmt { + Statement::AttachDatabase { + schema_name, + database_file_name: Expr::Value(Value::SingleQuotedString(literal_name)), + database: true, + } => { + assert_eq!(schema_name.value, "test"); + assert_eq!(literal_name, "test.db"); + } + _ => unreachable!(), + } +} + fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], From e718ce6c42365d4fb987ce4977716c28091020a5 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 13:23:25 +0200 Subject: [PATCH 02/11] bigquery: EXTRACT support For DAYOFWEEK, DAYOFYEAR, ISOWEEK, TIME (#980) --- src/ast/value.rs | 8 ++++++++ src/keywords.rs | 3 +++ src/parser/mod.rs | 4 ++++ tests/sqlparser_common.rs | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/src/ast/value.rs b/src/ast/value.rs index 9c18a325c..e6f139256 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -117,6 +117,8 @@ pub enum DateTimeField { Month, Week, Day, + DayOfWeek, + DayOfYear, Date, Hour, Minute, @@ -127,6 +129,7 @@ pub enum DateTimeField { Doy, Epoch, Isodow, + IsoWeek, Isoyear, Julian, Microsecond, @@ -138,6 +141,7 @@ pub enum DateTimeField { Nanosecond, Nanoseconds, Quarter, + Time, Timezone, TimezoneHour, TimezoneMinute, @@ -151,6 +155,8 @@ impl fmt::Display for DateTimeField { DateTimeField::Month => "MONTH", DateTimeField::Week => "WEEK", DateTimeField::Day => "DAY", + DateTimeField::DayOfWeek => "DAYOFWEEK", + DateTimeField::DayOfYear => "DAYOFYEAR", DateTimeField::Date => "DATE", DateTimeField::Hour => "HOUR", DateTimeField::Minute => "MINUTE", @@ -162,6 +168,7 @@ impl fmt::Display for DateTimeField { DateTimeField::Epoch => "EPOCH", DateTimeField::Isodow => "ISODOW", DateTimeField::Isoyear => "ISOYEAR", + DateTimeField::IsoWeek => "ISOWEEK", DateTimeField::Julian => "JULIAN", DateTimeField::Microsecond => "MICROSECOND", DateTimeField::Microseconds => "MICROSECONDS", @@ -172,6 +179,7 @@ impl fmt::Display for DateTimeField { DateTimeField::Nanosecond => "NANOSECOND", DateTimeField::Nanoseconds => "NANOSECONDS", DateTimeField::Quarter => "QUARTER", + DateTimeField::Time => "TIME", DateTimeField::Timezone => "TIMEZONE", DateTimeField::TimezoneHour => "TIMEZONE_HOUR", DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE", diff --git a/src/keywords.rs b/src/keywords.rs index eee961350..6fb74a8e0 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -196,6 +196,8 @@ define_keywords!( DATE, DATETIME, DAY, + DAYOFWEEK, + DAYOFYEAR, DEALLOCATE, DEC, DECADE, @@ -334,6 +336,7 @@ define_keywords!( IS, ISODOW, ISOLATION, + ISOWEEK, ISOYEAR, JAR, JOIN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 49cd24899..279abf968 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1508,6 +1508,8 @@ impl<'a> Parser<'a> { Keyword::MONTH => Ok(DateTimeField::Month), Keyword::WEEK => Ok(DateTimeField::Week), Keyword::DAY => Ok(DateTimeField::Day), + Keyword::DAYOFWEEK => Ok(DateTimeField::DayOfWeek), + Keyword::DAYOFYEAR => Ok(DateTimeField::DayOfYear), Keyword::DATE => Ok(DateTimeField::Date), Keyword::HOUR => Ok(DateTimeField::Hour), Keyword::MINUTE => Ok(DateTimeField::Minute), @@ -1519,6 +1521,7 @@ impl<'a> Parser<'a> { Keyword::EPOCH => Ok(DateTimeField::Epoch), Keyword::ISODOW => Ok(DateTimeField::Isodow), Keyword::ISOYEAR => Ok(DateTimeField::Isoyear), + Keyword::ISOWEEK => Ok(DateTimeField::IsoWeek), Keyword::JULIAN => Ok(DateTimeField::Julian), Keyword::MICROSECOND => Ok(DateTimeField::Microsecond), Keyword::MICROSECONDS => Ok(DateTimeField::Microseconds), @@ -1529,6 +1532,7 @@ impl<'a> Parser<'a> { Keyword::NANOSECOND => Ok(DateTimeField::Nanosecond), Keyword::NANOSECONDS => Ok(DateTimeField::Nanoseconds), Keyword::QUARTER => Ok(DateTimeField::Quarter), + Keyword::TIME => Ok(DateTimeField::Time), Keyword::TIMEZONE => Ok(DateTimeField::Timezone), Keyword::TIMEZONE_HOUR => Ok(DateTimeField::TimezoneHour), Keyword::TIMEZONE_MINUTE => Ok(DateTimeField::TimezoneMinute), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6f780de9e..a9ce3cd6c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2069,6 +2069,8 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(MONTH FROM d)"); verified_stmt("SELECT EXTRACT(WEEK FROM d)"); verified_stmt("SELECT EXTRACT(DAY FROM d)"); + verified_stmt("SELECT EXTRACT(DAYOFWEEK FROM d)"); + verified_stmt("SELECT EXTRACT(DAYOFYEAR FROM d)"); verified_stmt("SELECT EXTRACT(DATE FROM d)"); verified_stmt("SELECT EXTRACT(HOUR FROM d)"); verified_stmt("SELECT EXTRACT(MINUTE FROM d)"); @@ -2082,6 +2084,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(DOY FROM d)"); verified_stmt("SELECT EXTRACT(EPOCH FROM d)"); verified_stmt("SELECT EXTRACT(ISODOW FROM d)"); + verified_stmt("SELECT EXTRACT(ISOWEEK FROM d)"); verified_stmt("SELECT EXTRACT(ISOYEAR FROM d)"); verified_stmt("SELECT EXTRACT(JULIAN FROM d)"); verified_stmt("SELECT EXTRACT(MICROSECOND FROM d)"); @@ -2094,6 +2097,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(TIMEZONE FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE_HOUR FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE_MINUTE FROM d)"); + verified_stmt("SELECT EXTRACT(TIME FROM d)"); let res = parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)"); assert_eq!( From 4903bd4b8b8b615a2d1eb0bfad028952321ede56 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 13:39:44 +0200 Subject: [PATCH 03/11] Add test for clickhouse: tokenize `==` as Token::DoubleEq (#981) --- src/test_utils.rs | 3 +++ src/tokenizer.rs | 24 +++++++++++++++++++++++- tests/sqlparser_clickhouse.rs | 8 ++++++++ tests/sqlparser_common.rs | 4 ++-- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index b81cd5f4e..8c64bfacd 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -31,6 +31,9 @@ use crate::parser::{Parser, ParserError}; use crate::tokenizer::Tokenizer; use crate::{ast::*, parser::ParserOptions}; +#[cfg(test)] +use pretty_assertions::assert_eq; + /// Tests use the methods on this struct to invoke the parser on one or /// multiple dialects. pub struct TestedDialects { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 175b5d3b1..067aa5a84 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1368,7 +1368,7 @@ fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool #[cfg(test)] mod tests { use super::*; - use crate::dialect::{GenericDialect, MsSqlDialect}; + use crate::dialect::{ClickHouseDialect, GenericDialect, MsSqlDialect}; #[test] fn tokenizer_error_impl() { @@ -1414,6 +1414,28 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_clickhouse_double_equal() { + let sql = String::from("SELECT foo=='1'"); + let dialect = ClickHouseDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Word(Word { + value: "foo".to_string(), + quote_style: None, + keyword: Keyword::NoKeyword, + }), + Token::DoubleEq, + Token::SingleQuotedString("1".to_string()), + ]; + + compare(expected, tokens); + } + #[test] fn tokenize_select_exponent() { let sql = String::from("SELECT 1e10, 1e-10, 1e+10, 1ea, 1e-10a, 1e-10-10"); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index a14598b3d..936b0799a 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -336,6 +336,14 @@ fn parse_create_table() { ); } +#[test] +fn parse_double_equal() { + clickhouse().one_statement_parses_to( + r#"SELECT foo FROM bar WHERE buz == 'buz'"#, + r#"SELECT foo FROM bar WHERE buz = 'buz'"#, + ); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a9ce3cd6c..46503c7f2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6792,10 +6792,10 @@ fn parse_time_functions() { // Validating Parenthesis let sql_without_parens = format!("SELECT {}", func_name); - let mut ast_without_parens = select_localtime_func_call_ast.clone(); + let mut ast_without_parens = select_localtime_func_call_ast; ast_without_parens.special = true; assert_eq!( - &Expr::Function(ast_without_parens.clone()), + &Expr::Function(ast_without_parens), expr_from_projection(&verified_only_select(&sql_without_parens).projection[0]) ); } From ed39329060bbc34d4fc7655f0d9973dacdea5534 Mon Sep 17 00:00:00 2001 From: William Date: Mon, 2 Oct 2023 08:36:17 -0400 Subject: [PATCH 04/11] Add JumpWire to users in README (#990) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1195cc941..454ea6c29 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ $ cargo run --features json_example --example cli FILENAME.sql [--dialectname] ## Users This parser is currently being used by the [DataFusion] query engine, -[LocustDB], [Ballista], [GlueSQL], and [Opteryx]. +[LocustDB], [Ballista], [GlueSQL], [Opteryx], and [JumpWire]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -179,6 +179,7 @@ licensed as above, without any additional terms or conditions. [Ballista]: https://github.com/apache/arrow-ballista [GlueSQL]: https://github.com/gluesql/gluesql [Opteryx]: https://github.com/mabel-dev/opteryx +[JumpWire]: https://github.com/extragoodlabs/jumpwire [Pratt Parser]: https://tdop.github.io/ [sql-2016-grammar]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html [sql-standard]: https://en.wikipedia.org/wiki/ISO/IEC_9075 From 6ffc3b3a52c6dc49d4f62733ddf3451ff3f9039d Mon Sep 17 00:00:00 2001 From: Ulrich Schmidt-Goertz Date: Mon, 2 Oct 2023 14:42:58 +0200 Subject: [PATCH 05/11] Support DELETE with ORDER BY and LIMIT (MySQL) (#992) --- src/ast/mod.rs | 12 ++++++++++++ src/parser/mod.rs | 13 ++++++++++++- tests/sqlparser_common.rs | 2 ++ tests/sqlparser_mysql.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 48274f68c..f2dbb8899 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1301,6 +1301,10 @@ pub enum Statement { selection: Option, /// RETURNING returning: Option>, + /// ORDER BY (MySQL) + order_by: Vec, + /// LIMIT (MySQL) + limit: Option, }, /// CREATE VIEW CreateView { @@ -2141,6 +2145,8 @@ impl fmt::Display for Statement { using, selection, returning, + order_by, + limit, } => { write!(f, "DELETE ")?; if !tables.is_empty() { @@ -2156,6 +2162,12 @@ impl fmt::Display for Statement { if let Some(returning) = returning { write!(f, " RETURNING {}", display_comma_separated(returning))?; } + if !order_by.is_empty() { + write!(f, " ORDER BY {}", display_comma_separated(order_by))?; + } + if let Some(limit) = limit { + write!(f, " LIMIT {limit}")?; + } Ok(()) } Statement::Close { cursor } => { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 279abf968..a3ebcc475 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5306,12 +5306,21 @@ impl<'a> Parser<'a> { } else { None }; - let returning = if self.parse_keyword(Keyword::RETURNING) { Some(self.parse_comma_separated(Parser::parse_select_item)?) } else { None }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + let limit = if self.parse_keyword(Keyword::LIMIT) { + self.parse_limit()? + } else { + None + }; Ok(Statement::Delete { tables, @@ -5319,6 +5328,8 @@ impl<'a> Parser<'a> { using, selection, returning, + order_by, + limit, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 46503c7f2..d73061f79 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -525,6 +525,7 @@ fn parse_where_delete_statement() { using, selection, returning, + .. } => { assert_eq!( TableFactor::Table { @@ -565,6 +566,7 @@ fn parse_where_delete_with_alias_statement() { using, selection, returning, + .. } => { assert_eq!( TableFactor::Table { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 80ef9f981..f1a054bfb 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1315,6 +1315,38 @@ fn parse_update_with_joins() { } } +#[test] +fn parse_delete_with_order_by() { + let sql = "DELETE FROM customers ORDER BY id DESC"; + match mysql().verified_stmt(sql) { + Statement::Delete { order_by, .. } => { + assert_eq!( + vec![OrderByExpr { + expr: Expr::Identifier(Ident { + value: "id".to_owned(), + quote_style: None + }), + asc: Some(false), + nulls_first: None, + }], + order_by + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_delete_with_limit() { + let sql = "DELETE FROM customers LIMIT 100"; + match mysql().verified_stmt(sql) { + Statement::Delete { limit, .. } => { + assert_eq!(Some(Expr::Value(number("100"))), limit); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_drop_primary_key() { assert_matches!( From 993769ec0267e8684a034ebeb268ae0360785822 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Mon, 2 Oct 2023 14:48:51 +0200 Subject: [PATCH 06/11] Add support for mixed BigQuery table name quoting (#971) Co-authored-by: ifeanyi --- src/parser/mod.rs | 21 ++++++++ src/test_utils.rs | 18 +++++++ tests/sqlparser_bigquery.rs | 95 +++++++++++++++++++++++++++++++++++-- 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a3ebcc475..a388a9137 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5041,6 +5041,27 @@ impl<'a> Parser<'a> { break; } } + + // BigQuery accepts any number of quoted identifiers of a table name. + // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#quoted_identifiers + if dialect_of!(self is BigQueryDialect) + && idents.iter().any(|ident| ident.value.contains('.')) + { + idents = idents + .into_iter() + .flat_map(|ident| { + ident + .value + .split('.') + .map(|value| Ident { + value: value.into(), + quote_style: ident.quote_style, + }) + .collect::>() + }) + .collect() + } + Ok(ObjectName(idents)) } diff --git a/src/test_utils.rs b/src/test_utils.rs index 8c64bfacd..f0c5e425a 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -162,6 +162,24 @@ impl TestedDialects { } } + /// Ensures that `sql` parses as a single [`Select`], and that additionally: + /// + /// 1. parsing `sql` results in the same [`Statement`] as parsing + /// `canonical`. + /// + /// 2. re-serializing the result of parsing `sql` produces the same + /// `canonical` sql string + pub fn verified_only_select_with_canonical(&self, query: &str, canonical: &str) -> Select { + let q = match self.one_statement_parses_to(query, canonical) { + Statement::Query(query) => *query, + _ => panic!("Expected Query"), + }; + match *q.body { + SetExpr::Select(s) => *s, + _ => panic!("Expected SetExpr::Select"), + } + } + /// Ensures that `sql` parses as an [`Expr`], and that /// re-serializing the parse result produces the same `sql` /// string (is not modified after a serialization round-trip). diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3502d7dfa..e05581d5f 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -13,6 +13,8 @@ #[macro_use] mod test_utils; +use std::ops::Deref; + use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use test_utils::*; @@ -84,9 +86,24 @@ fn parse_raw_literal() { #[test] fn parse_table_identifiers() { - fn test_table_ident(ident: &str, expected: Vec) { + /// Parses a table identifier ident and verifies that re-serializing the + /// parsed identifier produces the original ident string. + /// + /// In some cases, re-serializing the result of the parsed ident is not + /// expected to produce the original ident string. canonical is provided + /// instead as the canonical representation of the identifier for comparison. + /// For example, re-serializing the result of ident `foo.bar` produces + /// the equivalent canonical representation `foo`.`bar` + fn test_table_ident(ident: &str, canonical: Option<&str>, expected: Vec) { let sql = format!("SELECT 1 FROM {ident}"); - let select = bigquery().verified_only_select(&sql); + let canonical = canonical.map(|ident| format!("SELECT 1 FROM {ident}")); + + let select = if let Some(canonical) = canonical { + bigquery().verified_only_select_with_canonical(&sql, canonical.deref()) + } else { + bigquery().verified_only_select(&sql) + }; + assert_eq!( select.from, vec![TableWithJoins { @@ -102,26 +119,30 @@ fn parse_table_identifiers() { },] ); } + fn test_table_ident_err(ident: &str) { let sql = format!("SELECT 1 FROM {ident}"); assert!(bigquery().parse_sql_statements(&sql).is_err()); } - test_table_ident("da-sh-es", vec![Ident::new("da-sh-es")]); + test_table_ident("da-sh-es", None, vec![Ident::new("da-sh-es")]); - test_table_ident("`spa ce`", vec![Ident::with_quote('`', "spa ce")]); + test_table_ident("`spa ce`", None, vec![Ident::with_quote('`', "spa ce")]); test_table_ident( "`!@#$%^&*()-=_+`", + None, vec![Ident::with_quote('`', "!@#$%^&*()-=_+")], ); test_table_ident( "_5abc.dataField", + None, vec![Ident::new("_5abc"), Ident::new("dataField")], ); test_table_ident( "`5abc`.dataField", + None, vec![Ident::with_quote('`', "5abc"), Ident::new("dataField")], ); @@ -129,6 +150,7 @@ fn parse_table_identifiers() { test_table_ident( "abc5.dataField", + None, vec![Ident::new("abc5"), Ident::new("dataField")], ); @@ -136,13 +158,76 @@ fn parse_table_identifiers() { test_table_ident( "`GROUP`.dataField", + None, vec![Ident::with_quote('`', "GROUP"), Ident::new("dataField")], ); // TODO: this should be error // test_table_ident_err("GROUP.dataField"); - test_table_ident("abc5.GROUP", vec![Ident::new("abc5"), Ident::new("GROUP")]); + test_table_ident( + "abc5.GROUP", + None, + vec![Ident::new("abc5"), Ident::new("GROUP")], + ); + + test_table_ident( + "`foo.bar.baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`foo.bar`.`baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`foo`.`bar.baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`foo`.`bar`.`baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`5abc.dataField`", + Some("`5abc`.`dataField`"), + vec![ + Ident::with_quote('`', "5abc"), + Ident::with_quote('`', "dataField"), + ], + ); + + test_table_ident( + "`_5abc.da-sh-es`", + Some("`_5abc`.`da-sh-es`"), + vec![ + Ident::with_quote('`', "_5abc"), + Ident::with_quote('`', "da-sh-es"), + ], + ); } #[test] From 2786c7eaf1d0b420ca5a5f338b45b6cc0fbd68be Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 17:53:32 +0200 Subject: [PATCH 07/11] clickhouse: add support for LIMIT BY (#977) --- src/ast/query.rs | 7 +++++++ src/parser/mod.rs | 13 ++++++++++++- tests/sqlparser_clickhouse.rs | 18 ++++++++++++++++++ tests/sqlparser_common.rs | 5 +++++ tests/sqlparser_mssql.rs | 3 +++ tests/sqlparser_mysql.rs | 9 +++++++++ tests/sqlparser_postgres.rs | 2 ++ 7 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index af35c37a3..d5f170791 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -35,6 +35,10 @@ pub struct Query { pub order_by: Vec, /// `LIMIT { | ALL }` pub limit: Option, + + /// `LIMIT { } BY { ,,... } }` + pub limit_by: Vec, + /// `OFFSET [ { ROW | ROWS } ]` pub offset: Option, /// `FETCH { FIRST | NEXT } [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }` @@ -58,6 +62,9 @@ impl fmt::Display for Query { if let Some(ref offset) = self.offset { write!(f, " {offset}")?; } + if !self.limit_by.is_empty() { + write!(f, " BY {}", display_separated(&self.limit_by, ", "))?; + } if let Some(ref fetch) = self.fetch { write!(f, " {fetch}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a388a9137..dcd731a65 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5431,6 +5431,7 @@ impl<'a> Parser<'a> { with, body: Box::new(SetExpr::Insert(insert)), limit: None, + limit_by: vec![], order_by: vec![], offset: None, fetch: None, @@ -5442,6 +5443,7 @@ impl<'a> Parser<'a> { with, body: Box::new(SetExpr::Update(update)), limit: None, + limit_by: vec![], order_by: vec![], offset: None, fetch: None, @@ -5468,7 +5470,7 @@ impl<'a> Parser<'a> { offset = Some(self.parse_offset()?) } - if dialect_of!(self is GenericDialect | MySqlDialect) + if dialect_of!(self is GenericDialect | MySqlDialect | ClickHouseDialect) && limit.is_some() && offset.is_none() && self.consume_token(&Token::Comma) @@ -5483,6 +5485,14 @@ impl<'a> Parser<'a> { } } + let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keyword(Keyword::BY) + { + self.parse_comma_separated(Parser::parse_expr)? + } else { + vec![] + }; + let fetch = if self.parse_keyword(Keyword::FETCH) { Some(self.parse_fetch()?) } else { @@ -5499,6 +5509,7 @@ impl<'a> Parser<'a> { body, order_by, limit, + limit_by, offset, fetch, locks, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 936b0799a..9efe4a368 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -25,6 +25,7 @@ use sqlparser::ast::TableFactor::Table; use sqlparser::ast::*; use sqlparser::dialect::ClickHouseDialect; +use sqlparser::dialect::GenericDialect; #[test] fn parse_map_access_expr() { @@ -344,9 +345,26 @@ fn parse_double_equal() { ); } +#[test] +fn parse_limit_by() { + clickhouse_and_generic().verified_stmt( + r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC LIMIT 1 BY asset"#, + ); + clickhouse_and_generic().verified_stmt( + r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC LIMIT 1 BY asset, toStartOfDay(created_at)"#, + ); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], options: None, } } + +fn clickhouse_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(ClickHouseDialect {}), Box::new(GenericDialect {})], + options: None, + } +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d73061f79..80e4cdf02 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -261,6 +261,7 @@ fn parse_update_set_from() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -2662,6 +2663,7 @@ fn parse_create_table_as_table() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -2685,6 +2687,7 @@ fn parse_create_table_as_table() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -3976,6 +3979,7 @@ fn parse_interval_and_or_xor() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -6392,6 +6396,7 @@ fn parse_merge() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 135e5d138..f9eb4d8fb 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -92,6 +92,7 @@ fn parse_create_procedure() { body: vec![Statement::Query(Box::new(Query { with: None, limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -493,6 +494,7 @@ fn parse_substring_in_select() { assert_eq!( Box::new(Query { with: None, + body: Box::new(SetExpr::Select(Box::new(Select { distinct: Some(Distinct::Distinct), top: None, @@ -532,6 +534,7 @@ fn parse_substring_in_select() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f1a054bfb..80b9dcfd8 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -562,6 +562,7 @@ fn parse_escaped_quote_identifiers_with_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -604,6 +605,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -643,6 +645,7 @@ fn parse_escaped_backticks_with_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -682,6 +685,7 @@ fn parse_escaped_backticks_with_no_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -956,6 +960,7 @@ fn parse_simple_insert() { })), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -991,6 +996,7 @@ fn parse_empty_row_insert() { })), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -1049,6 +1055,7 @@ fn parse_insert_with_on_duplicate_update() { })), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -1428,6 +1435,7 @@ fn parse_substring_in_select() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -1708,6 +1716,7 @@ fn parse_hex_string_introducer() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bb3857817..fe336bda7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1000,6 +1000,7 @@ fn parse_copy_to() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -2046,6 +2047,7 @@ fn parse_array_subquery_expr() { }), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], From 40e2ecbdf34f0068863ed74b0ae3a8eb410c6401 Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Mon, 2 Oct 2023 10:28:13 -0700 Subject: [PATCH 08/11] snowflake: support for UNPIVOT and a fix for chained PIVOTs (#983) --- src/ast/query.rs | 67 ++++++++++------ src/keywords.rs | 2 + src/parser/mod.rs | 54 +++++++++---- tests/sqlparser_common.rs | 155 +++++++++++++++++++++++++++++++++++--- 4 files changed, 231 insertions(+), 47 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index d5f170791..88b0931de 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -720,13 +720,28 @@ pub enum TableFactor { /// For example `FROM monthly_sales PIVOT(sum(amount) FOR MONTH IN ('JAN', 'FEB'))` /// See Pivot { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - name: ObjectName, - table_alias: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_table_factor"))] + table: Box, aggregate_function: Expr, // Function expression value_column: Vec, pivot_values: Vec, - pivot_alias: Option, + alias: Option, + }, + /// An UNPIVOT operation on a table. + /// + /// Syntax: + /// ```sql + /// table UNPIVOT(value FOR name IN (column1, [ column2, ... ])) [ alias ] + /// ``` + /// + /// See . + Unpivot { + #[cfg_attr(feature = "visitor", visit(with = "visit_table_factor"))] + table: Box, + value: Ident, + name: Ident, + columns: Vec, + alias: Option, }, } @@ -810,32 +825,42 @@ impl fmt::Display for TableFactor { Ok(()) } TableFactor::Pivot { - name, - table_alias, + table, aggregate_function, value_column, pivot_values, - pivot_alias, + alias, } => { - write!(f, "{}", name)?; - if table_alias.is_some() { - write!(f, " AS {}", table_alias.as_ref().unwrap())?; - } write!( f, - " PIVOT({} FOR {} IN (", + "{} PIVOT({} FOR {} IN ({}))", + table, aggregate_function, - Expr::CompoundIdentifier(value_column.to_vec()) + Expr::CompoundIdentifier(value_column.to_vec()), + display_comma_separated(pivot_values) )?; - for value in pivot_values { - write!(f, "{}", value)?; - if !value.eq(pivot_values.last().unwrap()) { - write!(f, ", ")?; - } + if alias.is_some() { + write!(f, " AS {}", alias.as_ref().unwrap())?; } - write!(f, "))")?; - if pivot_alias.is_some() { - write!(f, " AS {}", pivot_alias.as_ref().unwrap())?; + Ok(()) + } + TableFactor::Unpivot { + table, + value, + name, + columns, + alias, + } => { + write!( + f, + "{} UNPIVOT({} FOR {} IN ({}))", + table, + value, + name, + display_comma_separated(columns) + )?; + if alias.is_some() { + write!(f, " AS {}", alias.as_ref().unwrap())?; } Ok(()) } diff --git a/src/keywords.rs b/src/keywords.rs index 6fb74a8e0..d85708032 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -635,6 +635,7 @@ define_keywords!( UNKNOWN, UNLOGGED, UNNEST, + UNPIVOT, UNSIGNED, UNTIL, UPDATE, @@ -693,6 +694,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::HAVING, Keyword::ORDER, Keyword::PIVOT, + Keyword::UNPIVOT, Keyword::TOP, Keyword::LATERAL, Keyword::VIEW, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dcd731a65..45600f42d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6276,9 +6276,8 @@ impl<'a> Parser<'a> { | TableFactor::Table { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::TableFunction { alias, .. } - | TableFactor::Pivot { - pivot_alias: alias, .. - } + | TableFactor::Pivot { alias, .. } + | TableFactor::Unpivot { alias, .. } | TableFactor::NestedJoin { alias, .. } => { // but not `FROM (mytable AS alias1) AS alias2`. if let Some(inner_alias) = alias { @@ -6357,11 +6356,6 @@ impl<'a> Parser<'a> { let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; - // Pivot - if self.parse_keyword(Keyword::PIVOT) { - return self.parse_pivot_table_factor(name, alias); - } - // MSSQL-specific table hints: let mut with_hints = vec![]; if self.parse_keyword(Keyword::WITH) { @@ -6373,14 +6367,25 @@ impl<'a> Parser<'a> { self.prev_token(); } }; - Ok(TableFactor::Table { + + let mut table = TableFactor::Table { name, alias, args, with_hints, version, partitions, - }) + }; + + while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { + table = match kw { + Keyword::PIVOT => self.parse_pivot_table_factor(table)?, + Keyword::UNPIVOT => self.parse_unpivot_table_factor(table)?, + _ => unreachable!(), + } + } + + Ok(table) } } @@ -6417,8 +6422,7 @@ impl<'a> Parser<'a> { pub fn parse_pivot_table_factor( &mut self, - name: ObjectName, - table_alias: Option, + table: TableFactor, ) -> Result { self.expect_token(&Token::LParen)?; let function_name = match self.next_token().token { @@ -6435,12 +6439,32 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; Ok(TableFactor::Pivot { - name, - table_alias, + table: Box::new(table), aggregate_function: function, value_column, pivot_values, - pivot_alias: alias, + alias, + }) + } + + pub fn parse_unpivot_table_factor( + &mut self, + table: TableFactor, + ) -> Result { + self.expect_token(&Token::LParen)?; + let value = self.parse_identifier()?; + self.expect_keyword(Keyword::FOR)?; + let name = self.parse_identifier()?; + self.expect_keyword(Keyword::IN)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + self.expect_token(&Token::RParen)?; + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + Ok(TableFactor::Unpivot { + table: Box::new(table), + value, + name, + columns, + alias, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 80e4cdf02..3c4a2d9ea 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20,7 +20,7 @@ use matches::assert_matches; use sqlparser::ast::SelectItem::UnnamedExpr; -use sqlparser::ast::TableFactor::Pivot; +use sqlparser::ast::TableFactor::{Pivot, Unpivot}; use sqlparser::ast::*; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, DuckDbDialect, GenericDialect, HiveDialect, @@ -7257,10 +7257,16 @@ fn parse_pivot_table() { assert_eq!( verified_only_select(sql).from[0].relation, Pivot { - name: ObjectName(vec![Ident::new("monthly_sales")]), - table_alias: Some(TableAlias { - name: Ident::new("a"), - columns: vec![] + table: Box::new(TableFactor::Table { + name: ObjectName(vec![Ident::new("monthly_sales")]), + alias: Some(TableAlias { + name: Ident::new("a"), + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], }), aggregate_function: Expr::Function(Function { name: ObjectName(vec![Ident::new("SUM")]), @@ -7279,7 +7285,7 @@ fn parse_pivot_table() { Value::SingleQuotedString("MAR".to_string()), Value::SingleQuotedString("APR".to_string()), ], - pivot_alias: Some(TableAlias { + alias: Some(TableAlias { name: Ident { value: "p".to_string(), quote_style: None @@ -7290,17 +7296,15 @@ fn parse_pivot_table() { ); assert_eq!(verified_stmt(sql).to_string(), sql); + // parsing should succeed with empty alias let sql_without_table_alias = concat!( "SELECT * FROM monthly_sales ", "PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ", "ORDER BY EMPID" ); assert_matches!( - verified_only_select(sql_without_table_alias).from[0].relation, - Pivot { - table_alias: None, // parsing should succeed with empty alias - .. - } + &verified_only_select(sql_without_table_alias).from[0].relation, + Pivot { table, .. } if matches!(&**table, TableFactor::Table { alias: None, .. }) ); assert_eq!( verified_stmt(sql_without_table_alias).to_string(), @@ -7308,6 +7312,135 @@ fn parse_pivot_table() { ); } +#[test] +fn parse_unpivot_table() { + let sql = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + + pretty_assertions::assert_eq!( + verified_only_select(sql).from[0].relation, + Unpivot { + table: Box::new(TableFactor::Table { + name: ObjectName(vec![Ident::new("sales")]), + alias: Some(TableAlias { + name: Ident::new("s"), + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }), + value: Ident { + value: "quantity".to_string(), + quote_style: None + }, + + name: Ident { + value: "quarter".to_string(), + quote_style: None + }, + columns: ["Q1", "Q2", "Q3", "Q4"] + .into_iter() + .map(Ident::new) + .collect(), + alias: Some(TableAlias { + name: Ident::new("u"), + columns: ["product", "quarter", "quantity"] + .into_iter() + .map(Ident::new) + .collect() + }), + } + ); + assert_eq!(verified_stmt(sql).to_string(), sql); + + let sql_without_aliases = concat!( + "SELECT * FROM sales ", + "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4))" + ); + + assert_matches!( + &verified_only_select(sql_without_aliases).from[0].relation, + Unpivot { + table, + alias: None, + .. + } if matches!(&**table, TableFactor::Table { alias: None, .. }) + ); + assert_eq!( + verified_stmt(sql_without_aliases).to_string(), + sql_without_aliases + ); +} + +#[test] +fn parse_pivot_unpivot_table() { + let sql = concat!( + "SELECT * FROM census AS c ", + "UNPIVOT(population FOR year IN (population_2000, population_2010)) AS u ", + "PIVOT(sum(population) FOR year IN ('population_2000', 'population_2010')) AS p" + ); + + pretty_assertions::assert_eq!( + verified_only_select(sql).from[0].relation, + Pivot { + table: Box::new(Unpivot { + table: Box::new(TableFactor::Table { + name: ObjectName(vec![Ident::new("census")]), + alias: Some(TableAlias { + name: Ident::new("c"), + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }), + value: Ident { + value: "population".to_string(), + quote_style: None + }, + + name: Ident { + value: "year".to_string(), + quote_style: None + }, + columns: ["population_2000", "population_2010"] + .into_iter() + .map(Ident::new) + .collect(), + alias: Some(TableAlias { + name: Ident::new("u"), + columns: vec![] + }), + }), + aggregate_function: Expr::Function(Function { + name: ObjectName(vec![Ident::new("sum")]), + args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("population")) + ))]), + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + value_column: vec![Ident::new("year")], + pivot_values: vec![ + Value::SingleQuotedString("population_2000".to_string()), + Value::SingleQuotedString("population_2010".to_string()) + ], + alias: Some(TableAlias { + name: Ident::new("p"), + columns: vec![] + }), + } + ); + assert_eq!(verified_stmt(sql).to_string(), sql); +} + /// Makes a predicate that looks like ((user_id = $id) OR user_id = $2...) fn make_where_clause(num: usize) -> String { use std::fmt::Write; From c811e2260505e2651b04e85e886ec09d237a0602 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 19:42:01 +0200 Subject: [PATCH 09/11] =?UTF-8?q?redshift:=20add=20support=20for=20CREATE?= =?UTF-8?q?=20VIEW=20=E2=80=A6=20WITH=20NO=20SCHEMA=20BINDING=20(#979)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ast/mod.rs | 11 +++++++++-- src/keywords.rs | 1 + src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_common.rs | 12 ++++++++++++ tests/sqlparser_redshift.rs | 14 ++++++++++++++ 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f2dbb8899..6f9d32c8d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -577,7 +577,7 @@ pub enum Expr { /// /// Syntax: /// ```sql - /// MARCH (, , ...) AGAINST ( []) + /// MATCH (, , ...) AGAINST ( []) /// /// = CompoundIdentifier /// = String literal @@ -1316,6 +1316,8 @@ pub enum Statement { query: Box, with_options: Vec, cluster_by: Vec, + /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause + with_no_schema_binding: bool, }, /// CREATE TABLE CreateTable { @@ -2271,6 +2273,7 @@ impl fmt::Display for Statement { materialized, with_options, cluster_by, + with_no_schema_binding, } => { write!( f, @@ -2288,7 +2291,11 @@ impl fmt::Display for Statement { if !cluster_by.is_empty() { write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; } - write!(f, " AS {query}") + write!(f, " AS {query}")?; + if *with_no_schema_binding { + write!(f, " WITH NO SCHEMA BINDING")?; + } + Ok(()) } Statement::CreateTable { name, diff --git a/src/keywords.rs b/src/keywords.rs index d85708032..e1bbf44ae 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -110,6 +110,7 @@ define_keywords!( BIGINT, BIGNUMERIC, BINARY, + BINDING, BLOB, BLOOMFILTER, BOOL, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 45600f42d..5f6788696 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2974,6 +2974,15 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::AS)?; let query = Box::new(self.parse_query()?); // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. + + let with_no_schema_binding = dialect_of!(self is RedshiftSqlDialect | GenericDialect) + && self.parse_keywords(&[ + Keyword::WITH, + Keyword::NO, + Keyword::SCHEMA, + Keyword::BINDING, + ]); + Ok(Statement::CreateView { name, columns, @@ -2982,6 +2991,7 @@ impl<'a> Parser<'a> { or_replace, with_options, cluster_by, + with_no_schema_binding, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c4a2d9ea..027dc312f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5320,6 +5320,7 @@ fn parse_create_view() { materialized, with_options, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5328,6 +5329,7 @@ fn parse_create_view() { assert!(!or_replace); assert_eq!(with_options, vec![]); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5368,6 +5370,7 @@ fn parse_create_view_with_columns() { query, materialized, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); @@ -5376,6 +5379,7 @@ fn parse_create_view_with_columns() { assert!(!materialized); assert!(!or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5393,6 +5397,7 @@ fn parse_create_or_replace_view() { query, materialized, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5401,6 +5406,7 @@ fn parse_create_or_replace_view() { assert!(!materialized); assert!(or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5422,6 +5428,7 @@ fn parse_create_or_replace_materialized_view() { query, materialized, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5430,6 +5437,7 @@ fn parse_create_or_replace_materialized_view() { assert!(materialized); assert!(or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5447,6 +5455,7 @@ fn parse_create_materialized_view() { materialized, with_options, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5455,6 +5464,7 @@ fn parse_create_materialized_view() { assert_eq!(with_options, vec![]); assert!(!or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5472,6 +5482,7 @@ fn parse_create_materialized_view_with_cluster_by() { materialized, with_options, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5480,6 +5491,7 @@ fn parse_create_materialized_view_with_cluster_by() { assert_eq!(with_options, vec![]); assert!(!or_replace); assert_eq!(cluster_by, vec![Ident::new("foo")]); + assert!(!late_binding); } _ => unreachable!(), } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index f17ca5841..5ae539b3c 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -16,6 +16,7 @@ mod test_utils; use test_utils::*; use sqlparser::ast::*; +use sqlparser::dialect::GenericDialect; use sqlparser::dialect::RedshiftSqlDialect; #[test] @@ -272,6 +273,13 @@ fn redshift() -> TestedDialects { } } +fn redshift_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(RedshiftSqlDialect {}), Box::new(GenericDialect {})], + options: None, + } +} + #[test] fn test_sharp() { let sql = "SELECT #_of_values"; @@ -281,3 +289,9 @@ fn test_sharp() { select.projection[0] ); } + +#[test] +fn test_create_view_with_no_schema_binding() { + redshift_and_generic() + .verified_stmt("CREATE VIEW myevent AS SELECT eventname FROM event WITH NO SCHEMA BINDING"); +} From 02f3d78a920ad90a91d318fbb3b08890c53e1a28 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 5 Oct 2023 15:25:03 -0400 Subject: [PATCH 10/11] Fix for clippy 1.73 (#995) --- src/ast/mod.rs | 2 +- src/ast/visitor.rs | 2 +- src/parser/mod.rs | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6f9d32c8d..d4e2f26ea 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -155,7 +155,7 @@ impl fmt::Display for Ident { let escaped = value::escape_quoted_string(&self.value, q); write!(f, "{q}{escaped}{q}") } - Some(q) if q == '[' => write!(f, "[{}]", self.value), + Some('[') => write!(f, "[{}]", self.value), None => f.write_str(&self.value), _ => panic!("unexpected quote style"), } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index bb7c19678..09cb20a0c 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -490,7 +490,7 @@ where /// /// This demonstrates how to effectively replace an expression with another more complicated one /// that references the original. This example avoids unnecessary allocations by using the -/// [`std::mem`](std::mem) family of functions. +/// [`std::mem`] family of functions. /// /// ``` /// # use sqlparser::parser::Parser; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5f6788696..d0b3cef78 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4701,7 +4701,11 @@ impl<'a> Parser<'a> { pub fn parse_literal_string(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { - Token::Word(Word { value, keyword, .. }) if keyword == Keyword::NoKeyword => Ok(value), + Token::Word(Word { + value, + keyword: Keyword::NoKeyword, + .. + }) => Ok(value), Token::SingleQuotedString(s) => Ok(s), Token::DoubleQuotedString(s) => Ok(s), Token::EscapedStringLiteral(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { @@ -5853,8 +5857,8 @@ impl<'a> Parser<'a> { self.expect_token(&Token::Colon)?; } else if self.parse_keyword(Keyword::ROLE) { let context_modifier = match modifier { - Some(keyword) if keyword == Keyword::LOCAL => ContextModifier::Local, - Some(keyword) if keyword == Keyword::SESSION => ContextModifier::Session, + Some(Keyword::LOCAL) => ContextModifier::Local, + Some(Keyword::SESSION) => ContextModifier::Session, _ => ContextModifier::None, }; @@ -6897,7 +6901,7 @@ impl<'a> Parser<'a> { } } - /// Parse an [`WildcardAdditionalOptions`](WildcardAdditionalOptions) information for wildcard select items. + /// Parse an [`WildcardAdditionalOptions`] information for wildcard select items. /// /// If it is not possible to parse it, will return an option. pub fn parse_wildcard_additional_options( From 5263da68cdaa052dfd4f8989760569eae253253e Mon Sep 17 00:00:00 2001 From: Gabriel Villalonga Simon Date: Thu, 5 Oct 2023 20:32:43 +0100 Subject: [PATCH 11/11] Handle CREATE [TEMPORARY|TEMP] VIEW [IF NOT EXISTS] (#993) --- src/ast/mod.rs | 12 +++++++-- src/parser/mod.rs | 12 +++++++-- tests/sqlparser_common.rs | 55 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_sqlite.rs | 31 ++++++++++++++++++++++ 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d4e2f26ea..d048ccc1b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1318,6 +1318,10 @@ pub enum Statement { cluster_by: Vec, /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause with_no_schema_binding: bool, + /// if true, has SQLite `IF NOT EXISTS` clause + if_not_exists: bool, + /// if true, has SQLite `TEMP` or `TEMPORARY` clause + temporary: bool, }, /// CREATE TABLE CreateTable { @@ -2274,13 +2278,17 @@ impl fmt::Display for Statement { with_options, cluster_by, with_no_schema_binding, + if_not_exists, + temporary, } => { write!( f, - "CREATE {or_replace}{materialized}VIEW {name}", + "CREATE {or_replace}{materialized}{temporary}VIEW {if_not_exists}{name}", or_replace = if *or_replace { "OR REPLACE " } else { "" }, materialized = if *materialized { "MATERIALIZED " } else { "" }, - name = name + name = name, + temporary = if *temporary { "TEMPORARY " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } )?; if !with_options.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_options))?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d0b3cef78..922a791f3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2478,7 +2478,7 @@ impl<'a> Parser<'a> { self.parse_create_table(or_replace, temporary, global, transient) } else if self.parse_keyword(Keyword::MATERIALIZED) || self.parse_keyword(Keyword::VIEW) { self.prev_token(); - self.parse_create_view(or_replace) + self.parse_create_view(or_replace, temporary) } else if self.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { @@ -2955,9 +2955,15 @@ impl<'a> Parser<'a> { } } - pub fn parse_create_view(&mut self, or_replace: bool) -> Result { + pub fn parse_create_view( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { let materialized = self.parse_keyword(Keyword::MATERIALIZED); self.expect_keyword(Keyword::VIEW)?; + let if_not_exists = dialect_of!(self is SQLiteDialect|GenericDialect) + && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let name = self.parse_object_name()?; @@ -2992,6 +2998,8 @@ impl<'a> Parser<'a> { with_options, cluster_by, with_no_schema_binding, + if_not_exists, + temporary, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 027dc312f..c0ec456a9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5321,6 +5321,8 @@ fn parse_create_view() { with_options, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5330,6 +5332,8 @@ fn parse_create_view() { assert_eq!(with_options, vec![]); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5371,6 +5375,8 @@ fn parse_create_view_with_columns() { materialized, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); @@ -5380,6 +5386,39 @@ fn parse_create_view_with_columns() { assert!(!or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_view_temporary() { + let sql = "CREATE TEMPORARY VIEW myschema.myview AS SELECT foo FROM bar"; + match verified_stmt(sql) { + Statement::CreateView { + name, + columns, + query, + or_replace, + materialized, + with_options, + cluster_by, + with_no_schema_binding: late_binding, + if_not_exists, + temporary, + } => { + assert_eq!("myschema.myview", name.to_string()); + assert_eq!(Vec::::new(), columns); + assert_eq!("SELECT foo FROM bar", query.to_string()); + assert!(!materialized); + assert!(!or_replace); + assert_eq!(with_options, vec![]); + assert_eq!(cluster_by, vec![]); + assert!(!late_binding); + assert!(!if_not_exists); + assert!(temporary); } _ => unreachable!(), } @@ -5398,6 +5437,8 @@ fn parse_create_or_replace_view() { materialized, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5407,6 +5448,8 @@ fn parse_create_or_replace_view() { assert!(or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5429,6 +5472,8 @@ fn parse_create_or_replace_materialized_view() { materialized, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5438,6 +5483,8 @@ fn parse_create_or_replace_materialized_view() { assert!(or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5456,6 +5503,8 @@ fn parse_create_materialized_view() { with_options, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5465,6 +5514,8 @@ fn parse_create_materialized_view() { assert!(!or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5483,6 +5534,8 @@ fn parse_create_materialized_view_with_cluster_by() { with_options, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5492,6 +5545,8 @@ fn parse_create_materialized_view_with_cluster_by() { assert!(!or_replace); assert_eq!(cluster_by, vec![Ident::new("foo")]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index c4e69d530..39a82cc8b 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -61,6 +61,37 @@ fn parse_create_virtual_table() { sqlite_and_generic().verified_stmt(sql); } +#[test] +fn parse_create_view_temporary_if_not_exists() { + let sql = "CREATE TEMPORARY VIEW IF NOT EXISTS myschema.myview AS SELECT foo FROM bar"; + match sqlite_and_generic().verified_stmt(sql) { + Statement::CreateView { + name, + columns, + query, + or_replace, + materialized, + with_options, + cluster_by, + with_no_schema_binding: late_binding, + if_not_exists, + temporary, + } => { + assert_eq!("myschema.myview", name.to_string()); + assert_eq!(Vec::::new(), columns); + assert_eq!("SELECT foo FROM bar", query.to_string()); + assert!(!materialized); + assert!(!or_replace); + assert_eq!(with_options, vec![]); + assert_eq!(cluster_by, vec![]); + assert!(!late_binding); + assert!(if_not_exists); + assert!(temporary); + } + _ => unreachable!(), + } +} + #[test] fn double_equality_operator() { // Sqlite supports this operator: https://www.sqlite.org/lang_expr.html#binaryops