From 658ebcd729356ed9d9fede632a128a9b85205f7a Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Tue, 1 Apr 2025 20:40:11 +0200 Subject: [PATCH 01/17] added functionality to handle output statement --- src/ast/mod.rs | 39 +++++++++++++++++++++++++++++++++++-- src/keywords.rs | 1 + src/parser/mod.rs | 34 +++++++++++++++++++++++++++++++- tests/sqlparser_bigquery.rs | 2 ++ tests/sqlparser_common.rs | 17 ++++++++++++++++ tests/sqlparser_mssql.rs | 6 ++++++ 6 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f187df995..60b22a627 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3828,6 +3828,8 @@ pub enum Statement { on: Box, /// Specifies the actions to perform when values match or do not match. clauses: Vec, + + output: Option, }, /// ```sql /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] @@ -5406,7 +5408,8 @@ impl fmt::Display for Statement { table, source, on, - clauses, + clauses, + output } => { write!( f, @@ -5414,7 +5417,12 @@ impl fmt::Display for Statement { int = if *into { " INTO" } else { "" } )?; write!(f, "ON {on} ")?; - write!(f, "{}", display_separated(clauses, " ")) + write!(f, "{}", display_separated(clauses, " "))?; + if output.is_some() { + let out = output.clone().unwrap(); + write!(f, " {out}")?; + } + Ok(()) } Statement::Cache { table_name, @@ -7900,6 +7908,33 @@ impl Display for MergeClause { } } +/// A Output in the end of a 'MERGE' Statement +/// +/// Example: +/// OUTPUT $action, deleted.* INTO dbo.temp_products; +/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql) + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Output { + pub select_items: Vec, + pub into_table: SelectInto, +} + +impl fmt::Display for Output { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let Output { select_items, into_table } = self; + + write!( + f, + "OUTPUT {} {}", + display_comma_separated(select_items), + into_table + ) + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/keywords.rs b/src/keywords.rs index 8609ec43d..112566d00 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -632,6 +632,7 @@ define_keywords!( ORGANIZATION, OUT, OUTER, + OUTPUT, OUTPUTFORMAT, OVER, OVERFLOW, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2b61529ff..be8d30d2b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14489,7 +14489,10 @@ impl<'a> Parser<'a> { pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { let mut clauses = vec![]; loop { - if self.peek_token() == Token::EOF || self.peek_token() == Token::SemiColon { + if self.peek_token() == Token::EOF + || self.peek_token() == Token::SemiColon + || self.peek_keyword(Keyword::OUTPUT) + { break; } self.expect_keyword_is(Keyword::WHEN)?; @@ -14586,6 +14589,29 @@ impl<'a> Parser<'a> { Ok(clauses) } + pub fn parse_output(&mut self) -> Result { + self.expect_keyword_is(Keyword::OUTPUT)?; + let select_items = self.parse_projection()?; + self.expect_keyword_is(Keyword::INTO)?; + let temporary = self + .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) + .is_some(); + let unlogged = self.parse_keyword(Keyword::UNLOGGED); + let table = self.parse_keyword(Keyword::TABLE); + let name = self.parse_object_name(false)?; + let into_table = SelectInto { + temporary, + unlogged, + table, + name, + }; + + Ok(Output { + select_items, + into_table, + }) + } + pub fn parse_merge(&mut self) -> Result { let into = self.parse_keyword(Keyword::INTO); @@ -14596,6 +14622,11 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::ON)?; let on = self.parse_expr()?; let clauses = self.parse_merge_clauses()?; + let output = if self.peek_keyword(Keyword::OUTPUT) { + Some(self.parse_output()?) + } else { + None + }; Ok(Statement::Merge { into, @@ -14603,6 +14634,7 @@ impl<'a> Parser<'a> { source, on: Box::new(on), clauses, + output, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3037d4ae5..5eb30d15c 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1735,6 +1735,7 @@ fn parse_merge() { }, ], }; + match bigquery_and_generic().verified_stmt(sql) { Statement::Merge { into, @@ -1742,6 +1743,7 @@ fn parse_merge() { source, on, clauses, + .. } => { assert!(!into); assert_eq!( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 795dae4b3..ca64785df 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9359,6 +9359,7 @@ fn parse_merge() { source, on, clauses, + .. }, Statement::Merge { into: no_into, @@ -9366,6 +9367,7 @@ fn parse_merge() { source: source_no_into, on: on_no_into, clauses: clauses_no_into, + .. }, ) => { assert!(into); @@ -9556,6 +9558,21 @@ fn parse_merge() { let sql = "MERGE INTO s.bar AS dest USING newArrivals AS S ON (1 > 1) WHEN NOT MATCHED THEN INSERT VALUES (stg.A, stg.B, stg.C)"; verified_stmt(sql); + +} + +#[test] +fn test_merge_with_output() { + + let sql = "MERGE INTO target_table USING source_table \ + ON target_table.id = source_table.oooid \ + WHEN MATCHED THEN \ + UPDATE SET target_table.description = source_table.description \ + WHEN NOT MATCHED THEN \ + INSERT (ID, description) VALUES (source_table.id, source_table.description) \ + OUTPUT inserted.* INTO log_target"; + + verified_stmt(sql); } #[test] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2bfc38a6a..7eb771065 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1921,3 +1921,9 @@ fn ms() -> TestedDialects { fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } + +#[test] +fn parse_mssql_merge_with_output() { + let stmt = "MERGE dso.products AS t USING dsi.products AS s ON s.ProductID = t.ProductID WHEN MATCHED AND NOT (t.ProductName = s.ProductName OR (ISNULL(t.ProductName, s.ProductName) IS NULL)) THEN UPDATE SET t.ProductName = s.ProductName WHEN NOT MATCHED BY TARGET THEN INSERT (ProductID, ProductName) VALUES (s.ProductID, s.ProductName) WHEN NOT MATCHED BY SOURCE THEN DELETE OUTPUT $action, deleted.ProductID INTO dsi.temp_products"; + ms_and_generic().verified_stmt(stmt); +} From 76f7b2a7f5b2ce3c63b0378d84570ea796cab148 Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Tue, 1 Apr 2025 20:44:47 +0200 Subject: [PATCH 02/17] clippy and fmt --- src/ast/mod.rs | 9 ++++++--- tests/sqlparser_common.rs | 2 -- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 60b22a627..b18a8160f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5408,8 +5408,8 @@ impl fmt::Display for Statement { table, source, on, - clauses, - output + clauses, + output, } => { write!( f, @@ -7924,7 +7924,10 @@ pub struct Output { impl fmt::Display for Output { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let Output { select_items, into_table } = self; + let Output { + select_items, + into_table, + } = self; write!( f, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ca64785df..d13f711f8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9558,12 +9558,10 @@ fn parse_merge() { let sql = "MERGE INTO s.bar AS dest USING newArrivals AS S ON (1 > 1) WHEN NOT MATCHED THEN INSERT VALUES (stg.A, stg.B, stg.C)"; verified_stmt(sql); - } #[test] fn test_merge_with_output() { - let sql = "MERGE INTO target_table USING source_table \ ON target_table.id = source_table.oooid \ WHEN MATCHED THEN \ From 3a294777975f881d0e6334053f1452c091e8b9e5 Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Wed, 2 Apr 2025 13:42:57 +0200 Subject: [PATCH 03/17] added documentation --- src/ast/mod.rs | 217 +++++++++++++++++++++++++------------------------ 1 file changed, 109 insertions(+), 108 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b18a8160f..ad8302ec8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -699,7 +699,7 @@ pub enum Expr { IsNotTrue(Box), /// `IS NULL` operator IsNull(Box), - /// `IS NOT NULL` operator + /// `IS NOT NULL` operatoDeleter IsNotNull(Box), /// `IS UNKNOWN` operator IsUnknown(Box), @@ -2678,120 +2678,120 @@ pub enum Set { /// /// Note: this is a PostgreSQL-specific statements /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL - /// However, we allow it for all dialects. - SetTimeZone { local: bool, value: Expr }, +/// Convenience function, instead of writing `Statement::Set(Set::Set...{...})` +impl From for Statement { + fn from(set: Set) -> Self { + Statement::Set(set) + } +} + +/// A top-level statement (SELECT, INSERT, CREATE, etc.) +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "visitor", + derive(Visit, VisitMut), + visit(with = "visit_statement") +)] +pub enum Statement { /// ```sql - /// SET NAMES 'charset_name' [COLLATE 'collation_name'] + /// ANALYZE /// ``` - SetNames { - charset_name: Ident, - collation_name: Option, + /// Analyze (Hive) + Analyze { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + table_name: ObjectName, + partitions: Option>, + for_columns: bool, + columns: Vec, + cache_metadata: bool, + noscan: bool, + compute_statistics: bool, + has_table_keyword: bool, }, + Set(Set), /// ```sql - /// SET NAMES DEFAULT + /// TRUNCATE /// ``` - /// - /// Note: this is a MySQL-specific statement. - SetNamesDefault {}, + /// Truncate (Hive) + Truncate { + table_names: Vec, + partitions: Option>, + /// TABLE - optional keyword; + table: bool, + /// Postgres-specific option + /// [ TRUNCATE TABLE ONLY ] + only: bool, + /// Postgres-specific option + /// [ RESTART IDENTITY | CONTINUE IDENTITY ] + identity: Option, + /// Postgres-specific option + /// [ CASCADE | RESTRICT ] + cascade: Option, + /// ClickHouse-specific option + /// [ ON CLUSTER cluster_name ] + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) + on_cluster: Option, + }, /// ```sql - /// SET TRANSACTION ... + /// MSCK /// ``` - SetTransaction { - modes: Vec, - snapshot: Option, - session: bool, + /// Msck (Hive) + Msck { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + table_name: ObjectName, + repair: bool, + partition_action: Option, }, -} - -impl Display for Set { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::ParenthesizedAssignments { variables, values } => write!( - f, - "SET ({}) = ({})", - display_comma_separated(variables), - display_comma_separated(values) - ), - Self::MultipleAssignments { assignments } => { - write!(f, "SET {}", display_comma_separated(assignments)) - } - Self::SetRole { - context_modifier, - role_name, - } => { - let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); - write!( - f, - "SET {modifier}ROLE {role_name}", - modifier = context_modifier - .map(|m| format!("{}", m)) - .unwrap_or_default() - ) - } - Self::SetSessionParam(kind) => write!(f, "SET {kind}"), - Self::SetTransaction { - modes, - snapshot, - session, - } => { - if *session { - write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?; - } else { - write!(f, "SET TRANSACTION")?; - } - if !modes.is_empty() { - write!(f, " {}", display_comma_separated(modes))?; - } - if let Some(snapshot_id) = snapshot { - write!(f, " SNAPSHOT {snapshot_id}")?; - } - Ok(()) - } - Self::SetTimeZone { local, value } => { - f.write_str("SET ")?; - if *local { - f.write_str("LOCAL ")?; - } - write!(f, "TIME ZONE {value}") - } - Self::SetNames { - charset_name, - collation_name, - } => { - write!(f, "SET NAMES {}", charset_name)?; - - if let Some(collation) = collation_name { - f.write_str(" COLLATE ")?; - f.write_str(collation)?; - }; - - Ok(()) - } - Self::SetNamesDefault {} => { - f.write_str("SET NAMES DEFAULT")?; - - Ok(()) - } - Set::SingleAssignment { - scope, - hivevar, - variable, - values, - } => { - write!( - f, - "SET {}{}{} = {}", - scope.map(|s| format!("{}", s)).unwrap_or_default(), - if *hivevar { "HIVEVAR:" } else { "" }, - variable, - display_comma_separated(values) - ) - } - } - } -} - -/// Convert a `Set` into a `Statement`. + /// ```sql + /// SELECT + /// ``` + Query(Box), + /// ```sql + /// INSERT + /// ``` + Insert(Insert), + /// ```sql + /// INSTALL + /// ``` + Install { + /// Only for DuckDB + extension_name: Ident, + }, + /// ```sql + /// LOAD + /// ``` + Load { + /// Only for DuckDB + extension_name: Ident, + }, + // TODO: Support ROW FORMAT + Directory { + overwrite: bool, + local: bool, + path: String, + file_format: Option, + source: Box, + }, + /// A `CASE` statement. + Case(CaseStatement), + /// An `IF` statement. + If(IfStatement), + /// A `RAISE` statement. + Raise(RaiseStatement), + /// ```sql + /// CALL + /// ``` + Call(Function), + /// ```sql + /// COPY [TO | FROM] ... + /// ``` + Copy { + /// The source of 'COPY TO', or the target of 'COPY FROM' + source: CopySource, + /// If true, is a 'COPY TO' statement. If false is a 'COPY FROM' /// Convenience function, instead of writing `Statement::Set(Set::Set...{...})` impl From for Statement { fn from(set: Set) -> Self { @@ -3817,6 +3817,7 @@ pub enum Statement { /// ``` /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + /// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql) Merge { /// optional INTO keyword into: bool, @@ -3828,7 +3829,7 @@ pub enum Statement { on: Box, /// Specifies the actions to perform when values match or do not match. clauses: Vec, - + /// Specifies the ouptu clause, to save changed values output: Option, }, /// ```sql From 9ff8e1851b96ea9c322767163ca18532185c0dfa Mon Sep 17 00:00:00 2001 From: LFC <990479+MichaelScofield@users.noreply.github.com> Date: Wed, 2 Apr 2025 01:06:19 +0800 Subject: [PATCH 04/17] Add GreptimeDB to the "Users" in README (#1788) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e8e460f6..6acfbcef7 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,8 @@ $ 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], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], [ParadeDB] and [CipherStash Proxy]. +[Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], [ParadeDB], [CipherStash Proxy], +and [GreptimeDB]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -276,3 +277,4 @@ licensed as above, without any additional terms or conditions. [`Dialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/trait.Dialect.html [`GenericDialect`]: https://docs.rs/sqlparser/latest/sqlparser/dialect/struct.GenericDialect.html [CipherStash Proxy]: https://github.com/cipherstash/proxy +[GreptimeDB]: https://github.com/GreptimeTeam/greptimedb From 9be6b276c24f0670570e0694d3eda67bf909e1d4 Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Wed, 2 Apr 2025 13:59:39 +0200 Subject: [PATCH 05/17] Revert "added documentation" This reverts commit 3a294777975f881d0e6334053f1452c091e8b9e5. The commit has a lot of stuff I don't think i've added --- src/ast/mod.rs | 217 ++++++++++++++++++++++++------------------------- 1 file changed, 108 insertions(+), 109 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ad8302ec8..b18a8160f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -699,7 +699,7 @@ pub enum Expr { IsNotTrue(Box), /// `IS NULL` operator IsNull(Box), - /// `IS NOT NULL` operatoDeleter + /// `IS NOT NULL` operator IsNotNull(Box), /// `IS UNKNOWN` operator IsUnknown(Box), @@ -2678,120 +2678,120 @@ pub enum Set { /// /// Note: this is a PostgreSQL-specific statements /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL -/// Convenience function, instead of writing `Statement::Set(Set::Set...{...})` -impl From for Statement { - fn from(set: Set) -> Self { - Statement::Set(set) - } -} - -/// A top-level statement (SELECT, INSERT, CREATE, etc.) -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr( - feature = "visitor", - derive(Visit, VisitMut), - visit(with = "visit_statement") -)] -pub enum Statement { - /// ```sql - /// ANALYZE - /// ``` - /// Analyze (Hive) - Analyze { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - partitions: Option>, - for_columns: bool, - columns: Vec, - cache_metadata: bool, - noscan: bool, - compute_statistics: bool, - has_table_keyword: bool, - }, - Set(Set), - /// ```sql - /// TRUNCATE - /// ``` - /// Truncate (Hive) - Truncate { - table_names: Vec, - partitions: Option>, - /// TABLE - optional keyword; - table: bool, - /// Postgres-specific option - /// [ TRUNCATE TABLE ONLY ] - only: bool, - /// Postgres-specific option - /// [ RESTART IDENTITY | CONTINUE IDENTITY ] - identity: Option, - /// Postgres-specific option - /// [ CASCADE | RESTRICT ] - cascade: Option, - /// ClickHouse-specific option - /// [ ON CLUSTER cluster_name ] - /// - /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) - on_cluster: Option, - }, + /// However, we allow it for all dialects. + SetTimeZone { local: bool, value: Expr }, /// ```sql - /// MSCK + /// SET NAMES 'charset_name' [COLLATE 'collation_name'] /// ``` - /// Msck (Hive) - Msck { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - repair: bool, - partition_action: Option, + SetNames { + charset_name: Ident, + collation_name: Option, }, /// ```sql - /// SELECT - /// ``` - Query(Box), - /// ```sql - /// INSERT - /// ``` - Insert(Insert), - /// ```sql - /// INSTALL + /// SET NAMES DEFAULT /// ``` - Install { - /// Only for DuckDB - extension_name: Ident, - }, + /// + /// Note: this is a MySQL-specific statement. + SetNamesDefault {}, /// ```sql - /// LOAD + /// SET TRANSACTION ... /// ``` - Load { - /// Only for DuckDB - extension_name: Ident, - }, - // TODO: Support ROW FORMAT - Directory { - overwrite: bool, - local: bool, - path: String, - file_format: Option, - source: Box, + SetTransaction { + modes: Vec, + snapshot: Option, + session: bool, }, - /// A `CASE` statement. - Case(CaseStatement), - /// An `IF` statement. - If(IfStatement), - /// A `RAISE` statement. - Raise(RaiseStatement), - /// ```sql - /// CALL - /// ``` - Call(Function), - /// ```sql - /// COPY [TO | FROM] ... - /// ``` - Copy { - /// The source of 'COPY TO', or the target of 'COPY FROM' - source: CopySource, - /// If true, is a 'COPY TO' statement. If false is a 'COPY FROM' +} + +impl Display for Set { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ParenthesizedAssignments { variables, values } => write!( + f, + "SET ({}) = ({})", + display_comma_separated(variables), + display_comma_separated(values) + ), + Self::MultipleAssignments { assignments } => { + write!(f, "SET {}", display_comma_separated(assignments)) + } + Self::SetRole { + context_modifier, + role_name, + } => { + let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); + write!( + f, + "SET {modifier}ROLE {role_name}", + modifier = context_modifier + .map(|m| format!("{}", m)) + .unwrap_or_default() + ) + } + Self::SetSessionParam(kind) => write!(f, "SET {kind}"), + Self::SetTransaction { + modes, + snapshot, + session, + } => { + if *session { + write!(f, "SET SESSION CHARACTERISTICS AS TRANSACTION")?; + } else { + write!(f, "SET TRANSACTION")?; + } + if !modes.is_empty() { + write!(f, " {}", display_comma_separated(modes))?; + } + if let Some(snapshot_id) = snapshot { + write!(f, " SNAPSHOT {snapshot_id}")?; + } + Ok(()) + } + Self::SetTimeZone { local, value } => { + f.write_str("SET ")?; + if *local { + f.write_str("LOCAL ")?; + } + write!(f, "TIME ZONE {value}") + } + Self::SetNames { + charset_name, + collation_name, + } => { + write!(f, "SET NAMES {}", charset_name)?; + + if let Some(collation) = collation_name { + f.write_str(" COLLATE ")?; + f.write_str(collation)?; + }; + + Ok(()) + } + Self::SetNamesDefault {} => { + f.write_str("SET NAMES DEFAULT")?; + + Ok(()) + } + Set::SingleAssignment { + scope, + hivevar, + variable, + values, + } => { + write!( + f, + "SET {}{}{} = {}", + scope.map(|s| format!("{}", s)).unwrap_or_default(), + if *hivevar { "HIVEVAR:" } else { "" }, + variable, + display_comma_separated(values) + ) + } + } + } +} + +/// Convert a `Set` into a `Statement`. /// Convenience function, instead of writing `Statement::Set(Set::Set...{...})` impl From for Statement { fn from(set: Set) -> Self { @@ -3817,7 +3817,6 @@ pub enum Statement { /// ``` /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) - /// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql) Merge { /// optional INTO keyword into: bool, @@ -3829,7 +3828,7 @@ pub enum Statement { on: Box, /// Specifies the actions to perform when values match or do not match. clauses: Vec, - /// Specifies the ouptu clause, to save changed values + output: Option, }, /// ```sql From 89ee390589325e4c6363cdfb4f69fa30a006737f Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Wed, 2 Apr 2025 14:02:56 +0200 Subject: [PATCH 06/17] adding documentation --- src/ast/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b18a8160f..ccffb0704 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3817,6 +3817,7 @@ pub enum Statement { /// ``` /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + /// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16) Merge { /// optional INTO keyword into: bool, @@ -3828,7 +3829,7 @@ pub enum Statement { on: Box, /// Specifies the actions to perform when values match or do not match. clauses: Vec, - + // Speccifies the output to save changes in MSSQL output: Option, }, /// ```sql From e3924944bbf193102831919656bcc488c87782f5 Mon Sep 17 00:00:00 2001 From: DilovanCelik Date: Wed, 2 Apr 2025 20:32:37 +0200 Subject: [PATCH 07/17] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ccffb0704..9b4c5d0fe 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7914,7 +7914,6 @@ impl Display for MergeClause { /// Example: /// OUTPUT $action, deleted.* INTO dbo.temp_products; /// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql) - #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] From ddb163c55a7299e5d1bb13c838010bfaf712c79c Mon Sep 17 00:00:00 2001 From: DilovanCelik Date: Wed, 2 Apr 2025 20:33:00 +0200 Subject: [PATCH 08/17] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9b4c5d0fe..7189d37fb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7917,7 +7917,7 @@ impl Display for MergeClause { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct Output { +pub struct OutputClause { pub select_items: Vec, pub into_table: SelectInto, } From afc3c5e26c64ef1cef8b99f87ca3ad9eaa2c8443 Mon Sep 17 00:00:00 2001 From: DilovanCelik Date: Wed, 2 Apr 2025 20:33:47 +0200 Subject: [PATCH 09/17] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7189d37fb..aed22ccd4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5419,9 +5419,8 @@ impl fmt::Display for Statement { )?; write!(f, "ON {on} ")?; write!(f, "{}", display_separated(clauses, " "))?; - if output.is_some() { - let out = output.clone().unwrap(); - write!(f, " {out}")?; + if let Some(output) = output { + write!(f, " {output}")?; } Ok(()) } From e6989fc93d3b51a8f2063b854ece9b8619008880 Mon Sep 17 00:00:00 2001 From: DilovanCelik Date: Wed, 2 Apr 2025 20:34:13 +0200 Subject: [PATCH 10/17] Update src/parser/mod.rs Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index be8d30d2b..f65bdd48b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14589,7 +14589,7 @@ impl<'a> Parser<'a> { Ok(clauses) } - pub fn parse_output(&mut self) -> Result { + fn parse_output(&mut self) -> Result { self.expect_keyword_is(Keyword::OUTPUT)?; let select_items = self.parse_projection()?; self.expect_keyword_is(Keyword::INTO)?; From 51990f864a7d9f619e7ee7b910730eadc3d123a3 Mon Sep 17 00:00:00 2001 From: DilovanCelik Date: Wed, 2 Apr 2025 20:36:26 +0200 Subject: [PATCH 11/17] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index aed22ccd4..6b18293c6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3829,7 +3829,7 @@ pub enum Statement { on: Box, /// Specifies the actions to perform when values match or do not match. clauses: Vec, - // Speccifies the output to save changes in MSSQL + // Specifies the output to save changes in MSSQL output: Option, }, /// ```sql From 62b29ea2d50e98602e8733372248ae8b6570972d Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Wed, 2 Apr 2025 20:53:24 +0200 Subject: [PATCH 12/17] change break loop condition and multiline test query --- src/parser/mod.rs | 7 ++----- tests/sqlparser_mssql.rs | 12 +++++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f65bdd48b..59f4cbf0b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14489,13 +14489,10 @@ impl<'a> Parser<'a> { pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { let mut clauses = vec![]; loop { - if self.peek_token() == Token::EOF - || self.peek_token() == Token::SemiColon - || self.peek_keyword(Keyword::OUTPUT) - { + if !(self.parse_keyword(Keyword::WHEN)) { break; } - self.expect_keyword_is(Keyword::WHEN)?; + let mut clause_kind = MergeClauseKind::Matched; if self.parse_keyword(Keyword::NOT) { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7eb771065..a0d26663a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1924,6 +1924,16 @@ fn ms_and_generic() -> TestedDialects { #[test] fn parse_mssql_merge_with_output() { - let stmt = "MERGE dso.products AS t USING dsi.products AS s ON s.ProductID = t.ProductID WHEN MATCHED AND NOT (t.ProductName = s.ProductName OR (ISNULL(t.ProductName, s.ProductName) IS NULL)) THEN UPDATE SET t.ProductName = s.ProductName WHEN NOT MATCHED BY TARGET THEN INSERT (ProductID, ProductName) VALUES (s.ProductID, s.ProductName) WHEN NOT MATCHED BY SOURCE THEN DELETE OUTPUT $action, deleted.ProductID INTO dsi.temp_products"; + let stmt = "MERGE dso.products AS t\ + USING dsi.products AS \ + s ON s.ProductID = t.ProductID \ + WHEN MATCHED AND \ + NOT (t.ProductName = s.ProductName OR (ISNULL(t.ProductName, s.ProductName) IS NULL)) \ + THEN UPDATE SET t.ProductName = s.ProductName \ + WHEN NOT MATCHED BY TARGET \ + THEN INSERT (ProductID, ProductName) \ + VALUES (s.ProductID, s.ProductName) \ + WHEN NOT MATCHED BY SOURCE THEN DELETE \ + OUTPUT $action, deleted.ProductID INTO dsi.temp_products"; ms_and_generic().verified_stmt(stmt); } From 01c7db690c01a84a5cf5d29445d3149889dbd02a Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Wed, 2 Apr 2025 21:07:38 +0200 Subject: [PATCH 13/17] parse_select_into, outputclause and typos --- src/ast/mod.rs | 8 ++++---- src/parser/mod.rs | 20 ++++++++++++-------- tests/sqlparser_mssql.rs | 2 +- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6b18293c6..c9bedfda7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3830,7 +3830,7 @@ pub enum Statement { /// Specifies the actions to perform when values match or do not match. clauses: Vec, // Specifies the output to save changes in MSSQL - output: Option, + output: Option, }, /// ```sql /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] @@ -7908,7 +7908,7 @@ impl Display for MergeClause { } } -/// A Output in the end of a 'MERGE' Statement +/// A Output Clause in the end of a 'MERGE' Statement /// /// Example: /// OUTPUT $action, deleted.* INTO dbo.temp_products; @@ -7921,9 +7921,9 @@ pub struct OutputClause { pub into_table: SelectInto, } -impl fmt::Display for Output { +impl fmt::Display for OutputClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let Output { + let OutputClause { select_items, into_table, } = self; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 59f4cbf0b..ded193292 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14493,7 +14493,6 @@ impl<'a> Parser<'a> { break; } - let mut clause_kind = MergeClauseKind::Matched; if self.parse_keyword(Keyword::NOT) { clause_kind = MergeClauseKind::NotMatched; @@ -14586,26 +14585,31 @@ impl<'a> Parser<'a> { Ok(clauses) } - fn parse_output(&mut self) -> Result { + fn parse_output(&mut self) -> Result { self.expect_keyword_is(Keyword::OUTPUT)?; let select_items = self.parse_projection()?; self.expect_keyword_is(Keyword::INTO)?; + let into_table = self.parse_select_into()?; + + Ok(OutputClause { + select_items, + into_table, + }) + } + + fn parse_select_into(&mut self) -> Result { let temporary = self .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) .is_some(); let unlogged = self.parse_keyword(Keyword::UNLOGGED); let table = self.parse_keyword(Keyword::TABLE); let name = self.parse_object_name(false)?; - let into_table = SelectInto { + + Ok(SelectInto { temporary, unlogged, table, name, - }; - - Ok(Output { - select_items, - into_table, }) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index a0d26663a..5d76fd019 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1924,7 +1924,7 @@ fn ms_and_generic() -> TestedDialects { #[test] fn parse_mssql_merge_with_output() { - let stmt = "MERGE dso.products AS t\ + let stmt = "MERGE dso.products AS t \ USING dsi.products AS \ s ON s.ProductID = t.ProductID \ WHEN MATCHED AND \ From 445ec3953cb5d1498f882275ee02e10711a6a114 Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Thu, 3 Apr 2025 08:48:27 +0200 Subject: [PATCH 14/17] updated declaration of select_into to use new function --- src/parser/mod.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ded193292..a0e022c85 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10910,18 +10910,7 @@ impl<'a> Parser<'a> { }; let into = if self.parse_keyword(Keyword::INTO) { - let temporary = self - .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) - .is_some(); - let unlogged = self.parse_keyword(Keyword::UNLOGGED); - let table = self.parse_keyword(Keyword::TABLE); - let name = self.parse_object_name(false)?; - Some(SelectInto { - temporary, - unlogged, - table, - name, - }) + Some(self.parse_select_into()?) } else { None }; From bde81579a648f1e1b817212592607e7c944ac56d Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:52:23 +0200 Subject: [PATCH 15/17] Extend snowflake grant options support (#1794) --- src/ast/mod.rs | 55 ++++++++++++++++++++++++++++++++---- src/keywords.rs | 1 + src/parser/mod.rs | 50 +++++++++++++++++++++++--------- tests/sqlparser_snowflake.rs | 39 ++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 19 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c9bedfda7..87ab0150a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6087,10 +6087,10 @@ pub enum Action { ManageReleases, ManageVersions, Modify { - modify_type: ActionModifyType, + modify_type: Option, }, Monitor { - monitor_type: ActionMonitorType, + monitor_type: Option, }, Operate, OverrideShareRestrictions, @@ -6123,7 +6123,7 @@ impl fmt::Display for Action { match self { Action::AddSearchOptimization => f.write_str("ADD SEARCH OPTIMIZATION")?, Action::Apply { apply_type } => write!(f, "APPLY {apply_type}")?, - Action::ApplyBudget => f.write_str("APPLY BUDGET")?, + Action::ApplyBudget => f.write_str("APPLYBUDGET")?, Action::AttachListing => f.write_str("ATTACH LISTING")?, Action::AttachPolicy => f.write_str("ATTACH POLICY")?, Action::Audit => f.write_str("AUDIT")?, @@ -6151,8 +6151,18 @@ impl fmt::Display for Action { Action::Manage { manage_type } => write!(f, "MANAGE {manage_type}")?, Action::ManageReleases => f.write_str("MANAGE RELEASES")?, Action::ManageVersions => f.write_str("MANAGE VERSIONS")?, - Action::Modify { modify_type } => write!(f, "MODIFY {modify_type}")?, - Action::Monitor { monitor_type } => write!(f, "MONITOR {monitor_type}")?, + Action::Modify { modify_type } => { + write!(f, "MODIFY")?; + if let Some(modify_type) = modify_type { + write!(f, " {modify_type}")?; + } + } + Action::Monitor { monitor_type } => { + write!(f, "MONITOR")?; + if let Some(monitor_type) = monitor_type { + write!(f, " {monitor_type}")? + } + } Action::Operate => f.write_str("OPERATE")?, Action::OverrideShareRestrictions => f.write_str("OVERRIDE SHARE RESTRICTIONS")?, Action::Ownership => f.write_str("OWNERSHIP")?, @@ -6470,6 +6480,20 @@ pub enum GrantObjects { Warehouses(Vec), /// Grant privileges on specific integrations Integrations(Vec), + /// Grant privileges on resource monitors + ResourceMonitors(Vec), + /// Grant privileges on users + Users(Vec), + /// Grant privileges on compute pools + ComputePools(Vec), + /// Grant privileges on connections + Connections(Vec), + /// Grant privileges on failover groups + FailoverGroup(Vec), + /// Grant privileges on replication group + ReplicationGroup(Vec), + /// Grant privileges on external volumes + ExternalVolumes(Vec), } impl fmt::Display for GrantObjects { @@ -6510,6 +6534,27 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::ResourceMonitors(objects) => { + write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) + } + GrantObjects::Users(objects) => { + write!(f, "USER {}", display_comma_separated(objects)) + } + GrantObjects::ComputePools(objects) => { + write!(f, "COMPUTE POOL {}", display_comma_separated(objects)) + } + GrantObjects::Connections(objects) => { + write!(f, "CONNECTION {}", display_comma_separated(objects)) + } + GrantObjects::FailoverGroup(objects) => { + write!(f, "FAILOVER GROUP {}", display_comma_separated(objects)) + } + GrantObjects::ReplicationGroup(objects) => { + write!(f, "REPLICATION GROUP {}", display_comma_separated(objects)) + } + GrantObjects::ExternalVolumes(objects) => { + write!(f, "EXTERNAL VOLUME {}", display_comma_separated(objects)) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 112566d00..8024a4fca 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -739,6 +739,7 @@ define_keywords!( REPLICATION, RESET, RESOLVE, + RESOURCE, RESPECT, RESTART, RESTRICT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a0e022c85..50a9aae84 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12864,6 +12864,26 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllSequencesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[Keyword::RESOURCE, Keyword::MONITOR]) { + Some(GrantObjects::ResourceMonitors(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::COMPUTE, Keyword::POOL]) { + Some(GrantObjects::ComputePools(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::FAILOVER, Keyword::GROUP]) { + Some(GrantObjects::FailoverGroup(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::REPLICATION, Keyword::GROUP]) { + Some(GrantObjects::ReplicationGroup(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) + } else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { + Some(GrantObjects::ExternalVolumes(self.parse_comma_separated( + |p| p.parse_object_name_with_wildcards(false, true), + )?)) } else { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, @@ -12877,6 +12897,8 @@ impl<'a> Parser<'a> { Keyword::VIEW, Keyword::WAREHOUSE, Keyword::INTEGRATION, + Keyword::USER, + Keyword::CONNECTION, ]); let objects = self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); @@ -12887,6 +12909,8 @@ impl<'a> Parser<'a> { Some(Keyword::WAREHOUSE) => Some(GrantObjects::Warehouses(objects?)), Some(Keyword::INTEGRATION) => Some(GrantObjects::Integrations(objects?)), Some(Keyword::VIEW) => Some(GrantObjects::Views(objects?)), + Some(Keyword::USER) => Some(GrantObjects::Users(objects?)), + Some(Keyword::CONNECTION) => Some(GrantObjects::Connections(objects?)), Some(Keyword::TABLE) | None => Some(GrantObjects::Tables(objects?)), _ => unreachable!(), } @@ -12972,10 +12996,10 @@ impl<'a> Parser<'a> { let manage_type = self.parse_action_manage_type()?; Ok(Action::Manage { manage_type }) } else if self.parse_keyword(Keyword::MODIFY) { - let modify_type = self.parse_action_modify_type()?; + let modify_type = self.parse_action_modify_type(); Ok(Action::Modify { modify_type }) } else if self.parse_keyword(Keyword::MONITOR) { - let monitor_type = self.parse_action_monitor_type()?; + let monitor_type = self.parse_action_monitor_type(); Ok(Action::Monitor { monitor_type }) } else if self.parse_keyword(Keyword::OPERATE) { Ok(Action::Operate) @@ -13116,29 +13140,29 @@ impl<'a> Parser<'a> { } } - fn parse_action_modify_type(&mut self) -> Result { + fn parse_action_modify_type(&mut self) -> Option { if self.parse_keywords(&[Keyword::LOG, Keyword::LEVEL]) { - Ok(ActionModifyType::LogLevel) + Some(ActionModifyType::LogLevel) } else if self.parse_keywords(&[Keyword::TRACE, Keyword::LEVEL]) { - Ok(ActionModifyType::TraceLevel) + Some(ActionModifyType::TraceLevel) } else if self.parse_keywords(&[Keyword::SESSION, Keyword::LOG, Keyword::LEVEL]) { - Ok(ActionModifyType::SessionLogLevel) + Some(ActionModifyType::SessionLogLevel) } else if self.parse_keywords(&[Keyword::SESSION, Keyword::TRACE, Keyword::LEVEL]) { - Ok(ActionModifyType::SessionTraceLevel) + Some(ActionModifyType::SessionTraceLevel) } else { - self.expected("GRANT MODIFY type", self.peek_token()) + None } } - fn parse_action_monitor_type(&mut self) -> Result { + fn parse_action_monitor_type(&mut self) -> Option { if self.parse_keyword(Keyword::EXECUTION) { - Ok(ActionMonitorType::Execution) + Some(ActionMonitorType::Execution) } else if self.parse_keyword(Keyword::SECURITY) { - Ok(ActionMonitorType::Security) + Some(ActionMonitorType::Security) } else if self.parse_keyword(Keyword::USAGE) { - Ok(ActionMonitorType::Usage) + Some(ActionMonitorType::Usage) } else { - self.expected("GRANT MONITOR type", self.peek_token()) + None } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 097af3466..62e52e2d1 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3357,7 +3357,7 @@ fn test_timetravel_at_before() { } #[test] -fn test_grant_account_privileges() { +fn test_grant_account_global_privileges() { let privileges = vec![ "ALL", "ALL PRIVILEGES", @@ -3462,6 +3462,43 @@ fn test_grant_account_privileges() { } } +#[test] +fn test_grant_account_object_privileges() { + let privileges = vec![ + "ALL", + "ALL PRIVILEGES", + "APPLYBUDGET", + "MODIFY", + "MONITOR", + "USAGE", + "OPERATE", + ]; + + let objects_types = vec![ + "USER", + "RESOURCE MONITOR", + "WAREHOUSE", + "COMPUTE POOL", + "DATABASE", + "INTEGRATION", + "CONNECTION", + "FAILOVER GROUP", + "REPLICATION GROUP", + "EXTERNAL VOLUME", + ]; + + let with_grant_options = vec!["", " WITH GRANT OPTION"]; + + for t in &objects_types { + for p in &privileges { + for wgo in &with_grant_options { + let sql = format!("GRANT {p} ON {t} obj1 TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + } +} + #[test] fn test_grant_role_to() { snowflake_and_generic().verified_stmt("GRANT ROLE r1 TO ROLE r2"); From 9b27153f6415d9c345d5185ea1b30a874c9374f4 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 4 Apr 2025 12:34:18 +0200 Subject: [PATCH 16/17] Fix clippy lint on rust 1.86 (#1796) --- src/ast/ddl.rs | 2 +- src/ast/mod.rs | 12 ++++++------ src/dialect/snowflake.rs | 15 +++++++-------- src/keywords.rs | 12 ++++++------ tests/sqlparser_common.rs | 3 +-- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 39e43ef15..6a649b73b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -868,7 +868,7 @@ impl fmt::Display for AlterColumnOperation { AlterColumnOperation::SetDefault { value } => { write!(f, "SET DEFAULT {value}") } - AlterColumnOperation::DropDefault {} => { + AlterColumnOperation::DropDefault => { write!(f, "DROP DEFAULT") } AlterColumnOperation::SetDataType { data_type, using } => { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 87ab0150a..8537bd858 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -662,17 +662,17 @@ pub enum Expr { /// such as maps, arrays, and lists: /// - Array /// - A 1-dim array `a[1]` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]` /// - A 2-dim array `a[1][2]` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]` /// - Map or Struct (Bracket-style) /// - A map `a['field1']` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]` /// - A 2-dim map `a['field1']['field2']` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]` /// - Struct (Dot-style) (only effect when the chain contains both subscript and expr) /// - A struct access `a[field1].field2` will be represented like: - /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]` + /// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]` /// - If a struct access likes `a.field1.field2`, it will be represented by CompoundIdentifier([a, field1, field2]) CompoundFieldAccess { root: Box, @@ -7625,7 +7625,7 @@ impl fmt::Display for CopyTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use CopyTarget::*; match self { - Stdin { .. } => write!(f, "STDIN"), + Stdin => write!(f, "STDIN"), Stdout => write!(f, "STDOUT"), File { filename } => write!(f, "'{}'", value::escape_single_quote_string(filename)), Program { command } => write!( diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d1a696a0b..f303f8218 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1038,14 +1038,13 @@ fn parse_session_options( } } } - options - .is_empty() - .then(|| { - Err(ParserError::ParserError( - "expected at least one option".to_string(), - )) - }) - .unwrap_or(Ok(options)) + if options.is_empty() { + Err(ParserError::ParserError( + "expected at least one option".to_string(), + )) + } else { + Ok(options) + } } /// Parses options provided within parentheses like: diff --git a/src/keywords.rs b/src/keywords.rs index 8024a4fca..a1b4c0c3a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -18,14 +18,14 @@ //! This module defines //! 1) a list of constants for every keyword //! 2) an `ALL_KEYWORDS` array with every keyword in it -//! This is not a list of *reserved* keywords: some of these can be -//! parsed as identifiers if the parser decides so. This means that -//! new keywords can be added here without affecting the parse result. +//! This is not a list of *reserved* keywords: some of these can be +//! parsed as identifiers if the parser decides so. This means that +//! new keywords can be added here without affecting the parse result. //! -//! As a matter of fact, most of these keywords are not used at all -//! and could be removed. +//! As a matter of fact, most of these keywords are not used at all +//! and could be removed. //! 3) a `RESERVED_FOR_TABLE_ALIAS` array with keywords reserved in a -//! "table alias" context. +//! "table alias" context. #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d13f711f8..365332170 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14106,8 +14106,7 @@ fn test_table_sample() { #[test] fn overflow() { - let expr = std::iter::repeat("1") - .take(1000) + let expr = std::iter::repeat_n("1", 1000) .collect::>() .join(" + "); let sql = format!("SELECT {}", expr); From f8e197ae259ff52afe54fbb838b2f1bba4e33b78 Mon Sep 17 00:00:00 2001 From: Dilovan Celik Date: Sat, 5 Apr 2025 16:41:39 +0200 Subject: [PATCH 17/17] fixed clippy error --- tests/sqlparser_redshift.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index c75abe16f..060e3853d 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -395,5 +395,5 @@ fn test_parse_nested_quoted_identifier() { #[test] fn parse_extract_single_quotes() { let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; - redshift().verified_stmt(&sql); + redshift().verified_stmt(sql); }