From be30697efb94cecdca749cc540c5a662c63fad3f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 17 Jun 2025 10:46:58 -0400 Subject: [PATCH 01/39] Add license header check to CI (#1888) --- .github/workflows/license.yml | 39 +++++++++++++++++++++++++++++++ dev/release/rat_exclude_files.txt | 9 +++---- 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/license.yml diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 00000000..c851bff3 --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: license + +# trigger for all PRs and changes to main +on: + push: + branches: + - main + pull_request: + +jobs: + + rat: + name: Release Audit Tool (RAT) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.8 + - name: Audit licenses + run: ./dev/release/run-rat.sh . diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index 562eec2f..280b1bce 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -1,7 +1,8 @@ -# Files to exclude from the Apache Rat (license) check -.gitignore .tool-versions +target/* +**.gitignore +rat.txt dev/release/rat_exclude_files.txt -fuzz/.gitignore sqlparser_bench/img/flamegraph.svg - +**Cargo.lock +filtered_rat.txt From b1b379e57086ad7fcfa67136c0c64b14d4efab9d Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 18 Jun 2025 13:00:53 +0800 Subject: [PATCH 02/39] Add support of parsing struct field's options in BigQuery (#1890) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 12 ++- src/parser/mod.rs | 3 + tests/sqlparser_bigquery.rs | 159 ++++++++++++++++++++++++---------- tests/sqlparser_clickhouse.rs | 10 ++- tests/sqlparser_duckdb.rs | 6 ++ 5 files changed, 138 insertions(+), 52 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a1d8ff6f..04401a48 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -428,14 +428,22 @@ impl fmt::Display for Interval { pub struct StructField { pub field_name: Option, pub field_type: DataType, + /// Struct field options. + /// See [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_name_and_column_schema) + pub options: Option>, } impl fmt::Display for StructField { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(name) = &self.field_name { - write!(f, "{name} {}", self.field_type) + write!(f, "{name} {}", self.field_type)?; } else { - write!(f, "{}", self.field_type) + write!(f, "{}", self.field_type)?; + } + if let Some(options) = &self.options { + write!(f, " OPTIONS({})", display_separated(options, ", ")) + } else { + Ok(()) } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 73cc3e0e..19c11d29 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3076,6 +3076,7 @@ impl<'a> Parser<'a> { Ok(StructField { field_name: Some(field_name), field_type, + options: None, }) }); self.expect_token(&Token::RParen)?; @@ -3109,10 +3110,12 @@ impl<'a> Parser<'a> { let (field_type, trailing_bracket) = self.parse_data_type_helper()?; + let options = self.maybe_parse_options(Keyword::OPTIONS)?; Ok(( StructField { field_name, field_type, + options, }, trailing_bracket, )) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 6a303577..e37e4a44 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -601,11 +601,13 @@ fn parse_nested_data_types() { field_name: Some("a".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket( Box::new(DataType::Int64,) - )) + )), + options: None, }, StructField { field_name: Some("b".into()), - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }, ], StructBracketKind::AngleBrackets @@ -619,6 +621,7 @@ fn parse_nested_data_types() { vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }], StructBracketKind::AngleBrackets ), @@ -771,6 +774,7 @@ fn parse_typed_struct_syntax_bigquery() { fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -799,7 +803,8 @@ fn parse_typed_struct_syntax_bigquery() { quote_style: None, span: Span::empty(), }), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident { @@ -807,7 +812,8 @@ fn parse_typed_struct_syntax_bigquery() { quote_style: None, span: Span::empty(), }), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }, ] }, @@ -825,17 +831,20 @@ fn parse_typed_struct_syntax_bigquery() { field_name: Some("arr".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Float64 - ))) + ))), + options: None, }, StructField { field_name: Some("str".into()), field_type: DataType::Struct( vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }], StructBracketKind::AngleBrackets - ) + ), + options: None, }, ] }, @@ -858,13 +867,15 @@ fn parse_typed_struct_syntax_bigquery() { field_type: DataType::Struct( Default::default(), StructBracketKind::AngleBrackets - ) + ), + options: None, }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) - ))) + ))), + options: None, }, ] }, @@ -879,7 +890,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -891,7 +903,8 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -907,7 +920,8 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Date + field_type: DataType::Date, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -920,7 +934,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Datetime(None) + field_type: DataType::Datetime(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -930,7 +945,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, - field_type: DataType::Float64 + field_type: DataType::Float64, + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -940,7 +956,8 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[3]) @@ -962,7 +979,8 @@ fn parse_typed_struct_syntax_bigquery() { })], fields: vec![StructField { field_name: None, - field_type: DataType::Interval + field_type: DataType::Interval, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -977,7 +995,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::JSON + field_type: DataType::JSON, + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -993,7 +1012,8 @@ fn parse_typed_struct_syntax_bigquery() { )], fields: vec![StructField { field_name: None, - field_type: DataType::String(Some(42)) + field_type: DataType::String(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1006,7 +1026,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Timestamp(None, TimezoneInfo::None) + field_type: DataType::Timestamp(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1020,7 +1041,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Time(None, TimezoneInfo::None) + field_type: DataType::Time(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1037,7 +1059,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Numeric(ExactNumberInfo::None) + field_type: DataType::Numeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1050,7 +1073,8 @@ fn parse_typed_struct_syntax_bigquery() { }], fields: vec![StructField { field_name: None, - field_type: DataType::BigNumeric(ExactNumberInfo::None) + field_type: DataType::BigNumeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1067,10 +1091,12 @@ fn parse_typed_struct_syntax_bigquery() { StructField { field_name: Some("key".into()), field_type: DataType::Int64, + options: None, }, StructField { field_name: Some("value".into()), field_type: DataType::Int64, + options: None, }, ] }, @@ -1092,6 +1118,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { fields: vec![StructField { field_name: None, field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1120,7 +1147,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { quote_style: None, span: Span::empty(), }), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident { @@ -1128,7 +1156,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { quote_style: None, span: Span::empty(), }), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }, ] }, @@ -1151,13 +1180,15 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { field_type: DataType::Struct( Default::default(), StructBracketKind::AngleBrackets - ) + ), + options: None, }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) - ))) + ))), + options: None, }, ] }, @@ -1172,7 +1203,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, - field_type: DataType::Bool + field_type: DataType::Bool, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1184,7 +1216,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Bytes(Some(42)) + field_type: DataType::Bytes(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1200,7 +1233,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::Date + field_type: DataType::Date, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1213,7 +1247,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Datetime(None) + field_type: DataType::Datetime(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1223,7 +1258,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, - field_type: DataType::Float64 + field_type: DataType::Float64, + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1233,7 +1269,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[3]) @@ -1255,7 +1292,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { })], fields: vec![StructField { field_name: None, - field_type: DataType::Interval + field_type: DataType::Interval, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1270,7 +1308,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::JSON + field_type: DataType::JSON, + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1286,7 +1325,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { )], fields: vec![StructField { field_name: None, - field_type: DataType::String(Some(42)) + field_type: DataType::String(Some(42)), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1299,7 +1339,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Timestamp(None, TimezoneInfo::None) + field_type: DataType::Timestamp(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1313,7 +1354,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Time(None, TimezoneInfo::None) + field_type: DataType::Time(None, TimezoneInfo::None), + options: None, }] }, expr_from_projection(&select.projection[2]) @@ -1330,7 +1372,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::Numeric(ExactNumberInfo::None) + field_type: DataType::Numeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1343,7 +1386,8 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }], fields: vec![StructField { field_name: None, - field_type: DataType::BigNumeric(ExactNumberInfo::None) + field_type: DataType::BigNumeric(ExactNumberInfo::None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1360,7 +1404,8 @@ fn parse_typed_struct_with_field_name_bigquery() { values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1372,7 +1417,8 @@ fn parse_typed_struct_with_field_name_bigquery() { )], fields: vec![StructField { field_name: Some(Ident::from("y")), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1387,11 +1433,13 @@ fn parse_typed_struct_with_field_name_bigquery() { fields: vec![ StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident::from("y")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, } ] }, @@ -1409,7 +1457,8 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }] }, expr_from_projection(&select.projection[0]) @@ -1421,7 +1470,8 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { )], fields: vec![StructField { field_name: Some(Ident::from("y")), - field_type: DataType::String(None) + field_type: DataType::String(None), + options: None, }] }, expr_from_projection(&select.projection[1]) @@ -1436,11 +1486,13 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { fields: vec![ StructField { field_name: Some(Ident::from("x")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, }, StructField { field_name: Some(Ident::from("y")), - field_type: DataType::Int64 + field_type: DataType::Int64, + options: None, } ] }, @@ -2407,3 +2459,16 @@ fn test_any_type() { fn test_any_type_dont_break_custom_type() { bigquery_and_generic().verified_stmt("CREATE TABLE foo (x ANY)"); } + +#[test] +fn test_struct_field_options() { + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRUCT, ", + "f1 STRUCT<", + "a STRING OPTIONS(description = 'This is a string', type = 'string'), ", + "b INT64", + "> OPTIONS(description = 'This is a struct field')", + ")", + )); +} diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index d0218b6c..93b4c4f5 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -669,11 +669,13 @@ fn parse_create_table_with_nested_data_types() { DataType::Tuple(vec![ StructField { field_name: None, - field_type: DataType::FixedString(128) + field_type: DataType::FixedString(128), + options: None, }, StructField { field_name: None, - field_type: DataType::Int128 + field_type: DataType::Int128, + options: None, } ]) ))), @@ -685,12 +687,14 @@ fn parse_create_table_with_nested_data_types() { StructField { field_name: Some("a".into()), field_type: DataType::Datetime64(9, None), + options: None, }, StructField { field_name: Some("b".into()), field_type: DataType::Array(ArrayElemTypeDef::Parenthesis( Box::new(DataType::Uuid) - )) + )), + options: None, }, ]), options: vec![], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 8e498365..f503ed55 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -44,10 +44,12 @@ fn test_struct() { StructField { field_name: Some(Ident::new("v")), field_type: DataType::Varchar(None), + options: None, }, StructField { field_name: Some(Ident::new("i")), field_type: DataType::Integer(None), + options: None, }, ], StructBracketKind::Parentheses, @@ -84,6 +86,7 @@ fn test_struct() { StructField { field_name: Some(Ident::new("v")), field_type: DataType::Varchar(None), + options: None, }, StructField { field_name: Some(Ident::new("s")), @@ -92,14 +95,17 @@ fn test_struct() { StructField { field_name: Some(Ident::new("a1")), field_type: DataType::Integer(None), + options: None, }, StructField { field_name: Some(Ident::new("a2")), field_type: DataType::Varchar(None), + options: None, }, ], StructBracketKind::Parentheses, ), + options: None, }, ], StructBracketKind::Parentheses, From 185a4902189565031fa066d165594325172f2893 Mon Sep 17 00:00:00 2001 From: hulk Date: Fri, 20 Jun 2025 22:56:26 +0800 Subject: [PATCH 03/39] Fix parsing error when having fields after nested struct in BigQuery (#1897) --- src/parser/mod.rs | 11 +--- tests/sqlparser_bigquery.rs | 104 ++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 38 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 19c11d29..ece9ba65 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3034,7 +3034,6 @@ impl<'a> Parser<'a> { where F: FnMut(&mut Parser<'a>) -> Result<(StructField, MatchedTrailingBracket), ParserError>, { - let start_token = self.peek_token(); self.expect_keyword_is(Keyword::STRUCT)?; // Nothing to do if we have no type information. @@ -3047,16 +3046,10 @@ impl<'a> Parser<'a> { let trailing_bracket = loop { let (def, trailing_bracket) = elem_parser(self)?; field_defs.push(def); - if !self.consume_token(&Token::Comma) { + // The struct field definition is finished if it occurs `>>` or comma. + if trailing_bracket.0 || !self.consume_token(&Token::Comma) { break trailing_bracket; } - - // Angle brackets are balanced so we only expect the trailing `>>` after - // we've matched all field types for the current struct. - // e.g. this is invalid syntax `STRUCT>>, INT>(NULL)` - if trailing_bracket.0 { - return parser_err!("unmatched > in STRUCT definition", start_token.span.start); - } }; Ok(( diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index e37e4a44..810d8480 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -635,35 +635,6 @@ fn parse_nested_data_types() { } } -#[test] -fn parse_invalid_brackets() { - let sql = "SELECT STRUCT>(NULL)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError("unmatched > in STRUCT literal".to_string()) - ); - - let sql = "SELECT STRUCT>>(NULL)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError("Expected: (, found: >".to_string()) - ); - - let sql = "CREATE TABLE table (x STRUCT>>)"; - assert_eq!( - bigquery_and_generic() - .parse_sql_statements(sql) - .unwrap_err(), - ParserError::ParserError( - "Expected: ',' or ')' after column definition, found: >".to_string() - ) - ); -} - #[test] fn parse_tuple_struct_literal() { // tuple syntax: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#tuple_syntax @@ -2472,3 +2443,78 @@ fn test_struct_field_options() { ")", )); } + +#[test] +fn test_struct_trailing_and_nested_bracket() { + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRING, ", + "f1 STRUCT>, ", + "f2 STRING", + ")", + )); + + // More complex nested structs + bigquery().verified_stmt(concat!( + "CREATE TABLE my_table (", + "f0 STRING, ", + "f1 STRUCT>>, ", + "f2 STRUCT>>>, ", + "f3 STRUCT>", + ")", + )); + + // Bad case with missing closing bracket + assert_eq!( + ParserError::ParserError("Expected: >, found: )".to_owned()), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT after parsing data type STRUCT)".to_owned() + ), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT>)") + .unwrap_err() + ); + + // Base case with redundant closing bracket in nested struct + assert_eq!( + ParserError::ParserError( + "Expected: ',' or ')' after column definition, found: >".to_owned() + ), + bigquery() + .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT>>, c INT64)") + .unwrap_err() + ); + + let sql = "SELECT STRUCT>(NULL)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("unmatched > in STRUCT literal".to_string()) + ); + + let sql = "SELECT STRUCT>>(NULL)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("Expected: (, found: >".to_string()) + ); + + let sql = "CREATE TABLE table (x STRUCT>>)"; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError( + "Expected: ',' or ')' after column definition, found: >".to_string() + ) + ); +} From 204d3b484db4a659396ae13e7bd2c20ac55fb07c Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Sat, 21 Jun 2025 08:12:07 +0200 Subject: [PATCH 04/39] Extend exception handling (#1884) --- src/ast/mod.rs | 60 +++++++++++++++++++++++++-------- src/dialect/bigquery.rs | 51 +++------------------------- src/dialect/snowflake.rs | 4 +++ src/keywords.rs | 1 + src/parser/mod.rs | 49 +++++++++++++++++++++++++-- tests/sqlparser_bigquery.rs | 9 +++-- tests/sqlparser_common.rs | 7 ++-- tests/sqlparser_snowflake.rs | 64 ++++++++++++++++++++++++++++++++++++ 8 files changed, 178 insertions(+), 67 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 04401a48..369489f5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2990,6 +2990,36 @@ impl From for Statement { } } +/// A representation of a `WHEN` arm with all the identifiers catched and the statements to execute +/// for the arm. +/// +/// Snowflake: +/// BigQuery: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExceptionWhen { + pub idents: Vec, + pub statements: Vec, +} + +impl Display for ExceptionWhen { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "WHEN {idents} THEN", + idents = display_separated(&self.idents, " OR ") + )?; + + if !self.statements.is_empty() { + write!(f, " ")?; + format_statement_list(f, &self.statements)?; + } + + Ok(()) + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3678,17 +3708,20 @@ pub enum Statement { /// END; /// ``` statements: Vec, - /// Statements of an exception clause. + /// Exception handling with exception clauses. /// Example: /// ```sql - /// BEGIN - /// SELECT 1; - /// EXCEPTION WHEN ERROR THEN - /// SELECT 2; - /// SELECT 3; - /// END; + /// EXCEPTION + /// WHEN EXCEPTION_1 THEN + /// SELECT 2; + /// WHEN EXCEPTION_2 OR EXCEPTION_3 THEN + /// SELECT 3; + /// WHEN OTHER THEN + /// SELECT 4; + /// ``` /// - exception_statements: Option>, + /// + exception: Option>, /// TRUE if the statement has an `END` keyword. has_end_keyword: bool, }, @@ -5533,7 +5566,7 @@ impl fmt::Display for Statement { transaction, modifier, statements, - exception_statements, + exception, has_end_keyword, } => { if *syntax_begin { @@ -5555,11 +5588,10 @@ impl fmt::Display for Statement { write!(f, " ")?; format_statement_list(f, statements)?; } - if let Some(exception_statements) = exception_statements { - write!(f, " EXCEPTION WHEN ERROR THEN")?; - if !exception_statements.is_empty() { - write!(f, " ")?; - format_statement_list(f, exception_statements)?; + if let Some(exception_when) = exception { + write!(f, " EXCEPTION")?; + for when in exception_when { + write!(f, " {when}")?; } } if *has_end_keyword { diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 68ca1390..c2cd507c 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -46,7 +46,11 @@ pub struct BigQueryDialect; impl Dialect for BigQueryDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { - self.maybe_parse_statement(parser) + if parser.parse_keyword(Keyword::BEGIN) { + return Some(parser.parse_begin_exception_end()); + } + + None } /// See @@ -141,48 +145,3 @@ impl Dialect for BigQueryDialect { true } } - -impl BigQueryDialect { - fn maybe_parse_statement(&self, parser: &mut Parser) -> Option> { - if parser.peek_keyword(Keyword::BEGIN) { - return Some(self.parse_begin(parser)); - } - None - } - - /// Parse a `BEGIN` statement. - /// - fn parse_begin(&self, parser: &mut Parser) -> Result { - parser.expect_keyword(Keyword::BEGIN)?; - - let statements = parser.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?; - - let has_exception_when_clause = parser.parse_keywords(&[ - Keyword::EXCEPTION, - Keyword::WHEN, - Keyword::ERROR, - Keyword::THEN, - ]); - let exception_statements = if has_exception_when_clause { - if !parser.peek_keyword(Keyword::END) { - Some(parser.parse_statement_list(&[Keyword::END])?) - } else { - Some(Default::default()) - } - } else { - None - }; - - parser.expect_keyword(Keyword::END)?; - - Ok(Statement::StartTransaction { - begin: true, - statements, - exception_statements, - has_end_keyword: true, - transaction: None, - modifier: None, - modes: Default::default(), - }) - } -} diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ea0b94a6..66e04ac2 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -131,6 +131,10 @@ impl Dialect for SnowflakeDialect { } fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keyword(Keyword::BEGIN) { + return Some(parser.parse_begin_exception_end()); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { diff --git a/src/keywords.rs b/src/keywords.rs index f5c5e567..cb6c7a6e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -646,6 +646,7 @@ define_keywords!( ORDER, ORDINALITY, ORGANIZATION, + OTHER, OUT, OUTER, OUTPUT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ece9ba65..be32093f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15092,7 +15092,7 @@ impl<'a> Parser<'a> { transaction: Some(BeginTransactionKind::Transaction), modifier: None, statements: vec![], - exception_statements: None, + exception: None, has_end_keyword: false, }) } @@ -15124,11 +15124,56 @@ impl<'a> Parser<'a> { transaction, modifier, statements: vec![], - exception_statements: None, + exception: None, has_end_keyword: false, }) } + pub fn parse_begin_exception_end(&mut self) -> Result { + let statements = self.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?; + + let exception = if self.parse_keyword(Keyword::EXCEPTION) { + let mut when = Vec::new(); + + // We can have multiple `WHEN` arms so we consume all cases until `END` + while !self.peek_keyword(Keyword::END) { + self.expect_keyword(Keyword::WHEN)?; + + // Each `WHEN` case can have one or more conditions, e.g. + // WHEN EXCEPTION_1 [OR EXCEPTION_2] THEN + // So we parse identifiers until the `THEN` keyword. + let mut idents = Vec::new(); + + while !self.parse_keyword(Keyword::THEN) { + let ident = self.parse_identifier()?; + idents.push(ident); + + self.maybe_parse(|p| p.expect_keyword(Keyword::OR))?; + } + + let statements = self.parse_statement_list(&[Keyword::WHEN, Keyword::END])?; + + when.push(ExceptionWhen { idents, statements }); + } + + Some(when) + } else { + None + }; + + self.expect_keyword(Keyword::END)?; + + Ok(Statement::StartTransaction { + begin: true, + statements, + exception, + has_end_keyword: true, + transaction: None, + modifier: None, + modes: Default::default(), + }) + } + pub fn parse_end(&mut self) -> Result { let modifier = if !self.dialect.supports_end_transaction_modifier() { None diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 810d8480..0de0b12b 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -261,10 +261,10 @@ fn parse_at_at_identifier() { #[test] fn parse_begin() { - let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; END"#; + let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; RAISE USING MESSAGE = FORMAT('ERR: %s', 'Bad'); END"#; let Statement::StartTransaction { statements, - exception_statements, + exception, has_end_keyword, .. } = bigquery().verified_stmt(sql) @@ -272,7 +272,10 @@ fn parse_begin() { unreachable!(); }; assert_eq!(1, statements.len()); - assert_eq!(1, exception_statements.unwrap().len()); + assert!(exception.is_some()); + + let exception = exception.unwrap(); + assert_eq!(1, exception.len()); assert!(has_end_keyword); bigquery().verified_stmt( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index abcadb45..e4363ff6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8592,8 +8592,11 @@ fn lateral_function() { #[test] fn parse_start_transaction() { let dialects = all_dialects_except(|d| - // BigQuery does not support this syntax - d.is::()); + // BigQuery and Snowflake does not support this syntax + // + // BigQuery: + // Snowflake: + d.is::() || d.is::()); match dialects .verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7d734ca2..b11a2cb0 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4082,3 +4082,67 @@ fn parse_connect_by_root_operator() { "sql parser error: Expected an expression, found: FROM" ); } + +#[test] +fn test_begin_exception_end() { + for sql in [ + "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END", + "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE EX_1; END", + "BEGIN SELECT 1; EXCEPTION WHEN FOO THEN SELECT 2; WHEN OTHER THEN SELECT 3; RAISE; END", + "BEGIN BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END; END", + ] { + snowflake().verified_stmt(sql); + } + + let sql = r#" +DECLARE + EXCEPTION_1 EXCEPTION (-20001, 'I caught the expected exception.'); + EXCEPTION_2 EXCEPTION (-20002, 'Not the expected exception!'); + EXCEPTION_3 EXCEPTION (-20003, 'The worst exception...'); +BEGIN + BEGIN + SELECT 1; + EXCEPTION + WHEN EXCEPTION_1 THEN + SELECT 1; + WHEN EXCEPTION_2 OR EXCEPTION_3 THEN + SELECT 2; + SELECT 3; + WHEN OTHER THEN + SELECT 4; + RAISE; + END; +END +"#; + + // Outer `BEGIN` of the two nested `BEGIN` statements. + let Statement::StartTransaction { mut statements, .. } = snowflake() + .parse_sql_statements(sql) + .unwrap() + .pop() + .unwrap() + else { + unreachable!(); + }; + + // Inner `BEGIN` of the two nested `BEGIN` statements. + let Statement::StartTransaction { + statements, + exception, + has_end_keyword, + .. + } = statements.pop().unwrap() + else { + unreachable!(); + }; + + assert_eq!(1, statements.len()); + assert!(has_end_keyword); + + let exception = exception.unwrap(); + assert_eq!(3, exception.len()); + assert_eq!(1, exception[0].idents.len()); + assert_eq!(1, exception[0].statements.len()); + assert_eq!(2, exception[1].idents.len()); + assert_eq!(2, exception[1].statements.len()); +} From 1d0dc7cdd8209d5e68f41675622dd17e2b336e26 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Sun, 22 Jun 2025 08:02:51 +0100 Subject: [PATCH 05/39] Postgres: Add support for text search types (#1889) --- src/ast/data_type.rs | 10 ++++++++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 6 ++++++ tests/sqlparser_postgres.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 3a4958c9..ef04b7e4 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -446,6 +446,14 @@ pub enum DataType { /// /// [PostgreSQL]: https://www.postgresql.org/docs/9.5/functions-geometry.html GeometricType(GeometricTypeKind), + /// PostgreSQL text search vectors, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html + TsVector, + /// PostgreSQL text search query, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html + TsQuery, } impl fmt::Display for DataType { @@ -738,6 +746,8 @@ impl fmt::Display for DataType { write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) } DataType::GeometricType(kind) => write!(f, "{}", kind), + DataType::TsVector => write!(f, "TSVECTOR"), + DataType::TsQuery => write!(f, "TSQUERY"), } } } diff --git a/src/keywords.rs b/src/keywords.rs index cb6c7a6e..f56178c1 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -935,6 +935,8 @@ define_keywords!( TRY, TRY_CAST, TRY_CONVERT, + TSQUERY, + TSVECTOR, TUPLE, TYPE, UBIGINT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index be32093f..f60323b2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9909,6 +9909,12 @@ impl<'a> Parser<'a> { Ok(DataType::Unsigned) } } + Keyword::TSVECTOR if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { + Ok(DataType::TsVector) + } + Keyword::TSQUERY if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { + Ok(DataType::TsQuery) + } _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6f0ba9c6..b6605cf1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6201,3 +6201,34 @@ fn parse_alter_table_replica_identity() { _ => unreachable!(), } } + +#[test] +fn parse_ts_datatypes() { + match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSVECTOR)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TsVector, + options: vec![], + }] + ); + } + _ => unreachable!(), + } + + match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSQUERY)") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "x".into(), + data_type: DataType::TsQuery, + options: vec![], + }] + ); + } + _ => unreachable!(), + } +} From 7865de015f8a45d9545dc7ca0659f37a0c3f7496 Mon Sep 17 00:00:00 2001 From: Dima <111751109+Dimchikkk@users.noreply.github.com> Date: Sun, 22 Jun 2025 08:22:45 +0100 Subject: [PATCH 06/39] Fix `limit` in subqueries (#1899) --- src/ast/mod.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 369489f5..17464f4a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -748,7 +748,7 @@ pub enum Expr { /// `[ NOT ] IN (SELECT ...)` InSubquery { expr: Box, - subquery: Box, + subquery: Box, negated: bool, }, /// `[ NOT ] IN UNNEST(array_expression)` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f60323b2..44bf58d0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3817,7 +3817,7 @@ impl<'a> Parser<'a> { }); } self.expect_token(&Token::LParen)?; - let in_op = match self.maybe_parse(|p| p.parse_query_body(p.dialect.prec_unknown()))? { + let in_op = match self.maybe_parse(|p| p.parse_query())? { Some(subquery) => Expr::InSubquery { expr: Box::new(expr), subquery, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e4363ff6..52054604 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2225,7 +2225,7 @@ fn parse_in_subquery() { assert_eq!( Expr::InSubquery { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: verified_query("SELECT segm FROM bar").body, + subquery: Box::new(verified_query("SELECT segm FROM bar")), negated: false, }, select.selection.unwrap() @@ -2239,7 +2239,9 @@ fn parse_in_union() { assert_eq!( Expr::InSubquery { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: verified_query("(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)").body, + subquery: Box::new(verified_query( + "(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)" + )), negated: false, }, select.selection.unwrap() @@ -15303,6 +15305,11 @@ fn parse_return() { let _ = all_dialects().verified_stmt("RETURN 1"); } +#[test] +fn parse_subquery_limit() { + let _ = all_dialects().verified_stmt("SELECT t1_id, t1_name FROM t1 WHERE t1_id IN (SELECT t2_id FROM t2 WHERE t1_name = t2_name LIMIT 10)"); +} + #[test] fn test_open() { let open_cursor = "OPEN Employee_Cursor"; From 5d63663bc6cd78d63d0cb2581ffc71a1dee3559d Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Mon, 23 Jun 2025 23:18:03 -0700 Subject: [PATCH 07/39] Use `IndexColumn` in all index definitions (#1900) --- src/ast/ddl.rs | 14 ++++---- src/ast/spans.rs | 35 +++++++++++-------- src/parser/mod.rs | 48 +++++++++++++++++-------- src/test_utils.rs | 44 +++++++++++++++++++++++ tests/sqlparser_mysql.rs | 75 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 35 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 059c6196..e35332da 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -32,9 +32,9 @@ use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, CommentDef, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, - FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, - OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, - ValueWithSpan, + FunctionDeterminismSpecifier, FunctionParallel, Ident, IndexColumn, MySQLColumnPosition, + ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, + Value, ValueWithSpan, }; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -979,7 +979,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Identifiers of the columns that are unique. - columns: Vec, + columns: Vec, index_options: Vec, characteristics: Option, /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` @@ -1015,7 +1015,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Identifiers of the columns that form the primary key. - columns: Vec, + columns: Vec, index_options: Vec, characteristics: Option, }, @@ -1060,7 +1060,7 @@ pub enum TableConstraint { /// [1]: IndexType index_type: Option, /// Referred column identifier list. - columns: Vec, + columns: Vec, }, /// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition is exactly the same, /// and MySQL displays both the same way, it is part of this definition as well. @@ -1083,7 +1083,7 @@ pub enum TableConstraint { /// Optional index name. opt_index_name: Option, /// Referred column identifier list. - columns: Vec, + columns: Vec, }, } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 14664b4c..ca321cc2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -28,16 +28,17 @@ use super::{ ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, - FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, NamedWindowDefinition, - ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, - OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, - RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, - SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, - TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithFill, + FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, + Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, + LateralView, LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, + NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, + OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, + ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, + TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, + UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -650,7 +651,7 @@ impl Spanned for TableConstraint { name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::PrimaryKey { @@ -664,7 +665,7 @@ impl Spanned for TableConstraint { name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::ForeignKey { @@ -700,7 +701,7 @@ impl Spanned for TableConstraint { } => union_spans( name.iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(columns.iter().map(|i| i.span())), ), TableConstraint::FulltextOrSpatial { fulltext: _, @@ -711,7 +712,7 @@ impl Spanned for TableConstraint { opt_index_name .iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(columns.iter().map(|i| i.span())), ), } } @@ -745,6 +746,12 @@ impl Spanned for CreateIndex { } } +impl Spanned for IndexColumn { + fn span(&self) -> Span { + self.column.span() + } +} + impl Spanned for CaseStatement { fn span(&self) -> Span { let CaseStatement { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 44bf58d0..35c05dfb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6868,9 +6868,7 @@ impl<'a> Parser<'a> { None }; - self.expect_token(&Token::LParen)?; - let columns = self.parse_comma_separated(Parser::parse_create_index_expr)?; - self.expect_token(&Token::RParen)?; + let columns = self.parse_parenthesized_index_column_list()?; let include = if self.parse_keyword(Keyword::INCLUDE) { self.expect_token(&Token::LParen)?; @@ -8070,7 +8068,7 @@ impl<'a> Parser<'a> { let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::Unique { @@ -8092,7 +8090,7 @@ impl<'a> Parser<'a> { let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::PrimaryKey { @@ -8170,7 +8168,7 @@ impl<'a> Parser<'a> { }; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; Ok(Some(TableConstraint::Index { display_as_key, @@ -8199,7 +8197,7 @@ impl<'a> Parser<'a> { let opt_index_name = self.parse_optional_ident()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_index_column_list()?; Ok(Some(TableConstraint::FulltextOrSpatial { fulltext, @@ -10601,6 +10599,14 @@ impl<'a> Parser<'a> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier()) } + /// Parses a parenthesized comma-separated list of index columns, which can be arbitrary + /// expressions with ordering information (and an opclass in some dialects). + fn parse_parenthesized_index_column_list(&mut self) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { + p.parse_create_index_expr() + }) + } + /// Parses a parenthesized comma-separated list of qualified, possibly quoted identifiers. /// For example: `(db1.sc1.tbl1.col1, db1.sc1.tbl1."col 2", ...)` pub fn parse_parenthesized_qualified_column_list( @@ -16527,6 +16533,20 @@ mod tests { }}; } + fn mk_expected_col(name: &str) -> IndexColumn { + IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(name.into()), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None, + } + } + let dialect = TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})]); @@ -16537,7 +16557,7 @@ mod tests { display_as_key: false, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16548,7 +16568,7 @@ mod tests { display_as_key: true, name: None, index_type: None, - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16559,7 +16579,7 @@ mod tests { display_as_key: false, name: Some(Ident::with_quote('\'', "index")), index_type: None, - columns: vec![Ident::new("c1"), Ident::new("c2")], + columns: vec![mk_expected_col("c1"), mk_expected_col("c2")], } ); @@ -16570,7 +16590,7 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16581,7 +16601,7 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16592,7 +16612,7 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); @@ -16603,7 +16623,7 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + columns: vec![mk_expected_col("c1")], } ); } diff --git a/src/test_utils.rs b/src/test_utils.rs index 24c0ca57..1af9f454 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -448,3 +448,47 @@ pub fn call(function: &str, args: impl IntoIterator) -> Expr { within_group: vec![], }) } + +/// Gets the first index column (mysql calls it a key part) of the first index found in a +/// [`Statement::CreateIndex`], [`Statement::CreateTable`], or [`Statement::AlterTable`]. +pub fn index_column(stmt: Statement) -> Expr { + match stmt { + Statement::CreateIndex(CreateIndex { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + Statement::CreateTable(CreateTable { constraints, .. }) => { + match constraints.first().unwrap() { + TableConstraint::Index { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + } + } + Statement::AlterTable { operations, .. } => match operations.first().unwrap() { + AlterTableOperation::AddConstraint(TableConstraint::Index { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + AlterTableOperation::AddConstraint(TableConstraint::Unique { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + AlterTableOperation::AddConstraint(TableConstraint::PrimaryKey { columns, .. }) => { + columns.first().unwrap().column.expr.clone() + } + AlterTableOperation::AddConstraint(TableConstraint::FulltextOrSpatial { + columns, + .. + }) => columns.first().unwrap().column.expr.clone(), + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + }, + _ => panic!("Expected CREATE INDEX, ALTER TABLE, or CREATE TABLE, got: {stmt:?}"), + } +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 540348ff..b11d76dd 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -670,6 +670,20 @@ fn table_constraint_unique_primary_ctor( characteristics: Option, unique_index_type_display: Option, ) -> TableConstraint { + let columns = columns + .into_iter() + .map(|ident| IndexColumn { + column: OrderByExpr { + expr: Expr::Identifier(ident), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }, + operator_class: None, + }) + .collect(); match unique_index_type_display { Some(index_type_display) => TableConstraint::Unique { name, @@ -795,6 +809,67 @@ fn parse_create_table_primary_and_unique_key_with_index_options() { } } +#[test] +fn parse_prefix_key_part() { + let expected = vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::value( + number("10"), + )))]; + for sql in [ + "CREATE INDEX idx_index ON t(textcol(10))", + "ALTER TABLE tab ADD INDEX idx_index (textcol(10))", + "ALTER TABLE tab ADD PRIMARY KEY (textcol(10))", + "ALTER TABLE tab ADD UNIQUE KEY (textcol(10))", + "ALTER TABLE tab ADD UNIQUE KEY (textcol(10))", + "ALTER TABLE tab ADD FULLTEXT INDEX (textcol(10))", + "CREATE TABLE t (textcol TEXT, INDEX idx_index (textcol(10)))", + ] { + match index_column(mysql_and_generic().verified_stmt(sql)) { + Expr::Function(Function { + name, + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => { + assert_eq!(name.to_string(), "textcol"); + assert_eq!(args, expected); + } + expr => panic!("unexpected expression {expr} for {sql}"), + } + } +} + +#[test] +fn test_functional_key_part() { + assert_eq!( + index_column( + mysql_and_generic() + .verified_stmt("CREATE INDEX idx_index ON t((col COLLATE utf8mb4_bin) DESC)") + ), + Expr::Nested(Box::new(Expr::Collate { + expr: Box::new(Expr::Identifier("col".into())), + collation: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier( + Ident::new("utf8mb4_bin") + )]), + })) + ); + assert_eq!( + index_column(mysql_and_generic().verified_stmt( + r#"CREATE TABLE t (jsoncol JSON, PRIMARY KEY ((CAST(col ->> '$.id' AS UNSIGNED)) ASC))"# + )), + Expr::Nested(Box::new(Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col"))), + op: BinaryOperator::LongArrow, + right: Box::new(Expr::Value( + Value::SingleQuotedString("$.id".to_string()).with_empty_span() + )), + }), + data_type: DataType::Unsigned, + format: None, + })), + ); +} + #[test] fn parse_create_table_primary_and_unique_key_with_index_type() { let sqls = ["UNIQUE", "PRIMARY KEY"].map(|key_ty| { From 44f3be38e5c9199479a4b986fdfaf417e925f0d4 Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Tue, 24 Jun 2025 09:29:44 +0300 Subject: [PATCH 08/39] fix: parse snowflake fetch clause (#1894) --- src/parser/mod.rs | 11 ++++++----- tests/sqlparser_snowflake.rs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 35c05dfb..a5e89069 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15023,7 +15023,8 @@ impl<'a> Parser<'a> { /// Parse a FETCH clause pub fn parse_fetch(&mut self) -> Result { - self.expect_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT])?; + let _ = self.parse_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]); + let (quantity, percent) = if self .parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS]) .is_some() @@ -15032,16 +15033,16 @@ impl<'a> Parser<'a> { } else { let quantity = Expr::Value(self.parse_value()?); let percent = self.parse_keyword(Keyword::PERCENT); - self.expect_one_of_keywords(&[Keyword::ROW, Keyword::ROWS])?; + let _ = self.parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS]); (Some(quantity), percent) }; + let with_ties = if self.parse_keyword(Keyword::ONLY) { false - } else if self.parse_keywords(&[Keyword::WITH, Keyword::TIES]) { - true } else { - return self.expected("one of ONLY or WITH TIES", self.peek_token()); + self.parse_keywords(&[Keyword::WITH, Keyword::TIES]) }; + Ok(Fetch { with_ties, percent, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b11a2cb0..7dc00f9a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4146,3 +4146,22 @@ END assert_eq!(2, exception[1].idents.len()); assert_eq!(2, exception[1].statements.len()); } + +#[test] +fn test_snowflake_fetch_clause_syntax() { + let canonical = "SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS ONLY"; + snowflake().verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2", canonical); + + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH FIRST 2", canonical); + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH NEXT 2", canonical); + + snowflake() + .verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2 ROW", canonical); + + snowflake().verified_only_select_with_canonical( + "SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS", + canonical, + ); +} From b9365b3853cf8878284746af07f7781915ae7052 Mon Sep 17 00:00:00 2001 From: ZacJW Date: Tue, 24 Jun 2025 07:39:02 +0100 Subject: [PATCH 09/39] Support procedure argmode (#1901) --- src/ast/ddl.rs | 9 ++++-- src/parser/mod.rs | 15 +++++++++- tests/sqlparser_common.rs | 62 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 6 ++-- 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e35332da..f81c6fc2 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,7 +30,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, CommentDef, CreateFunctionBody, + display_comma_separated, display_separated, ArgMode, CommentDef, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, Ident, IndexColumn, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, @@ -1367,11 +1367,16 @@ impl fmt::Display for NullsDistinctOption { pub struct ProcedureParam { pub name: Ident, pub data_type: DataType, + pub mode: Option, } impl fmt::Display for ProcedureParam { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {}", self.name, self.data_type) + if let Some(mode) = &self.mode { + write!(f, "{mode} {} {}", self.name, self.data_type) + } else { + write!(f, "{} {}", self.name, self.data_type) + } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a5e89069..ca658d7d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7624,9 +7624,22 @@ impl<'a> Parser<'a> { } pub fn parse_procedure_param(&mut self) -> Result { + let mode = if self.parse_keyword(Keyword::IN) { + Some(ArgMode::In) + } else if self.parse_keyword(Keyword::OUT) { + Some(ArgMode::Out) + } else if self.parse_keyword(Keyword::INOUT) { + Some(ArgMode::InOut) + } else { + None + }; let name = self.parse_identifier()?; let data_type = self.parse_data_type()?; - Ok(ProcedureParam { name, data_type }) + Ok(ProcedureParam { + name, + data_type, + mode, + }) } pub fn parse_column_def(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 52054604..d45da69a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15356,3 +15356,65 @@ fn check_enforced() { "CREATE TABLE t (a INT, b INT, c INT, CHECK (a > 0) NOT ENFORCED, CHECK (b > 0) ENFORCED, CHECK (c > 0))", ); } + +#[test] +fn parse_create_procedure_with_parameter_modes() { + let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER, OUT b TEXT, INOUT c TIMESTAMP, d BOOL) AS BEGIN SELECT 1; END"#; + match verified_stmt(sql) { + Statement::CreateProcedure { + or_alter, + name, + params, + .. + } => { + assert_eq!(or_alter, false); + assert_eq!(name.to_string(), "test_proc"); + let fake_span = Span { + start: Location { line: 0, column: 0 }, + end: Location { line: 0, column: 0 }, + }; + assert_eq!( + params, + Some(vec![ + ProcedureParam { + name: Ident { + value: "a".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Integer(None), + mode: Some(ArgMode::In) + }, + ProcedureParam { + name: Ident { + value: "b".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Text, + mode: Some(ArgMode::Out) + }, + ProcedureParam { + name: Ident { + value: "c".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Timestamp(None, TimezoneInfo::None), + mode: Some(ArgMode::InOut) + }, + ProcedureParam { + name: Ident { + value: "d".into(), + quote_style: None, + span: fake_span, + }, + data_type: DataType::Bool, + mode: None + }, + ]) + ); + } + _ => unreachable!(), + } +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2a314502..8edb100a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -153,7 +153,8 @@ fn parse_create_procedure() { quote_style: None, span: Span::empty(), }, - data_type: DataType::Int(None) + data_type: DataType::Int(None), + mode: None, }, ProcedureParam { name: Ident { @@ -164,7 +165,8 @@ fn parse_create_procedure() { data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 256, unit: None - })) + })), + mode: None, } ]), name: ObjectName::from(vec![Ident { From b2ab0061c172aa55e06ece592309a96083d2220d Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Wed, 25 Jun 2025 12:21:59 +0200 Subject: [PATCH 10/39] Fix `impl Ord for Ident` (#1893) --- src/ast/mod.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 17464f4a..ef0c4dc9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -28,6 +28,7 @@ use helpers::{ stmt_data_loading::{FileStagingCommand, StageLoadSelectItemKind}, }; +use core::cmp::Ordering; use core::ops::Deref; use core::{ fmt::{self, Display}, @@ -172,7 +173,7 @@ fn format_statement_list(f: &mut fmt::Formatter, statements: &[Statement]) -> fm } /// An identifier, decomposed into its value or character data and the quote style. -#[derive(Debug, Clone, PartialOrd, Ord)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Ident { @@ -214,6 +215,35 @@ impl core::hash::Hash for Ident { impl Eq for Ident {} +impl PartialOrd for Ident { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Ident { + fn cmp(&self, other: &Self) -> Ordering { + let Ident { + value, + quote_style, + // exhaustiveness check; we ignore spans in ordering + span: _, + } = self; + + let Ident { + value: other_value, + quote_style: other_quote_style, + // exhaustiveness check; we ignore spans in ordering + span: _, + } = other; + + // First compare by value, then by quote_style + value + .cmp(other_value) + .then_with(|| quote_style.cmp(other_quote_style)) + } +} + impl Ident { /// Create a new identifier with the given value and no quotes and an empty span. pub fn new(value: S) -> Self @@ -4214,7 +4244,7 @@ pub enum Statement { /// ```sql /// NOTIFY channel [ , payload ] /// ``` - /// send a notification event together with an optional “payload” string to channel + /// send a notification event together with an optional "payload" string to channel /// /// See Postgres NOTIFY { @@ -9771,6 +9801,8 @@ impl fmt::Display for NullInclusion { #[cfg(test)] mod tests { + use crate::tokenizer::Location; + use super::*; #[test] @@ -10066,4 +10098,16 @@ mod tests { test_steps(OneOrManyWithParens::Many(vec![2]), vec![2], 3); test_steps(OneOrManyWithParens::Many(vec![3, 4]), vec![3, 4], 4); } + + // Tests that the position in the code of an `Ident` does not affect its + // ordering. + #[test] + fn test_ident_ord() { + let mut a = Ident::with_span(Span::new(Location::new(1, 1), Location::new(1, 1)), "a"); + let mut b = Ident::with_span(Span::new(Location::new(2, 2), Location::new(2, 2)), "b"); + + assert!(a < b); + std::mem::swap(&mut a.span, &mut b.span); + assert!(a < b); + } } From 1bbc05cdff7952182e2cdafe55091de0f1f7b3ce Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Wed, 25 Jun 2025 16:10:01 +0200 Subject: [PATCH 11/39] Snowflake: support multiple column options in `CREATE VIEW` (#1891) --- src/ast/ddl.rs | 28 ++++++++++++++++++++++++++-- src/ast/mod.rs | 15 ++++++++------- src/ast/spans.rs | 17 +++++++++++------ src/dialect/mod.rs | 4 ++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 31 ++++++++++++++++++++----------- tests/sqlparser_bigquery.rs | 18 ++++++++++-------- tests/sqlparser_clickhouse.rs | 4 ++-- tests/sqlparser_common.rs | 2 +- tests/sqlparser_snowflake.rs | 15 ++++++++++++--- 10 files changed, 98 insertions(+), 40 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f81c6fc2..d2863c3a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1426,7 +1426,24 @@ impl fmt::Display for ColumnDef { pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, - pub options: Option>, + pub options: Option, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ColumnOptions { + CommaSeparated(Vec), + SpaceSeparated(Vec), +} + +impl ColumnOptions { + pub fn as_slice(&self) -> &[ColumnOption] { + match self { + ColumnOptions::CommaSeparated(options) => options.as_slice(), + ColumnOptions::SpaceSeparated(options) => options.as_slice(), + } + } } impl fmt::Display for ViewColumnDef { @@ -1436,7 +1453,14 @@ impl fmt::Display for ViewColumnDef { write!(f, " {}", data_type)?; } if let Some(options) = self.options.as_ref() { - write!(f, " {}", display_comma_separated(options.as_slice()))?; + match options { + ColumnOptions::CommaSeparated(column_options) => { + write!(f, " {}", display_comma_separated(column_options.as_slice()))?; + } + ColumnOptions::SpaceSeparated(column_options) => { + write!(f, " {}", display_separated(column_options.as_slice(), " "))? + } + } } Ok(()) } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ef0c4dc9..bfed91fb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -61,13 +61,14 @@ pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, Deduplicate, - DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, - ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, TagsColumnOption, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, + Deduplicate, DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, + Partition, ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, + TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index ca321cc2..78ed772b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use crate::ast::query::SelectItemQualifiedWildcardKind; +use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions}; use core::iter; use crate::tokenizer::Span; @@ -991,10 +991,13 @@ impl Spanned for ViewColumnDef { options, } = self; - union_spans( - core::iter::once(name.span) - .chain(options.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ) + name.span.union_opt(&options.as_ref().map(|o| o.span())) + } +} + +impl Spanned for ColumnOptions { + fn span(&self) -> Span { + union_spans(self.as_slice().iter().map(|i| i.span())) } } @@ -1055,7 +1058,9 @@ impl Spanned for CreateTableOptions { match self { CreateTableOptions::None => Span::empty(), CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())), - CreateTableOptions::Options(vec) => union_spans(vec.iter().map(|i| i.span())), + CreateTableOptions::Options(vec) => { + union_spans(vec.as_slice().iter().map(|i| i.span())) + } CreateTableOptions::Plain(vec) => union_spans(vec.iter().map(|i| i.span())), CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())), } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a4c899e6..bc92948d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1028,6 +1028,10 @@ pub trait Dialect: Debug + Any { fn supports_set_names(&self) -> bool { false } + + fn supports_space_separated_column_options(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 66e04ac2..5ebb7e37 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -356,6 +356,10 @@ impl Dialect for SnowflakeDialect { fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR } + + fn supports_space_separated_column_options(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ca658d7d..44ba0bb7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10579,17 +10579,7 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier()?; - let options = if (dialect_of!(self is BigQueryDialect | GenericDialect) - && self.parse_keyword(Keyword::OPTIONS)) - || (dialect_of!(self is SnowflakeDialect | GenericDialect) - && self.parse_keyword(Keyword::COMMENT)) - { - self.prev_token(); - self.parse_optional_column_option()? - .map(|option| vec![option]) - } else { - None - }; + let options = self.parse_view_column_options()?; let data_type = if dialect_of!(self is ClickHouseDialect) { Some(self.parse_data_type()?) } else { @@ -10602,6 +10592,25 @@ impl<'a> Parser<'a> { }) } + fn parse_view_column_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + loop { + let option = self.parse_optional_column_option()?; + if let Some(option) = option { + options.push(option); + } else { + break; + } + } + if options.is_empty() { + Ok(None) + } else if self.dialect.supports_space_separated_column_options() { + Ok(Some(ColumnOptions::SpaceSeparated(options))) + } else { + Ok(Some(ColumnOptions::CommaSeparated(options))) + } + } + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 0de0b12b..2bcdb4e5 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -355,14 +355,16 @@ fn parse_create_view_with_options() { ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { - key: Ident::new("description"), - value: Expr::Value( - Value::DoubleQuotedString("field age".to_string()).with_span( - Span::new(Location::new(1, 42), Location::new(1, 52)) - ) - ), - }])]), + options: Some(ColumnOptions::CommaSeparated(vec![ColumnOption::Options( + vec![SqlOption::KeyValue { + key: Ident::new("description"), + value: Expr::Value( + Value::DoubleQuotedString("field age".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), + }] + )])), }, ], columns diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 93b4c4f5..ed5d7ad2 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -914,7 +914,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ViewColumnDef { name: "f".into(), @@ -926,7 +926,7 @@ fn parse_create_view_with_fields_data_types() { }]), vec![] )), - options: None + options: None, }, ] ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d45da69a..3c20f82a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7990,7 +7990,7 @@ fn parse_create_view_with_columns() { .map(|name| ViewColumnDef { name, data_type: None, - options: None + options: None, }) .collect::>() ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7dc00f9a..8b3988d9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3124,7 +3124,7 @@ fn view_comment_option_should_be_after_column_list() { "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') COMMENT = 'Comment' AS SELECT a FROM t", "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') WITH (foo = bar) COMMENT = 'Comment' AS SELECT a FROM t", ] { - snowflake_and_generic() + snowflake() .verified_stmt(sql); } } @@ -3133,7 +3133,7 @@ fn view_comment_option_should_be_after_column_list() { fn parse_view_column_descriptions() { let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; - match snowflake_and_generic().verified_stmt(sql) { + match snowflake().verified_stmt(sql) { Statement::CreateView { name, columns, .. } => { assert_eq!(name.to_string(), "v"); assert_eq!( @@ -3142,7 +3142,9 @@ fn parse_view_column_descriptions() { ViewColumnDef { name: Ident::new("a"), data_type: None, - options: Some(vec![ColumnOption::Comment("Comment".to_string())]), + options: Some(ColumnOptions::SpaceSeparated(vec![ColumnOption::Comment( + "Comment".to_string() + )])), }, ViewColumnDef { name: Ident::new("b"), @@ -4165,3 +4167,10 @@ fn test_snowflake_fetch_clause_syntax() { canonical, ); } + +#[test] +fn test_snowflake_create_view_with_multiple_column_options() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} From 95d16e3b2d08ba74ada44dbcfe4ece3400b2d89b Mon Sep 17 00:00:00 2001 From: ZacJW Date: Fri, 27 Jun 2025 17:22:21 +0100 Subject: [PATCH 12/39] Add support for `LANGUAGE` clause in `CREATE PROCEDURE` (#1903) --- src/ast/mod.rs | 6 ++++++ src/parser/mod.rs | 8 ++++++++ tests/sqlparser_common.rs | 30 ++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 3 ++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bfed91fb..1ea0b5c2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3945,6 +3945,7 @@ pub enum Statement { or_alter: bool, name: ObjectName, params: Option>, + language: Option, body: ConditionalStatements, }, /// ```sql @@ -4848,6 +4849,7 @@ impl fmt::Display for Statement { name, or_alter, params, + language, body, } => { write!( @@ -4863,6 +4865,10 @@ impl fmt::Display for Statement { } } + if let Some(language) = language { + write!(f, " LANGUAGE {language}")?; + } + write!(f, " AS {body}") } Statement::CreateMacro { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 44ba0bb7..e09b7e33 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15802,6 +15802,13 @@ impl<'a> Parser<'a> { pub fn parse_create_procedure(&mut self, or_alter: bool) -> Result { let name = self.parse_object_name(false)?; let params = self.parse_optional_procedure_parameters()?; + + let language = if self.parse_keyword(Keyword::LANGUAGE) { + Some(self.parse_identifier()?) + } else { + None + }; + self.expect_keyword_is(Keyword::AS)?; let body = self.parse_conditional_statements(&[Keyword::END])?; @@ -15810,6 +15817,7 @@ impl<'a> Parser<'a> { name, or_alter, params, + language, body, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c20f82a..93cc89e5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15357,6 +15357,36 @@ fn check_enforced() { ); } +#[test] +fn parse_create_procedure_with_language() { + let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#; + match verified_stmt(sql) { + Statement::CreateProcedure { + or_alter, + name, + params, + language, + .. + } => { + assert_eq!(or_alter, false); + assert_eq!(name.to_string(), "test_proc"); + assert_eq!(params, Some(vec![])); + assert_eq!( + language, + Some(Ident { + value: "sql".into(), + quote_style: None, + span: Span { + start: Location::empty(), + end: Location::empty() + } + }) + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_procedure_with_parameter_modes() { let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER, OUT b TEXT, INOUT c TIMESTAMP, d BOOL) AS BEGIN SELECT 1; END"#; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 8edb100a..9ec28f42 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -173,7 +173,8 @@ fn parse_create_procedure() { value: "test".into(), quote_style: None, span: Span::empty(), - }]) + }]), + language: None, } ) } From 5f2b5fe7beb0698663f9ba9572c711c6bee9ef6e Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 27 Jun 2025 20:21:17 +0200 Subject: [PATCH 13/39] Fix clippy lints on 1.88.0 (#1910) --- examples/cli.rs | 2 +- sqlparser_bench/benches/sqlparser_bench.rs | 11 ++- src/ast/data_type.rs | 16 ++-- src/ast/dcl.rs | 18 ++-- src/ast/ddl.rs | 28 +++---- src/ast/dml.rs | 12 +-- src/ast/helpers/key_value_options.rs | 2 +- src/ast/mod.rs | 96 +++++++++++----------- src/ast/query.rs | 76 ++++++++--------- src/ast/value.rs | 6 +- src/ast/visitor.rs | 4 +- src/dialect/mod.rs | 2 +- src/dialect/postgresql.rs | 2 +- src/parser/mod.rs | 13 ++- src/test_utils.rs | 2 +- src/tokenizer.rs | 10 +-- tests/sqlparser_clickhouse.rs | 6 +- tests/sqlparser_common.rs | 20 ++--- tests/sqlparser_databricks.rs | 12 +-- tests/sqlparser_duckdb.rs | 10 ++- tests/sqlparser_hive.rs | 4 +- tests/sqlparser_mssql.rs | 10 +-- tests/sqlparser_mysql.rs | 47 ++++++----- tests/sqlparser_postgres.rs | 11 ++- tests/sqlparser_snowflake.rs | 32 ++++---- tests/sqlparser_sqlite.rs | 6 +- 26 files changed, 226 insertions(+), 232 deletions(-) diff --git a/examples/cli.rs b/examples/cli.rs index 0252fca7..08a40a6d 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -63,7 +63,7 @@ $ cargo run --example cli - [--dialectname] }; let contents = if filename == "-" { - println!("Parsing from stdin using {:?}", dialect); + println!("Parsing from stdin using {dialect:?}"); let mut buf = Vec::new(); stdin() .read_to_end(&mut buf) diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index 24c59c07..6132ee43 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -45,25 +45,24 @@ fn basic_queries(c: &mut Criterion) { let large_statement = { let expressions = (0..1000) - .map(|n| format!("FN_{}(COL_{})", n, n)) + .map(|n| format!("FN_{n}(COL_{n})")) .collect::>() .join(", "); let tables = (0..1000) - .map(|n| format!("TABLE_{}", n)) + .map(|n| format!("TABLE_{n}")) .collect::>() .join(" JOIN "); let where_condition = (0..1000) - .map(|n| format!("COL_{} = {}", n, n)) + .map(|n| format!("COL_{n} = {n}")) .collect::>() .join(" OR "); let order_condition = (0..1000) - .map(|n| format!("COL_{} DESC", n)) + .map(|n| format!("COL_{n} DESC")) .collect::>() .join(", "); format!( - "SELECT {} FROM {} WHERE {} ORDER BY {}", - expressions, tables, where_condition, order_condition + "SELECT {expressions} FROM {tables} WHERE {where_condition} ORDER BY {order_condition}" ) }; diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index ef04b7e4..0897f2db 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -666,7 +666,7 @@ impl fmt::Display for DataType { } DataType::Enum(vals, bits) => { match bits { - Some(bits) => write!(f, "ENUM{}", bits), + Some(bits) => write!(f, "ENUM{bits}"), None => write!(f, "ENUM"), }?; write!(f, "(")?; @@ -714,16 +714,16 @@ impl fmt::Display for DataType { } // ClickHouse DataType::Nullable(data_type) => { - write!(f, "Nullable({})", data_type) + write!(f, "Nullable({data_type})") } DataType::FixedString(character_length) => { - write!(f, "FixedString({})", character_length) + write!(f, "FixedString({character_length})") } DataType::LowCardinality(data_type) => { - write!(f, "LowCardinality({})", data_type) + write!(f, "LowCardinality({data_type})") } DataType::Map(key_data_type, value_data_type) => { - write!(f, "Map({}, {})", key_data_type, value_data_type) + write!(f, "Map({key_data_type}, {value_data_type})") } DataType::Tuple(fields) => { write!(f, "Tuple({})", display_comma_separated(fields)) @@ -745,7 +745,7 @@ impl fmt::Display for DataType { DataType::NamedTable { name, columns } => { write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) } - DataType::GeometricType(kind) => write!(f, "{}", kind), + DataType::GeometricType(kind) => write!(f, "{kind}"), DataType::TsVector => write!(f, "TSVECTOR"), DataType::TsQuery => write!(f, "TSQUERY"), } @@ -942,7 +942,7 @@ impl fmt::Display for CharacterLength { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CharacterLength::IntegerLength { length, unit } => { - write!(f, "{}", length)?; + write!(f, "{length}")?; if let Some(unit) = unit { write!(f, " {unit}")?; } @@ -997,7 +997,7 @@ impl fmt::Display for BinaryLength { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BinaryLength::IntegerLength { length } => { - write!(f, "{}", length)?; + write!(f, "{length}")?; } BinaryLength::Max => { write!(f, "MAX")?; diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 735ab0cc..07989407 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -173,7 +173,7 @@ impl fmt::Display for AlterRoleOperation { in_database, } => { if let Some(database_name) = in_database { - write!(f, "IN DATABASE {} ", database_name)?; + write!(f, "IN DATABASE {database_name} ")?; } match config_value { @@ -187,7 +187,7 @@ impl fmt::Display for AlterRoleOperation { in_database, } => { if let Some(database_name) = in_database { - write!(f, "IN DATABASE {} ", database_name)?; + write!(f, "IN DATABASE {database_name} ")?; } match config_name { @@ -218,15 +218,15 @@ impl fmt::Display for Use { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("USE ")?; match self { - Use::Catalog(name) => write!(f, "CATALOG {}", name), - Use::Schema(name) => write!(f, "SCHEMA {}", name), - Use::Database(name) => write!(f, "DATABASE {}", name), - Use::Warehouse(name) => write!(f, "WAREHOUSE {}", name), - Use::Role(name) => write!(f, "ROLE {}", name), + Use::Catalog(name) => write!(f, "CATALOG {name}"), + Use::Schema(name) => write!(f, "SCHEMA {name}"), + Use::Database(name) => write!(f, "DATABASE {name}"), + Use::Warehouse(name) => write!(f, "WAREHOUSE {name}"), + Use::Role(name) => write!(f, "ROLE {name}"), Use::SecondaryRoles(secondary_roles) => { - write!(f, "SECONDARY ROLES {}", secondary_roles) + write!(f, "SECONDARY ROLES {secondary_roles}") } - Use::Object(name) => write!(f, "{}", name), + Use::Object(name) => write!(f, "{name}"), Use::Default => write!(f, "DEFAULT"), } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d2863c3a..afe9f228 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -57,7 +57,7 @@ impl fmt::Display for ReplicaIdentity { ReplicaIdentity::None => f.write_str("NONE"), ReplicaIdentity::Full => f.write_str("FULL"), ReplicaIdentity::Default => f.write_str("DEFAULT"), - ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {}", idx), + ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {idx}"), } } } @@ -450,7 +450,7 @@ pub enum Owner { impl fmt::Display for Owner { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Owner::Ident(ident) => write!(f, "{}", ident), + Owner::Ident(ident) => write!(f, "{ident}"), Owner::CurrentRole => write!(f, "CURRENT_ROLE"), Owner::CurrentUser => write!(f, "CURRENT_USER"), Owner::SessionUser => write!(f, "SESSION_USER"), @@ -525,7 +525,7 @@ impl fmt::Display for AlterTableOperation { if *if_not_exists { write!(f, " IF NOT EXISTS")?; } - write!(f, " {} ({})", name, query) + write!(f, " {name} ({query})") } AlterTableOperation::Algorithm { equals, algorithm } => { write!( @@ -540,7 +540,7 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name) + write!(f, " {name}") } AlterTableOperation::MaterializeProjection { if_exists, @@ -551,9 +551,9 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name)?; + write!(f, " {name}")?; if let Some(partition) = partition { - write!(f, " IN PARTITION {}", partition)?; + write!(f, " IN PARTITION {partition}")?; } Ok(()) } @@ -566,9 +566,9 @@ impl fmt::Display for AlterTableOperation { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {}", name)?; + write!(f, " {name}")?; if let Some(partition) = partition { - write!(f, " IN PARTITION {}", partition)?; + write!(f, " IN PARTITION {partition}")?; } Ok(()) } @@ -1168,7 +1168,7 @@ impl fmt::Display for TableConstraint { write!(f, " ON UPDATE {action}")?; } if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1308,7 +1308,7 @@ impl fmt::Display for IndexType { Self::SPGiST => write!(f, "SPGIST"), Self::BRIN => write!(f, "BRIN"), Self::Bloom => write!(f, "BLOOM"), - Self::Custom(name) => write!(f, "{}", name), + Self::Custom(name) => write!(f, "{name}"), } } } @@ -1450,7 +1450,7 @@ impl fmt::Display for ViewColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; if let Some(data_type) = self.data_type.as_ref() { - write!(f, " {}", data_type)?; + write!(f, " {data_type}")?; } if let Some(options) = self.options.as_ref() { match options { @@ -1845,7 +1845,7 @@ impl fmt::Display for ColumnOption { } => { write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" })?; if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1867,7 +1867,7 @@ impl fmt::Display for ColumnOption { write!(f, " ON UPDATE {action}")?; } if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + write!(f, " {characteristics}")?; } Ok(()) } @@ -1927,7 +1927,7 @@ impl fmt::Display for ColumnOption { write!(f, "{parameters}") } OnConflict(keyword) => { - write!(f, "ON CONFLICT {:?}", keyword)?; + write!(f, "ON CONFLICT {keyword:?}")?; Ok(()) } Policy(parameters) => { diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 292650c8..e179f5d7 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -55,7 +55,7 @@ impl Display for IndexColumn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.column)?; if let Some(operator_class) = &self.operator_class { - write!(f, " {}", operator_class)?; + write!(f, " {operator_class}")?; } Ok(()) } @@ -266,7 +266,7 @@ impl Display for CreateTable { name = self.name, )?; if let Some(on_cluster) = &self.on_cluster { - write!(f, " ON CLUSTER {}", on_cluster)?; + write!(f, " ON CLUSTER {on_cluster}")?; } if !self.columns.is_empty() || !self.constraints.is_empty() { f.write_str(" (")?; @@ -383,15 +383,15 @@ impl Display for CreateTable { match &self.table_options { options @ CreateTableOptions::With(_) | options @ CreateTableOptions::Plain(_) - | options @ CreateTableOptions::TableProperties(_) => write!(f, " {}", options)?, + | options @ CreateTableOptions::TableProperties(_) => write!(f, " {options}")?, _ => (), } if let Some(primary_key) = &self.primary_key { - write!(f, " PRIMARY KEY {}", primary_key)?; + write!(f, " PRIMARY KEY {primary_key}")?; } if let Some(order_by) = &self.order_by { - write!(f, " ORDER BY {}", order_by)?; + write!(f, " ORDER BY {order_by}")?; } if let Some(inherits) = &self.inherits { write!(f, " INHERITS ({})", display_comma_separated(inherits))?; @@ -403,7 +403,7 @@ impl Display for CreateTable { write!(f, " CLUSTER BY {cluster_by}")?; } if let options @ CreateTableOptions::Options(_) = &self.table_options { - write!(f, " {}", options)?; + write!(f, " {options}")?; } if let Some(external_volume) = self.external_volume.as_ref() { write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?; diff --git a/src/ast/helpers/key_value_options.rs b/src/ast/helpers/key_value_options.rs index 06f028dd..796bfd5e 100644 --- a/src/ast/helpers/key_value_options.rs +++ b/src/ast/helpers/key_value_options.rs @@ -67,7 +67,7 @@ impl fmt::Display for KeyValueOptions { } else { f.write_str(" ")?; } - write!(f, "{}", option)?; + write!(f, "{option}")?; } } Ok(()) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1ea0b5c2..0f682062 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -357,7 +357,7 @@ impl ObjectNamePart { impl fmt::Display for ObjectNamePart { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - ObjectNamePart::Identifier(ident) => write!(f, "{}", ident), + ObjectNamePart::Identifier(ident) => write!(f, "{ident}"), } } } @@ -1210,8 +1210,8 @@ pub enum AccessExpr { impl fmt::Display for AccessExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AccessExpr::Dot(expr) => write!(f, ".{}", expr), - AccessExpr::Subscript(subscript) => write!(f, "[{}]", subscript), + AccessExpr::Dot(expr) => write!(f, ".{expr}"), + AccessExpr::Subscript(subscript) => write!(f, "[{subscript}]"), } } } @@ -1413,12 +1413,12 @@ impl fmt::Display for Expr { match self { Expr::Identifier(s) => write!(f, "{s}"), Expr::Wildcard(_) => f.write_str("*"), - Expr::QualifiedWildcard(prefix, _) => write!(f, "{}.*", prefix), + Expr::QualifiedWildcard(prefix, _) => write!(f, "{prefix}.*"), Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), Expr::CompoundFieldAccess { root, access_chain } => { - write!(f, "{}", root)?; + write!(f, "{root}")?; for field in access_chain { - write!(f, "{}", field)?; + write!(f, "{field}")?; } Ok(()) } @@ -1547,7 +1547,7 @@ impl fmt::Display for Expr { } => { let not_ = if *negated { "NOT " } else { "" }; if form.is_none() { - write!(f, "{} IS {}NORMALIZED", expr, not_) + write!(f, "{expr} IS {not_}NORMALIZED") } else { write!( f, @@ -1869,7 +1869,7 @@ impl fmt::Display for Expr { } } Expr::Named { expr, name } => { - write!(f, "{} AS {}", expr, name) + write!(f, "{expr} AS {name}") } Expr::Dictionary(fields) => { write!(f, "{{{}}}", display_comma_separated(fields)) @@ -2425,7 +2425,7 @@ impl fmt::Display for ConditionalStatements { } Ok(()) } - ConditionalStatements::BeginEnd(bes) => write!(f, "{}", bes), + ConditionalStatements::BeginEnd(bes) => write!(f, "{bes}"), } } } @@ -2945,9 +2945,7 @@ impl Display for Set { write!( f, "SET {modifier}ROLE {role_name}", - modifier = context_modifier - .map(|m| format!("{}", m)) - .unwrap_or_default() + modifier = context_modifier.map(|m| format!("{m}")).unwrap_or_default() ) } Self::SetSessionParam(kind) => write!(f, "SET {kind}"), @@ -2980,7 +2978,7 @@ impl Display for Set { charset_name, collation_name, } => { - write!(f, "SET NAMES {}", charset_name)?; + write!(f, "SET NAMES {charset_name}")?; if let Some(collation) = collation_name { f.write_str(" COLLATE ")?; @@ -3003,7 +3001,7 @@ impl Display for Set { write!( f, "SET {}{}{} = {}", - scope.map(|s| format!("{}", s)).unwrap_or_default(), + scope.map(|s| format!("{s}")).unwrap_or_default(), if *hivevar { "HIVEVAR:" } else { "" }, variable, display_comma_separated(values) @@ -4405,7 +4403,7 @@ impl fmt::Display for Statement { write!(f, "{describe_alias} ")?; if let Some(format) = hive_format { - write!(f, "{} ", format)?; + write!(f, "{format} ")?; } if *has_table_keyword { write!(f, "TABLE ")?; @@ -5241,7 +5239,7 @@ impl fmt::Display for Statement { if *only { write!(f, "ONLY ")?; } - write!(f, "{name} ", name = name)?; + write!(f, "{name} ")?; if let Some(cluster) = on_cluster { write!(f, "ON CLUSTER {cluster} ")?; } @@ -5319,7 +5317,7 @@ impl fmt::Display for Statement { )?; if !session_params.options.is_empty() { if *set { - write!(f, " {}", session_params)?; + write!(f, " {session_params}")?; } else { let options = session_params .options @@ -5353,7 +5351,7 @@ impl fmt::Display for Statement { if *purge { " PURGE" } else { "" }, )?; if let Some(table_name) = table.as_ref() { - write!(f, " ON {}", table_name)?; + write!(f, " ON {table_name}")?; }; Ok(()) } @@ -5608,7 +5606,7 @@ impl fmt::Display for Statement { } => { if *syntax_begin { if let Some(modifier) = *modifier { - write!(f, "BEGIN {}", modifier)?; + write!(f, "BEGIN {modifier}")?; } else { write!(f, "BEGIN")?; } @@ -5644,7 +5642,7 @@ impl fmt::Display for Statement { if *end_syntax { write!(f, "END")?; if let Some(modifier) = *modifier { - write!(f, " {}", modifier)?; + write!(f, " {modifier}")?; } if *chain { write!(f, " AND CHAIN")?; @@ -5743,7 +5741,7 @@ impl fmt::Display for Statement { write!(f, " GRANTED BY {grantor}")?; } if let Some(cascade) = cascade { - write!(f, " {}", cascade)?; + write!(f, " {cascade}")?; } Ok(()) } @@ -5922,13 +5920,13 @@ impl fmt::Display for Statement { if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, )?; if !directory_table_params.options.is_empty() { - write!(f, " DIRECTORY=({})", directory_table_params)?; + write!(f, " DIRECTORY=({directory_table_params})")?; } if !file_format.options.is_empty() { - write!(f, " FILE_FORMAT=({})", file_format)?; + write!(f, " FILE_FORMAT=({file_format})")?; } if !copy_options.options.is_empty() { - write!(f, " COPY_OPTIONS=({})", copy_options)?; + write!(f, " COPY_OPTIONS=({copy_options})")?; } if comment.is_some() { write!(f, " COMMENT='{}'", comment.as_ref().unwrap())?; @@ -5951,7 +5949,7 @@ impl fmt::Display for Statement { validation_mode, partition, } => { - write!(f, "COPY INTO {}", into)?; + write!(f, "COPY INTO {into}")?; if let Some(into_columns) = into_columns { write!(f, " ({})", display_comma_separated(into_columns))?; } @@ -5967,12 +5965,12 @@ impl fmt::Display for Statement { )?; } if let Some(from_obj_alias) = from_obj_alias { - write!(f, " AS {}", from_obj_alias)?; + write!(f, " AS {from_obj_alias}")?; } write!(f, ")")?; } else if let Some(from_obj) = from_obj { // Standard data load - write!(f, " FROM {}{}", from_obj, stage_params)?; + write!(f, " FROM {from_obj}{stage_params}")?; if let Some(from_obj_alias) = from_obj_alias { write!(f, " AS {from_obj_alias}")?; } @@ -5985,24 +5983,24 @@ impl fmt::Display for Statement { write!(f, " FILES = ('{}')", display_separated(files, "', '"))?; } if let Some(pattern) = pattern { - write!(f, " PATTERN = '{}'", pattern)?; + write!(f, " PATTERN = '{pattern}'")?; } if let Some(partition) = partition { write!(f, " PARTITION BY {partition}")?; } if !file_format.options.is_empty() { - write!(f, " FILE_FORMAT=({})", file_format)?; + write!(f, " FILE_FORMAT=({file_format})")?; } if !copy_options.options.is_empty() { match kind { CopyIntoSnowflakeKind::Table => { - write!(f, " COPY_OPTIONS=({})", copy_options)? + write!(f, " COPY_OPTIONS=({copy_options})")? } CopyIntoSnowflakeKind::Location => write!(f, " {copy_options}")?, } } if let Some(validation_mode) = validation_mode { - write!(f, " VALIDATION_MODE = {}", validation_mode)?; + write!(f, " VALIDATION_MODE = {validation_mode}")?; } Ok(()) } @@ -6048,10 +6046,10 @@ impl fmt::Display for Statement { } => { write!(f, "OPTIMIZE TABLE {name}")?; if let Some(on_cluster) = on_cluster { - write!(f, " ON CLUSTER {on_cluster}", on_cluster = on_cluster)?; + write!(f, " ON CLUSTER {on_cluster}")?; } if let Some(partition) = partition { - write!(f, " {partition}", partition = partition)?; + write!(f, " {partition}")?; } if *include_final { write!(f, " FINAL")?; @@ -6178,7 +6176,7 @@ impl fmt::Display for SetAssignment { write!( f, "{}{} = {}", - self.scope.map(|s| format!("{}", s)).unwrap_or_default(), + self.scope.map(|s| format!("{s}")).unwrap_or_default(), self.name, self.value ) @@ -6907,7 +6905,7 @@ impl fmt::Display for GranteeName { match self { GranteeName::ObjectName(name) => name.fmt(f), GranteeName::UserHost { user, host } => { - write!(f, "{}@{}", user, host) + write!(f, "{user}@{host}") } } } @@ -7077,7 +7075,7 @@ pub enum AssignmentTarget { impl fmt::Display for AssignmentTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - AssignmentTarget::ColumnName(column) => write!(f, "{}", column), + AssignmentTarget::ColumnName(column) => write!(f, "{column}"), AssignmentTarget::Tuple(columns) => write!(f, "({})", display_comma_separated(columns)), } } @@ -7322,8 +7320,8 @@ impl fmt::Display for FunctionArguments { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FunctionArguments::None => Ok(()), - FunctionArguments::Subquery(query) => write!(f, "({})", query), - FunctionArguments::List(args) => write!(f, "({})", args), + FunctionArguments::Subquery(query) => write!(f, "({query})"), + FunctionArguments::List(args) => write!(f, "({args})"), } } } @@ -7344,7 +7342,7 @@ pub struct FunctionArgumentList { impl fmt::Display for FunctionArgumentList { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(duplicate_treatment) = self.duplicate_treatment { - write!(f, "{} ", duplicate_treatment)?; + write!(f, "{duplicate_treatment} ")?; } write!(f, "{}", display_comma_separated(&self.args))?; if !self.clauses.is_empty() { @@ -7404,7 +7402,7 @@ impl fmt::Display for FunctionArgumentClause { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment) => { - write!(f, "{}", null_treatment) + write!(f, "{null_treatment}") } FunctionArgumentClause::OrderBy(order_by) => { write!(f, "ORDER BY {}", display_comma_separated(order_by)) @@ -7860,12 +7858,12 @@ pub enum SqlOption { impl fmt::Display for SqlOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - SqlOption::Clustered(c) => write!(f, "{}", c), + SqlOption::Clustered(c) => write!(f, "{c}"), SqlOption::Ident(ident) => { - write!(f, "{}", ident) + write!(f, "{ident}") } SqlOption::KeyValue { key: name, value } => { - write!(f, "{} = {}", name, value) + write!(f, "{name} = {value}") } SqlOption::Partition { column_name, @@ -7905,7 +7903,7 @@ impl fmt::Display for SqlOption { SqlOption::NamedParenthesizedList(value) => { write!(f, "{} = ", value.key)?; if let Some(key) = &value.name { - write!(f, "{}", key)?; + write!(f, "{key}")?; } if !value.values.is_empty() { write!(f, "({})", display_comma_separated(&value.values))? @@ -7962,7 +7960,7 @@ impl fmt::Display for AttachDuckDBDatabaseOption { AttachDuckDBDatabaseOption::ReadOnly(Some(true)) => write!(f, "READ_ONLY true"), AttachDuckDBDatabaseOption::ReadOnly(Some(false)) => write!(f, "READ_ONLY false"), AttachDuckDBDatabaseOption::ReadOnly(None) => write!(f, "READ_ONLY"), - AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {}", t), + AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {t}"), } } } @@ -9485,10 +9483,10 @@ impl fmt::Display for ShowStatementIn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.clause)?; if let Some(parent_type) = &self.parent_type { - write!(f, " {}", parent_type)?; + write!(f, " {parent_type}")?; } if let Some(parent_name) = &self.parent_name { - write!(f, " {}", parent_name)?; + write!(f, " {parent_name}")?; } Ok(()) } @@ -9569,7 +9567,7 @@ impl fmt::Display for TableObject { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::TableName(table_name) => write!(f, "{table_name}"), - Self::TableFunction(func) => write!(f, "FUNCTION {}", func), + Self::TableFunction(func) => write!(f, "FUNCTION {func}"), } } } @@ -9757,7 +9755,7 @@ pub struct ReturnStatement { impl fmt::Display for ReturnStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.value { - Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {}", expr), + Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {expr}"), None => write!(f, "RETURN"), } } diff --git a/src/ast/query.rs b/src/ast/query.rs index 1fb93b6c..c79ec110 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1183,7 +1183,7 @@ impl fmt::Display for TableIndexHints { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {} ", self.hint_type, self.index_type)?; if let Some(for_clause) = &self.for_clause { - write!(f, "FOR {} ", for_clause)?; + write!(f, "FOR {for_clause} ")?; } write!(f, "({})", display_comma_separated(&self.index_names)) } @@ -1459,7 +1459,7 @@ impl fmt::Display for TableSampleQuantity { } write!(f, "{}", self.value)?; if let Some(unit) = &self.unit { - write!(f, " {}", unit)?; + write!(f, " {unit}")?; } if self.parenthesized { write!(f, ")")?; @@ -1552,7 +1552,7 @@ impl fmt::Display for TableSampleBucket { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "BUCKET {} OUT OF {}", self.bucket, self.total)?; if let Some(on) = &self.on { - write!(f, " ON {}", on)?; + write!(f, " ON {on}")?; } Ok(()) } @@ -1561,19 +1561,19 @@ impl fmt::Display for TableSample { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.modifier)?; if let Some(name) = &self.name { - write!(f, " {}", name)?; + write!(f, " {name}")?; } if let Some(quantity) = &self.quantity { - write!(f, " {}", quantity)?; + write!(f, " {quantity}")?; } if let Some(seed) = &self.seed { - write!(f, " {}", seed)?; + write!(f, " {seed}")?; } if let Some(bucket) = &self.bucket { - write!(f, " ({})", bucket)?; + write!(f, " ({bucket})")?; } if let Some(offset) = &self.offset { - write!(f, " OFFSET {}", offset)?; + write!(f, " OFFSET {offset}")?; } Ok(()) } @@ -1651,7 +1651,7 @@ impl fmt::Display for RowsPerMatch { RowsPerMatch::AllRows(mode) => { write!(f, "ALL ROWS PER MATCH")?; if let Some(mode) = mode { - write!(f, " {}", mode)?; + write!(f, " {mode}")?; } Ok(()) } @@ -1777,7 +1777,7 @@ impl fmt::Display for MatchRecognizePattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use MatchRecognizePattern::*; match self { - Symbol(symbol) => write!(f, "{}", symbol), + Symbol(symbol) => write!(f, "{symbol}"), Exclude(symbol) => write!(f, "{{- {symbol} -}}"), Permute(symbols) => write!(f, "PERMUTE({})", display_comma_separated(symbols)), Concat(patterns) => write!(f, "{}", display_separated(patterns, " ")), @@ -2148,7 +2148,7 @@ impl fmt::Display for TableAliasColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; if let Some(ref data_type) = self.data_type { - write!(f, " {}", data_type)?; + write!(f, " {data_type}")?; } Ok(()) } @@ -2398,7 +2398,7 @@ impl fmt::Display for OrderBy { write!(f, " {}", display_comma_separated(exprs))?; } OrderByKind::All(all) => { - write!(f, " ALL{}", all)?; + write!(f, " ALL{all}")?; } } @@ -2429,7 +2429,7 @@ impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", self.expr, self.options)?; if let Some(ref with_fill) = self.with_fill { - write!(f, " {}", with_fill)? + write!(f, " {with_fill}")? } Ok(()) } @@ -2452,13 +2452,13 @@ impl fmt::Display for WithFill { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "WITH FILL")?; if let Some(ref from) = self.from { - write!(f, " FROM {}", from)?; + write!(f, " FROM {from}")?; } if let Some(ref to) = self.to { - write!(f, " TO {}", to)?; + write!(f, " TO {to}")?; } if let Some(ref step) = self.step { - write!(f, " STEP {}", step)?; + write!(f, " STEP {step}")?; } Ok(()) } @@ -2487,7 +2487,7 @@ impl fmt::Display for InterpolateExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.column)?; if let Some(ref expr) = self.expr { - write!(f, " AS {}", expr)?; + write!(f, " AS {expr}")?; } Ok(()) } @@ -2565,7 +2565,7 @@ impl fmt::Display for LimitClause { Ok(()) } LimitClause::OffsetCommaLimit { offset, limit } => { - write!(f, " LIMIT {}, {}", offset, limit) + write!(f, " LIMIT {offset}, {limit}") } } } @@ -2702,12 +2702,12 @@ impl fmt::Display for PipeOperator { write!(f, "DROP {}", display_comma_separated(columns.as_slice())) } PipeOperator::As { alias } => { - write!(f, "AS {}", alias) + write!(f, "AS {alias}") } PipeOperator::Limit { expr, offset } => { - write!(f, "LIMIT {}", expr)?; + write!(f, "LIMIT {expr}")?; if let Some(offset) = offset { - write!(f, " OFFSET {}", offset)?; + write!(f, " OFFSET {offset}")?; } Ok(()) } @@ -2730,14 +2730,14 @@ impl fmt::Display for PipeOperator { } PipeOperator::Where { expr } => { - write!(f, "WHERE {}", expr) + write!(f, "WHERE {expr}") } PipeOperator::OrderBy { exprs } => { write!(f, "ORDER BY {}", display_comma_separated(exprs.as_slice())) } PipeOperator::TableSample { sample } => { - write!(f, "{}", sample) + write!(f, "{sample}") } } } @@ -3016,7 +3016,7 @@ pub enum FormatClause { impl fmt::Display for FormatClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - FormatClause::Identifier(ident) => write!(f, "FORMAT {}", ident), + FormatClause::Identifier(ident) => write!(f, "FORMAT {ident}"), FormatClause::Null => write!(f, "FORMAT NULL"), } } @@ -3078,9 +3078,9 @@ impl fmt::Display for ForClause { without_array_wrapper, } => { write!(f, "FOR JSON ")?; - write!(f, "{}", for_json)?; + write!(f, "{for_json}")?; if let Some(root) = root { - write!(f, ", ROOT('{}')", root)?; + write!(f, ", ROOT('{root}')")?; } if *include_null_values { write!(f, ", INCLUDE_NULL_VALUES")?; @@ -3098,7 +3098,7 @@ impl fmt::Display for ForClause { r#type, } => { write!(f, "FOR XML ")?; - write!(f, "{}", for_xml)?; + write!(f, "{for_xml}")?; if *binary_base64 { write!(f, ", BINARY BASE64")?; } @@ -3106,7 +3106,7 @@ impl fmt::Display for ForClause { write!(f, ", TYPE")?; } if let Some(root) = root { - write!(f, ", ROOT('{}')", root)?; + write!(f, ", ROOT('{root}')")?; } if *elements { write!(f, ", ELEMENTS")?; @@ -3133,7 +3133,7 @@ impl fmt::Display for ForXml { ForXml::Raw(root) => { write!(f, "RAW")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -3142,7 +3142,7 @@ impl fmt::Display for ForXml { ForXml::Path(root) => { write!(f, "PATH")?; if let Some(root) = root { - write!(f, "('{}')", root)?; + write!(f, "('{root}')")?; } Ok(()) } @@ -3205,7 +3205,7 @@ impl fmt::Display for JsonTableColumn { JsonTableColumn::Named(json_table_named_column) => { write!(f, "{json_table_named_column}") } - JsonTableColumn::ForOrdinality(ident) => write!(f, "{} FOR ORDINALITY", ident), + JsonTableColumn::ForOrdinality(ident) => write!(f, "{ident} FOR ORDINALITY"), JsonTableColumn::Nested(json_table_nested_column) => { write!(f, "{json_table_nested_column}") } @@ -3271,10 +3271,10 @@ impl fmt::Display for JsonTableNamedColumn { self.path )?; if let Some(on_empty) = &self.on_empty { - write!(f, " {} ON EMPTY", on_empty)?; + write!(f, " {on_empty} ON EMPTY")?; } if let Some(on_error) = &self.on_error { - write!(f, " {} ON ERROR", on_error)?; + write!(f, " {on_error} ON ERROR")?; } Ok(()) } @@ -3296,7 +3296,7 @@ impl fmt::Display for JsonTableColumnErrorHandling { match self { JsonTableColumnErrorHandling::Null => write!(f, "NULL"), JsonTableColumnErrorHandling::Default(json_string) => { - write!(f, "DEFAULT {}", json_string) + write!(f, "DEFAULT {json_string}") } JsonTableColumnErrorHandling::Error => write!(f, "ERROR"), } @@ -3429,12 +3429,12 @@ impl fmt::Display for XmlTableColumn { default, nullable, } => { - write!(f, " {}", r#type)?; + write!(f, " {type}")?; if let Some(p) = path { - write!(f, " PATH {}", p)?; + write!(f, " PATH {p}")?; } if let Some(d) = default { - write!(f, " DEFAULT {}", d)?; + write!(f, " DEFAULT {d}")?; } if !*nullable { write!(f, " NOT NULL")?; @@ -3465,7 +3465,7 @@ impl fmt::Display for XmlPassingArgument { } write!(f, "{}", self.expr)?; if let Some(alias) = &self.alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } Ok(()) } diff --git a/src/ast/value.rs b/src/ast/value.rs index 98616407..90dbccbf 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -551,16 +551,16 @@ impl fmt::Display for EscapeUnicodeStringLiteral<'_> { write!(f, r#"\\"#)?; } x if x.is_ascii() => { - write!(f, "{}", c)?; + write!(f, "{c}")?; } _ => { let codepoint = c as u32; // if the character fits in 32 bits, we can use the \XXXX format // otherwise, we need to use the \+XXXXXX format if codepoint <= 0xFFFF { - write!(f, "\\{:04X}", codepoint)?; + write!(f, "\\{codepoint:04X}")?; } else { - write!(f, "\\+{:06X}", codepoint)?; + write!(f, "\\+{codepoint:06X}")?; } } } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index ab4f73aa..8e0a3139 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -926,10 +926,10 @@ mod tests { #[test] fn overflow() { let cond = (0..1000) - .map(|n| format!("X = {}", n)) + .map(|n| format!("X = {n}")) .collect::>() .join(" OR "); - let sql = format!("SELECT x where {0}", cond); + let sql = format!("SELECT x where {cond}"); let dialect = GenericDialect {}; let tokens = Tokenizer::new(&dialect, sql.as_str()).tokenize().unwrap(); diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bc92948d..c79b4517 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -587,7 +587,7 @@ pub trait Dialect: Debug + Any { } let token = parser.peek_token(); - debug!("get_next_precedence_full() {:?}", token); + debug!("get_next_precedence_full() {token:?}"); match token.token { Token::Word(w) if w.keyword == Keyword::OR => Ok(p!(Or)), Token::Word(w) if w.keyword == Keyword::AND => Ok(p!(And)), diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9b08b8f3..a91ab598 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -104,7 +104,7 @@ impl Dialect for PostgreSqlDialect { fn get_next_precedence(&self, parser: &Parser) -> Option> { let token = parser.peek_token(); - debug!("get_next_precedence() {:?}", token); + debug!("get_next_precedence() {token:?}"); // we only return some custom value here when the behaviour (not merely the numeric value) differs // from the default implementation diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e09b7e33..f1b09afb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -436,7 +436,7 @@ impl<'a> Parser<'a> { /// /// See example on [`Parser::new()`] for an example pub fn try_with_sql(self, sql: &str) -> Result { - debug!("Parsing sql '{}'...", sql); + debug!("Parsing sql '{sql}'..."); let tokens = Tokenizer::new(self.dialect, sql) .with_unescape(self.options.unescape) .tokenize_with_location()?; @@ -1226,10 +1226,10 @@ impl<'a> Parser<'a> { expr = self.parse_compound_expr(expr, vec![])?; - debug!("prefix: {:?}", expr); + debug!("prefix: {expr:?}"); loop { let next_precedence = self.get_next_precedence()?; - debug!("next precedence: {:?}", next_precedence); + debug!("next precedence: {next_precedence:?}"); if precedence >= next_precedence { break; @@ -1631,8 +1631,7 @@ impl<'a> Parser<'a> { Token::QuestionPipe => UnaryOperator::QuestionPipe, _ => { return Err(ParserError::ParserError(format!( - "Unexpected token in unary operator parsing: {:?}", - tok + "Unexpected token in unary operator parsing: {tok:?}" ))) } }; @@ -13655,7 +13654,7 @@ impl<'a> Parser<'a> { let ident = self.parse_identifier()?; if let GranteeName::ObjectName(namespace) = name { name = GranteeName::ObjectName(ObjectName::from(vec![Ident::new( - format!("{}:{}", namespace, ident), + format!("{namespace}:{ident}"), )])); }; } @@ -14625,7 +14624,7 @@ impl<'a> Parser<'a> { self.dialect .get_reserved_keywords_for_select_item_operator(), ) - .map(|keyword| Ident::new(format!("{:?}", keyword))); + .map(|keyword| Ident::new(format!("{keyword:?}"))); match self.parse_wildcard_expr()? { Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard( diff --git a/src/test_utils.rs b/src/test_utils.rs index 1af9f454..c7965c3f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -270,7 +270,7 @@ impl TestedDialects { tokenizer = tokenizer.with_unescape(options.unescape); } let tokens = tokenizer.tokenize().unwrap(); - assert_eq!(expected, tokens, "Tokenized differently for {:?}", dialect); + assert_eq!(expected, tokens, "Tokenized differently for {dialect:?}"); }); } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index afe1e35c..8382a534 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1751,7 +1751,7 @@ impl<'a> Tokenizer<'a> { (None, Some(tok)) => Ok(Some(tok)), (None, None) => self.tokenizer_error( chars.location(), - format!("Expected a valid binary operator after '{}'", prefix), + format!("Expected a valid binary operator after '{prefix}'"), ), } } @@ -1809,7 +1809,7 @@ impl<'a> Tokenizer<'a> { chars.next(); let mut temp = String::new(); - let end_delimiter = format!("${}$", value); + let end_delimiter = format!("${value}$"); loop { match chars.next() { @@ -2402,13 +2402,13 @@ fn take_char_from_hex_digits( location: chars.location(), })?; let digit = next_char.to_digit(16).ok_or_else(|| TokenizerError { - message: format!("Invalid hex digit in escaped unicode string: {}", next_char), + message: format!("Invalid hex digit in escaped unicode string: {next_char}"), location: chars.location(), })?; result = result * 16 + digit; } char::from_u32(result).ok_or_else(|| TokenizerError { - message: format!("Invalid unicode character: {:x}", result), + message: format!("Invalid unicode character: {result:x}"), location: chars.location(), }) } @@ -3504,7 +3504,7 @@ mod tests { } fn check_unescape(s: &str, expected: Option<&str>) { - let s = format!("'{}'", s); + let s = format!("'{s}'"); let mut state = State { peekable: s.chars().peekable(), line: 0, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index ed5d7ad2..0a60c9c4 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1345,7 +1345,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - clickhouse().verified_stmt(&format!("USE {}", object_name)), + clickhouse().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -1353,7 +1353,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - clickhouse().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + clickhouse().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -1367,7 +1367,7 @@ fn parse_use() { fn test_query_with_format_clause() { let format_options = vec!["TabSeparated", "JSONCompact", "NULL"]; for format in &format_options { - let sql = format!("SELECT * FROM t FORMAT {}", format); + let sql = format!("SELECT * FROM t FORMAT {format}"); match clickhouse_and_generic().verified_stmt(&sql) { Statement::Query(query) => { if *format == "NULL" { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 93cc89e5..6c4ed08c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3563,7 +3563,7 @@ fn test_double_value() { for (input, expected) in test_cases { for (i, expr) in input.iter().enumerate() { if let Statement::Query(query) = - dialects.one_statement_parses_to(&format!("SELECT {}", expr), "") + dialects.one_statement_parses_to(&format!("SELECT {expr}"), "") { if let SetExpr::Select(select) = *query.body { assert_eq!(expected[i], select.projection[0]); @@ -4023,13 +4023,13 @@ fn parse_create_table_column_constraint_characteristics() { syntax }; - let sql = format!("CREATE TABLE t (a int UNIQUE {})", syntax); + let sql = format!("CREATE TABLE t (a int UNIQUE {syntax})"); let expected_clause = if syntax.is_empty() { String::new() } else { format!(" {syntax}") }; - let expected = format!("CREATE TABLE t (a INT UNIQUE{})", expected_clause); + let expected = format!("CREATE TABLE t (a INT UNIQUE{expected_clause})"); let ast = one_statement_parses_to(&sql, &expected); let expected_value = if deferrable.is_some() || initially.is_some() || enforced.is_some() { @@ -7499,7 +7499,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 1)"); assert!(matches!(*query.body, SetExpr::Update(_))); } - other => panic!("Expected: UPDATE, got: {:?}", other), + other => panic!("Expected: UPDATE, got: {other:?}"), } match verified_stmt("WITH t (x) AS (SELECT 9) DELETE FROM q WHERE id IN (SELECT x FROM t)") { @@ -7507,7 +7507,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH t (x) AS (SELECT 9)"); assert!(matches!(*query.body, SetExpr::Delete(_))); } - other => panic!("Expected: DELETE, got: {:?}", other), + other => panic!("Expected: DELETE, got: {other:?}"), } match verified_stmt("WITH x AS (SELECT 42) INSERT INTO t SELECT foo FROM x") { @@ -7515,7 +7515,7 @@ fn parse_cte_in_data_modification_statements() { assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 42)"); assert!(matches!(*query.body, SetExpr::Insert(_))); } - other => panic!("Expected: INSERT, got: {:?}", other), + other => panic!("Expected: INSERT, got: {other:?}"), } } @@ -10043,7 +10043,7 @@ fn parse_offset_and_limit() { #[test] fn parse_time_functions() { fn test_time_function(func_name: &'static str) { - let sql = format!("SELECT {}()", func_name); + let sql = format!("SELECT {func_name}()"); let select = verified_only_select(&sql); let select_localtime_func_call_ast = Function { name: ObjectName::from(vec![Ident::new(func_name)]), @@ -10065,7 +10065,7 @@ fn parse_time_functions() { ); // Validating Parenthesis - let sql_without_parens = format!("SELECT {}", func_name); + let sql_without_parens = format!("SELECT {func_name}"); let mut ast_without_parens = select_localtime_func_call_ast; ast_without_parens.args = FunctionArguments::None; assert_eq!( @@ -14306,7 +14306,7 @@ fn overflow() { let expr = std::iter::repeat_n("1", 1000) .collect::>() .join(" + "); - let sql = format!("SELECT {}", expr); + let sql = format!("SELECT {expr}"); let mut statements = Parser::parse_sql(&GenericDialect {}, sql.as_str()).unwrap(); let statement = statements.pop().unwrap(); @@ -14606,7 +14606,7 @@ fn test_conditional_statement_span() { else_block.unwrap().span() ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 99b7eecd..baf279fa 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -213,7 +213,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - databricks().verified_stmt(&format!("USE {}", object_name)), + databricks().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -221,7 +221,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - databricks().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + databricks().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -233,21 +233,21 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with keyword and different type of quotes assert_eq!( - databricks().verified_stmt(&format!("USE CATALOG {0}my_catalog{0}", quote)), + databricks().verified_stmt(&format!("USE CATALOG {quote}my_catalog{quote}")), Statement::Use(Use::Catalog(ObjectName::from(vec![Ident::with_quote( quote, "my_catalog".to_string(), )]))) ); assert_eq!( - databricks().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + databricks().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")), Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote( quote, "my_database".to_string(), )]))) ); assert_eq!( - databricks().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + databricks().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")), Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( quote, "my_schema".to_string(), @@ -357,6 +357,6 @@ fn data_type_timestamp_ntz() { }] ); } - s => panic!("Unexpected statement: {:?}", s), + s => panic!("Unexpected statement: {s:?}"), } } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index f503ed55..44cb22ce 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -368,7 +368,7 @@ fn test_duckdb_specific_int_types() { ("HUGEINT", DataType::HugeInt), ]; for (dtype_string, data_type) in duckdb_dtypes { - let sql = format!("SELECT 123::{}", dtype_string); + let sql = format!("SELECT 123::{dtype_string}"); let select = duckdb().verified_only_select(&sql); assert_eq!( &Expr::Cast { @@ -792,7 +792,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {}", object_name)), + duckdb().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -800,7 +800,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), + duckdb().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -812,7 +812,9 @@ fn parse_use() { for "e in "e_styles { // Test double identifier with different type of quotes assert_eq!( - duckdb().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), + duckdb().verified_stmt(&format!( + "USE {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index fd52b773..56a72ec8 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -524,7 +524,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - hive().verified_stmt(&format!("USE {}", object_name)), + hive().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -532,7 +532,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - hive().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + hive().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9ec28f42..ebbec25f 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1673,7 +1673,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - ms().verified_stmt(&format!("USE {}", object_name)), + ms().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -1681,7 +1681,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + ms().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -2187,7 +2187,7 @@ fn parse_mssql_if_else() { "IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;" ); } - _ => panic!("Unexpected statements: {:?}", stmts), + _ => panic!("Unexpected statements: {stmts:?}"), } } @@ -2237,7 +2237,7 @@ fn test_mssql_if_statements_span() { Span::new(Location::new(1, 21), Location::new(1, 36)) ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Blocks @@ -2258,7 +2258,7 @@ fn test_mssql_if_statements_span() { Span::new(Location::new(1, 32), Location::new(1, 57)) ); } - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b11d76dd..d2feee03 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -593,7 +593,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - mysql_and_generic().verified_stmt(&format!("USE {}", object_name)), + mysql_and_generic().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -601,8 +601,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - mysql_and_generic() - .verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + mysql_and_generic().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -2263,11 +2262,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { assert_eq!(&[Ident::new("t"), Ident::new("15to29")], &parts[..]); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 2: Qualified column name that starts with digits and on its own represents a number. @@ -2277,11 +2276,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => { assert_eq!(&[Ident::new("t"), Ident::new("15e29")], &parts[..]); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 3: Unqualified, the same token is parsed as a number. @@ -2295,11 +2294,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { value, .. }))) => { assert_eq!(&number("15e29"), value); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 4: Quoted simple identifier. @@ -2309,11 +2308,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { Some(SelectItem::UnnamedExpr(Expr::Identifier(name))) => { assert_eq!(&Ident::with_quote('`', "15e29"), name); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 5: Quoted compound identifier. @@ -2326,11 +2325,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 6: Multi-level compound identifiers. @@ -2347,11 +2346,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } // Case 7: Multi-level compound quoted identifiers. @@ -2368,11 +2367,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() { &parts[..] ); } - proj => panic!("Unexpected projection: {:?}", proj), + proj => panic!("Unexpected projection: {proj:?}"), }, - body => panic!("Unexpected statement body: {:?}", body), + body => panic!("Unexpected statement body: {body:?}"), }, - stmt => panic!("Unexpected statement: {:?}", stmt), + stmt => panic!("Unexpected statement: {stmt:?}"), } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b6605cf1..7b0a8c5d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2535,12 +2535,12 @@ fn parse_create_indices_with_operator_classes() { for expected_operator_class in &operator_classes { let single_column_sql_statement = format!( "CREATE INDEX the_index_name ON users USING {expected_index_type} (concat_users_name(first_name, last_name){})", - expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + expected_operator_class.as_ref().map(|oc| format!(" {oc}")) .unwrap_or_default() ); let multi_column_sql_statement = format!( "CREATE INDEX the_index_name ON users USING {expected_index_type} (column_name,concat_users_name(first_name, last_name){})", - expected_operator_class.as_ref().map(|oc| format!(" {}", oc)) + expected_operator_class.as_ref().map(|oc| format!(" {oc}")) .unwrap_or_default() ); @@ -3273,7 +3273,7 @@ fn test_fn_arg_with_value_operator() { assert!(matches!( &args[..], &[FunctionArg::ExprNamed { operator: FunctionArgOperator::Value, .. }] - ), "Invalid function argument: {:?}", args); + ), "Invalid function argument: {args:?}"); } other => panic!("Expected: JSON_OBJECT('name' VALUE 'value') to be parsed as a function, but got {other:?}"), } @@ -5679,7 +5679,7 @@ fn parse_drop_trigger() { "DROP TRIGGER{} check_update ON table_name{}", if if_exists { " IF EXISTS" } else { "" }, option - .map(|o| format!(" {}", o)) + .map(|o| format!(" {o}")) .unwrap_or_else(|| "".to_string()) ); assert_eq!( @@ -5773,8 +5773,7 @@ fn parse_trigger_related_functions() { // Now we parse the statements and check if they are parsed correctly. let mut statements = pg() .parse_sql_statements(&format!( - "{}{}{}{}", - sql_table_creation, sql_create_function, sql_create_trigger, sql_drop_trigger + "{sql_table_creation}{sql_create_function}{sql_create_trigger}{sql_drop_trigger}" )) .unwrap(); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 8b3988d9..3ffd3339 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2510,10 +2510,7 @@ fn test_snowflake_stage_object_names_into_location() { .zip(allowed_object_names.iter_mut()) { let (formatted_name, object_name) = it; - let sql = format!( - "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", - formatted_name - ); + let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'"); match snowflake().verified_stmt(&sql) { Statement::CopyIntoSnowflake { into, .. } => { assert_eq!(into.0, object_name.0) @@ -2536,10 +2533,7 @@ fn test_snowflake_stage_object_names_into_table() { .zip(allowed_object_names.iter_mut()) { let (formatted_name, object_name) = it; - let sql = format!( - "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", - formatted_name - ); + let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'"); match snowflake().verified_stmt(&sql) { Statement::CopyIntoSnowflake { into, .. } => { assert_eq!(into.0, object_name.0) @@ -3020,7 +3014,7 @@ fn parse_use() { for object_name in &valid_object_names { // Test single identifier without quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}", object_name)), + snowflake().verified_stmt(&format!("USE {object_name}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) @@ -3028,7 +3022,7 @@ fn parse_use() { for "e in "e_styles { // Test single identifier with different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + snowflake().verified_stmt(&format!("USE {quote}{object_name}{quote}")), Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), @@ -3040,7 +3034,9 @@ fn parse_use() { for "e in "e_styles { // Test double identifier with different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!( + "USE {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") @@ -3059,35 +3055,37 @@ fn parse_use() { for "e in "e_styles { // Test single and double identifier with keyword and different type of quotes assert_eq!( - snowflake().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + snowflake().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")), Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote( quote, "my_database".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")), Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( quote, "my_schema".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE SCHEMA {0}CATALOG{0}.{0}my_schema{0}", quote)), + snowflake().verified_stmt(&format!( + "USE SCHEMA {quote}CATALOG{quote}.{quote}my_schema{quote}" + )), Statement::Use(Use::Schema(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") ]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE ROLE {0}my_role{0}", quote)), + snowflake().verified_stmt(&format!("USE ROLE {quote}my_role{quote}")), Statement::Use(Use::Role(ObjectName::from(vec![Ident::with_quote( quote, "my_role".to_string(), )]))) ); assert_eq!( - snowflake().verified_stmt(&format!("USE WAREHOUSE {0}my_wh{0}", quote)), + snowflake().verified_stmt(&format!("USE WAREHOUSE {quote}my_wh{quote}")), Statement::Use(Use::Warehouse(ObjectName::from(vec![Ident::with_quote( quote, "my_wh".to_string(), @@ -3629,7 +3627,7 @@ fn test_alter_session_followed_by_statement() { .unwrap(); match stmts[..] { [Statement::AlterSession { .. }, Statement::Query { .. }] => {} - _ => panic!("Unexpected statements: {:?}", stmts), + _ => panic!("Unexpected statements: {stmts:?}"), } } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index b759065f..06496f0c 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -324,7 +324,7 @@ fn parse_create_table_on_conflict_col() { Keyword::IGNORE, Keyword::REPLACE, ] { - let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {:?})", keyword); + let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {keyword:?})"); match sqlite_and_generic().verified_stmt(&sql) { Statement::CreateTable(CreateTable { columns, .. }) => { assert_eq!( @@ -410,7 +410,7 @@ fn parse_window_function_with_filter() { "count", "user_defined_function", ] { - let sql = format!("SELECT {}(x) FILTER (WHERE y) OVER () FROM t", func_name); + let sql = format!("SELECT {func_name}(x) FILTER (WHERE y) OVER () FROM t"); let select = sqlite().verified_only_select(&sql); assert_eq!(select.to_string(), sql); assert_eq!( @@ -444,7 +444,7 @@ fn parse_window_function_with_filter() { 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)); + assert_eq!(sql, format!("{verified_stmt}")); match verified_stmt { Statement::AttachDatabase { schema_name, From 6c38cdcadb45ff5b343ea49bad23fdfeebc92cf5 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:21:38 +0200 Subject: [PATCH 14/39] Snowflake: Add support for future grants (#1906) --- src/ast/mod.rs | 27 +++++++++++++++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 27 +++++++++++++++++++++++++++ tests/sqlparser_common.rs | 4 +++- 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0f682062..19966d21 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6920,6 +6920,12 @@ pub enum GrantObjects { AllSequencesInSchema { schemas: Vec }, /// Grant privileges on `ALL TABLES IN SCHEMA [, ...]` AllTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE SCHEMAS IN DATABASE [, ...]` + FutureSchemasInDatabase { databases: Vec }, + /// Grant privileges on `FUTURE TABLES IN SCHEMA [, ...]` + FutureTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE VIEWS IN SCHEMA [, ...]` + FutureViewsInSchema { schemas: Vec }, /// Grant privileges on specific databases Databases(Vec), /// Grant privileges on specific schemas @@ -6988,6 +6994,27 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::FutureSchemasInDatabase { databases } => { + write!( + f, + "FUTURE SCHEMAS IN DATABASE {}", + display_comma_separated(databases) + ) + } + GrantObjects::FutureTablesInSchema { schemas } => { + write!( + f, + "FUTURE TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureViewsInSchema { schemas } => { + write!( + f, + "FUTURE VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::ResourceMonitors(objects) => { write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) } diff --git a/src/keywords.rs b/src/keywords.rs index f56178c1..a8bbca3d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -395,6 +395,7 @@ define_keywords!( FUNCTION, FUNCTIONS, FUSION, + FUTURE, GENERAL, GENERATE, GENERATED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f1b09afb..68d89a1e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13691,6 +13691,33 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllTablesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::SCHEMAS, + Keyword::IN, + Keyword::DATABASE, + ]) { + Some(GrantObjects::FutureSchemasInDatabase { + databases: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::ALL, Keyword::SEQUENCES, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6c4ed08c..17b46e6f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9386,9 +9386,11 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); verified_stmt("GRANT EXEC ON my_sp TO runner"); verified_stmt("GRANT UPDATE ON my_table TO updater_role AS dbo"); - all_dialects_where(|d| d.identifier_quote_style("none") == Some('[')) .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); + verified_stmt("GRANT SELECT ON FUTURE SCHEMAS IN DATABASE db1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); } #[test] From 50c605a47138b1602575731c196d04b0d280ad3d Mon Sep 17 00:00:00 2001 From: Sergey Olontsev Date: Sat, 28 Jun 2025 07:13:11 +0100 Subject: [PATCH 15/39] Support for Map values in ClickHouse settings (#1896) Co-authored-by: Ifeanyi Ubah --- src/ast/query.rs | 2 +- src/ast/value.rs | 1 - src/parser/mod.rs | 17 ++--- src/test_utils.rs | 5 ++ tests/sqlparser_clickhouse.rs | 130 +++++++++++++++++++++++++--------- 5 files changed, 112 insertions(+), 43 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index c79ec110..99cd2ef2 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1047,7 +1047,7 @@ impl fmt::Display for ConnectBy { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Setting { pub key: Ident, - pub value: Value, + pub value: Expr, } impl fmt::Display for Setting { diff --git a/src/ast/value.rs b/src/ast/value.rs index 90dbccbf..fdfa6a67 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -116,7 +116,6 @@ impl From for Value { derive(Visit, VisitMut), visit(with = "visit_value") )] - pub enum Value { /// Numeric literal #[cfg(not(feature = "bigdecimal"))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 68d89a1e..adf50a8f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2770,7 +2770,7 @@ impl<'a> Parser<'a> { if self.dialect.supports_dictionary_syntax() { self.prev_token(); // Put back the '{' - return self.parse_duckdb_struct_literal(); + return self.parse_dictionary(); } self.expected("an expression", token) @@ -3139,7 +3139,7 @@ impl<'a> Parser<'a> { Ok(fields) } - /// DuckDB specific: Parse a duckdb [dictionary] + /// DuckDB and ClickHouse specific: Parse a duckdb [dictionary] or a clickhouse [map] setting /// /// Syntax: /// @@ -3148,18 +3148,18 @@ impl<'a> Parser<'a> { /// ``` /// /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs - fn parse_duckdb_struct_literal(&mut self) -> Result { + /// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters + fn parse_dictionary(&mut self) -> Result { self.expect_token(&Token::LBrace)?; - let fields = - self.parse_comma_separated0(Self::parse_duckdb_dictionary_field, Token::RBrace)?; + let fields = self.parse_comma_separated0(Self::parse_dictionary_field, Token::RBrace)?; self.expect_token(&Token::RBrace)?; Ok(Expr::Dictionary(fields)) } - /// Parse a field for a duckdb [dictionary] + /// Parse a field for a duckdb [dictionary] or a clickhouse [map] setting /// /// Syntax /// @@ -3168,7 +3168,8 @@ impl<'a> Parser<'a> { /// ``` /// /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs - fn parse_duckdb_dictionary_field(&mut self) -> Result { + /// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters + fn parse_dictionary_field(&mut self) -> Result { let key = self.parse_identifier()?; self.expect_token(&Token::Colon)?; @@ -11216,7 +11217,7 @@ impl<'a> Parser<'a> { let key_values = self.parse_comma_separated(|p| { let key = p.parse_identifier()?; p.expect_token(&Token::Eq)?; - let value = p.parse_value()?.value; + let value = p.parse_expr()?; Ok(Setting { key, value }) })?; Some(key_values) diff --git a/src/test_utils.rs b/src/test_utils.rs index c7965c3f..db7b3dd6 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -366,6 +366,11 @@ pub fn number(n: &str) -> Value { Value::Number(n.parse().unwrap(), false) } +/// Creates a [Value::SingleQuotedString] +pub fn single_quoted_string(s: impl Into) -> Value { + Value::SingleQuotedString(s.into()) +} + pub fn table_alias(name: impl Into) -> Option { Some(TableAlias { name: Ident::new(name), diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0a60c9c4..0288c6d2 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -28,7 +28,7 @@ use test_utils::*; use sqlparser::ast::Expr::{BinaryOp, Identifier}; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Table; -use sqlparser::ast::Value::Number; +use sqlparser::ast::Value::Boolean; use sqlparser::ast::*; use sqlparser::dialect::ClickHouseDialect; use sqlparser::dialect::GenericDialect; @@ -965,38 +965,103 @@ fn parse_limit_by() { #[test] fn parse_settings_in_query() { - match clickhouse_and_generic() - .verified_stmt(r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#) - { - Statement::Query(query) => { - assert_eq!( - query.settings, - Some(vec![ - Setting { - key: Ident::new("max_threads"), - value: Number("1".parse().unwrap(), false) - }, - Setting { - key: Ident::new("max_block_size"), - value: Number("10000".parse().unwrap(), false) - }, - ]) - ); + fn check_settings(sql: &str, expected: Vec) { + match clickhouse_and_generic().verified_stmt(sql) { + Statement::Query(q) => { + assert_eq!(q.settings, Some(expected)); + } + _ => unreachable!(), } - _ => unreachable!(), + } + + for (sql, expected_settings) in [ + ( + r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#, + vec![ + Setting { + key: Ident::new("max_threads"), + value: Expr::value(number("1")), + }, + Setting { + key: Ident::new("max_block_size"), + value: Expr::value(number("10000")), + }, + ], + ), + ( + r#"SELECT * FROM t SETTINGS additional_table_filters = {'table_1': 'x != 2'}"#, + vec![Setting { + key: Ident::new("additional_table_filters"), + value: Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "table_1"), + value: Expr::value(single_quoted_string("x != 2")).into(), + }]), + }], + ), + ( + r#"SELECT * FROM t SETTINGS additional_result_filter = 'x != 2', query_plan_optimize_lazy_materialization = false"#, + vec![ + Setting { + key: Ident::new("additional_result_filter"), + value: Expr::value(single_quoted_string("x != 2")), + }, + Setting { + key: Ident::new("query_plan_optimize_lazy_materialization"), + value: Expr::value(Boolean(false)), + }, + ], + ), + ] { + check_settings(sql, expected_settings); } let invalid_cases = vec![ - "SELECT * FROM t SETTINGS a", - "SELECT * FROM t SETTINGS a=", - "SELECT * FROM t SETTINGS a=1, b", - "SELECT * FROM t SETTINGS a=1, b=", - "SELECT * FROM t SETTINGS a=1, b=c", + ("SELECT * FROM t SETTINGS a", "Expected: =, found: EOF"), + ( + "SELECT * FROM t SETTINGS a=", + "Expected: an expression, found: EOF", + ), + ("SELECT * FROM t SETTINGS a=1, b", "Expected: =, found: EOF"), + ( + "SELECT * FROM t SETTINGS a=1, b=", + "Expected: an expression, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {", + "Expected: identifier, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b'", + "Expected: :, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': ", + "Expected: an expression, found: EOF", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c',}", + "Expected: identifier, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c', 'd'}", + "Expected: :, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {'b': 'c', 'd': }", + "Expected: an expression, found: }", + ), + ( + "SELECT * FROM t SETTINGS a = {ANY(b)}", + "Expected: :, found: (", + ), ]; - for sql in invalid_cases { - clickhouse_and_generic() - .parse_sql_statements(sql) - .expect_err("Expected: SETTINGS key = value, found: "); + for (sql, error_msg) in invalid_cases { + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), + ParserError(error_msg.to_string()) + ); } } #[test] @@ -1550,11 +1615,11 @@ fn parse_select_table_function_settings() { settings: Some(vec![ Setting { key: "s0".into(), - value: Value::Number("3".parse().unwrap(), false), + value: Expr::value(number("3")), }, Setting { key: "s1".into(), - value: Value::SingleQuotedString("s".into()), + value: Expr::value(single_quoted_string("s")), }, ]), }, @@ -1575,11 +1640,11 @@ fn parse_select_table_function_settings() { settings: Some(vec![ Setting { key: "s0".into(), - value: Value::Number("3".parse().unwrap(), false), + value: Expr::value(number("3")), }, Setting { key: "s1".into(), - value: Value::SingleQuotedString("s".into()), + value: Expr::value(single_quoted_string("s")), }, ]), }, @@ -1589,7 +1654,6 @@ fn parse_select_table_function_settings() { "SELECT * FROM t(SETTINGS a=)", "SELECT * FROM t(SETTINGS a=1, b)", "SELECT * FROM t(SETTINGS a=1, b=)", - "SELECT * FROM t(SETTINGS a=1, b=c)", ]; for sql in invalid_cases { clickhouse_and_generic() From 3bc94234dfcf73d091c592edd1dab3fdd075ab82 Mon Sep 17 00:00:00 2001 From: Dima <111751109+Dimchikkk@users.noreply.github.com> Date: Sat, 28 Jun 2025 19:24:25 +0100 Subject: [PATCH 16/39] Fix join precedence for non-snowflake queries (#1905) --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 28 ++++++++++++++++++++++++++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 6 +++++- tests/sqlparser_common.rs | 23 +++++++++++++++++++++++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8f57e487..0b882247 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -52,6 +52,10 @@ impl Dialect for GenericDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c79b4517..028aa58a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -278,6 +278,34 @@ pub trait Dialect: Debug + Any { false } + /// Indicates whether the dialect supports left-associative join parsing + /// by default when parentheses are omitted in nested joins. + /// + /// Most dialects (like MySQL or Postgres) assume **left-associative** precedence, + /// so a query like: + /// + /// ```sql + /// SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON ... + /// ``` + /// is interpreted as: + /// ```sql + /// ((t1 NATURAL JOIN t5) INNER JOIN t0 ON ...) + /// ``` + /// and internally represented as a **flat list** of joins. + /// + /// In contrast, some dialects (e.g. **Snowflake**) assume **right-associative** + /// precedence and interpret the same query as: + /// ```sql + /// (t1 NATURAL JOIN (t5 INNER JOIN t0 ON ...)) + /// ``` + /// which results in a **nested join** structure in the AST. + /// + /// If this method returns `false`, the parser must build nested join trees + /// even in the absence of parentheses to reflect the correct associativity + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + /// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN. fn supports_outer_join_operator(&self) -> bool { false diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 5ebb7e37..ba28a8ec 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -283,6 +283,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + false + } + fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { // Unreserve some keywords that Snowflake accepts as identifiers // See: https://docs.snowflake.com/en/sql-reference/reserved-keywords diff --git a/src/parser/mod.rs b/src/parser/mod.rs index adf50a8f..a079488f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12495,7 +12495,11 @@ impl<'a> Parser<'a> { }; let mut relation = self.parse_table_factor()?; - if self.peek_parens_less_nested_join() { + if !self + .dialect + .supports_left_associative_joins_without_parens() + && self.peek_parens_less_nested_join() + { let joins = self.parse_joins()?; relation = TableFactor::NestedJoin { table_with_joins: Box::new(TableWithJoins { relation, joins }), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 17b46e6f..f6f51459 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15359,6 +15359,29 @@ fn check_enforced() { ); } +#[test] +fn join_precedence() { + all_dialects_except(|d| !d.supports_left_associative_joins_without_parens()) + .verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string without parentheses + "SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0", + ); + all_dialects_except(|d| d.supports_left_associative_joins_without_parens()).verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string with parentheses + "SELECT * FROM t1 NATURAL JOIN (t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0) WHERE t0.v1 = t1.v0", + ); +} + #[test] fn parse_create_procedure_with_language() { let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#; From abd80f9ecbf11c9d4b533a59010f079cd9df5b6c Mon Sep 17 00:00:00 2001 From: Simon Vandel Sillesen Date: Mon, 30 Jun 2025 17:51:55 +0200 Subject: [PATCH 17/39] Support remaining pipe operators (#1879) --- src/ast/query.rs | 153 ++++++++++++++ src/parser/mod.rs | 170 ++++++++++++++++ tests/sqlparser_common.rs | 416 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 739 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 99cd2ef2..febf1fc6 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2684,6 +2684,79 @@ pub enum PipeOperator { /// Syntax: `|> TABLESAMPLE SYSTEM (10 PERCENT) /// See more at TableSample { sample: Box }, + /// Renames columns in the input table. + /// + /// Syntax: `|> RENAME old_name AS new_name, ...` + /// + /// See more at + Rename { mappings: Vec }, + /// Combines the input table with one or more tables using UNION. + /// + /// Syntax: `|> UNION [ALL|DISTINCT] (), (), ...` + /// + /// See more at + Union { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Returns only the rows that are present in both the input table and the specified tables. + /// + /// Syntax: `|> INTERSECT [DISTINCT] (), (), ...` + /// + /// See more at + Intersect { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Returns only the rows that are present in the input table but not in the specified tables. + /// + /// Syntax: `|> EXCEPT DISTINCT (), (), ...` + /// + /// See more at + Except { + set_quantifier: SetQuantifier, + queries: Vec, + }, + /// Calls a table function or procedure that returns a table. + /// + /// Syntax: `|> CALL function_name(args) [AS alias]` + /// + /// See more at + Call { + function: Function, + alias: Option, + }, + /// Pivots data from rows to columns. + /// + /// Syntax: `|> PIVOT(aggregate_function(column) FOR pivot_column IN (value1, value2, ...)) [AS alias]` + /// + /// See more at + Pivot { + aggregate_functions: Vec, + value_column: Vec, + value_source: PivotValueSource, + alias: Option, + }, + /// The `UNPIVOT` pipe operator transforms columns into rows. + /// + /// Syntax: + /// ```sql + /// |> UNPIVOT(value_column FOR name_column IN (column1, column2, ...)) [alias] + /// ``` + /// + /// See more at + Unpivot { + value_column: Ident, + name_column: Ident, + unpivot_columns: Vec, + alias: Option, + }, + /// Joins the input table with another table. + /// + /// Syntax: `|> [JOIN_TYPE] JOIN [alias] ON ` or `|> [JOIN_TYPE] JOIN
[alias] USING ()` + /// + /// See more at + Join(Join), } impl fmt::Display for PipeOperator { @@ -2739,10 +2812,90 @@ impl fmt::Display for PipeOperator { PipeOperator::TableSample { sample } => { write!(f, "{sample}") } + PipeOperator::Rename { mappings } => { + write!(f, "RENAME {}", display_comma_separated(mappings)) + } + PipeOperator::Union { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "UNION", set_quantifier, queries), + PipeOperator::Intersect { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "INTERSECT", set_quantifier, queries), + PipeOperator::Except { + set_quantifier, + queries, + } => Self::fmt_set_operation(f, "EXCEPT", set_quantifier, queries), + PipeOperator::Call { function, alias } => { + write!(f, "CALL {function}")?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Pivot { + aggregate_functions, + value_column, + value_source, + alias, + } => { + write!( + f, + "PIVOT({} FOR {} IN ({}))", + display_comma_separated(aggregate_functions), + Expr::CompoundIdentifier(value_column.to_vec()), + value_source + )?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Unpivot { + value_column, + name_column, + unpivot_columns, + alias, + } => { + write!( + f, + "UNPIVOT({} FOR {} IN ({}))", + value_column, + name_column, + display_comma_separated(unpivot_columns) + )?; + Self::fmt_optional_alias(f, alias) + } + PipeOperator::Join(join) => write!(f, "{join}"), } } } +impl PipeOperator { + /// Helper function to format optional alias for pipe operators + fn fmt_optional_alias(f: &mut fmt::Formatter<'_>, alias: &Option) -> fmt::Result { + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + Ok(()) + } + + /// Helper function to format set operations (UNION, INTERSECT, EXCEPT) with queries + fn fmt_set_operation( + f: &mut fmt::Formatter<'_>, + operation: &str, + set_quantifier: &SetQuantifier, + queries: &[Query], + ) -> fmt::Result { + write!(f, "{operation}")?; + match set_quantifier { + SetQuantifier::None => {} + _ => { + write!(f, " {set_quantifier}")?; + } + } + write!(f, " ")?; + let parenthesized_queries: Vec = + queries.iter().map(|query| format!("({query})")).collect(); + write!(f, "{}", display_comma_separated(&parenthesized_queries)) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a079488f..4f8f1b85 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9988,6 +9988,48 @@ impl<'a> Parser<'a> { Ok(IdentWithAlias { ident, alias }) } + /// Parse `identifier [AS] identifier` where the AS keyword is optional + fn parse_identifier_with_optional_alias(&mut self) -> Result { + let ident = self.parse_identifier()?; + let _after_as = self.parse_keyword(Keyword::AS); + let alias = self.parse_identifier()?; + Ok(IdentWithAlias { ident, alias }) + } + + /// Parse comma-separated list of parenthesized queries for pipe operators + fn parse_pipe_operator_queries(&mut self) -> Result, ParserError> { + self.parse_comma_separated(|parser| { + parser.expect_token(&Token::LParen)?; + let query = parser.parse_query()?; + parser.expect_token(&Token::RParen)?; + Ok(*query) + }) + } + + /// Parse set quantifier for pipe operators that require DISTINCT. E.g. INTERSECT and EXCEPT + fn parse_distinct_required_set_quantifier( + &mut self, + operator_name: &str, + ) -> Result { + let quantifier = self.parse_set_quantifier(&Some(SetOperator::Intersect)); + match quantifier { + SetQuantifier::Distinct | SetQuantifier::DistinctByName => Ok(quantifier), + _ => Err(ParserError::ParserError(format!( + "{operator_name} pipe operator requires DISTINCT modifier", + ))), + } + } + + /// Parse optional identifier alias (with or without AS keyword) + fn parse_identifier_optional_alias(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::AS) { + Ok(Some(self.parse_identifier()?)) + } else { + // Check if the next token is an identifier (implicit alias) + self.maybe_parse(|parser| parser.parse_identifier()) + } + } + /// Optionally parses an alias for a select list item fn maybe_parse_select_item_alias(&mut self) -> Result, ParserError> { fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { @@ -11134,6 +11176,19 @@ impl<'a> Parser<'a> { Keyword::AGGREGATE, Keyword::ORDER, Keyword::TABLESAMPLE, + Keyword::RENAME, + Keyword::UNION, + Keyword::INTERSECT, + Keyword::EXCEPT, + Keyword::CALL, + Keyword::PIVOT, + Keyword::UNPIVOT, + Keyword::JOIN, + Keyword::INNER, + Keyword::LEFT, + Keyword::RIGHT, + Keyword::FULL, + Keyword::CROSS, ])?; match kw { Keyword::SELECT => { @@ -11200,6 +11255,121 @@ impl<'a> Parser<'a> { let sample = self.parse_table_sample(TableSampleModifier::TableSample)?; pipe_operators.push(PipeOperator::TableSample { sample }); } + Keyword::RENAME => { + let mappings = + self.parse_comma_separated(Parser::parse_identifier_with_optional_alias)?; + pipe_operators.push(PipeOperator::Rename { mappings }); + } + Keyword::UNION => { + let set_quantifier = self.parse_set_quantifier(&Some(SetOperator::Union)); + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Union { + set_quantifier, + queries, + }); + } + Keyword::INTERSECT => { + let set_quantifier = + self.parse_distinct_required_set_quantifier("INTERSECT")?; + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Intersect { + set_quantifier, + queries, + }); + } + Keyword::EXCEPT => { + let set_quantifier = self.parse_distinct_required_set_quantifier("EXCEPT")?; + let queries = self.parse_pipe_operator_queries()?; + pipe_operators.push(PipeOperator::Except { + set_quantifier, + queries, + }); + } + Keyword::CALL => { + let function_name = self.parse_object_name(false)?; + let function_expr = self.parse_function(function_name)?; + if let Expr::Function(function) = function_expr { + let alias = self.parse_identifier_optional_alias()?; + pipe_operators.push(PipeOperator::Call { function, alias }); + } else { + return Err(ParserError::ParserError( + "Expected function call after CALL".to_string(), + )); + } + } + Keyword::PIVOT => { + self.expect_token(&Token::LParen)?; + let aggregate_functions = + self.parse_comma_separated(Self::parse_aliased_function_call)?; + self.expect_keyword_is(Keyword::FOR)?; + let value_column = self.parse_period_separated(|p| p.parse_identifier())?; + self.expect_keyword_is(Keyword::IN)?; + + self.expect_token(&Token::LParen)?; + let value_source = if self.parse_keyword(Keyword::ANY) { + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + PivotValueSource::Any(order_by) + } else if self.peek_sub_query() { + PivotValueSource::Subquery(self.parse_query()?) + } else { + PivotValueSource::List( + self.parse_comma_separated(Self::parse_expr_with_alias)?, + ) + }; + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::RParen)?; + + let alias = self.parse_identifier_optional_alias()?; + + pipe_operators.push(PipeOperator::Pivot { + aggregate_functions, + value_column, + value_source, + alias, + }); + } + Keyword::UNPIVOT => { + self.expect_token(&Token::LParen)?; + let value_column = self.parse_identifier()?; + self.expect_keyword(Keyword::FOR)?; + let name_column = self.parse_identifier()?; + self.expect_keyword(Keyword::IN)?; + + self.expect_token(&Token::LParen)?; + let unpivot_columns = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + + self.expect_token(&Token::RParen)?; + + let alias = self.parse_identifier_optional_alias()?; + + pipe_operators.push(PipeOperator::Unpivot { + value_column, + name_column, + unpivot_columns, + alias, + }); + } + Keyword::JOIN + | Keyword::INNER + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::FULL + | Keyword::CROSS => { + self.prev_token(); + let mut joins = self.parse_joins()?; + if joins.len() != 1 { + return Err(ParserError::ParserError( + "Join pipe operator must have a single join".to_string(), + )); + } + let join = joins.swap_remove(0); + pipe_operators.push(PipeOperator::Join(join)) + } unhandled => { return Err(ParserError::ParserError(format!( "`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}" diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f6f51459..42bdeae1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15217,10 +15217,426 @@ fn parse_pipeline_operator() { dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50 PERCENT)"); dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50) REPEATABLE (10)"); + // rename pipe operator + dialects.verified_stmt("SELECT * FROM users |> RENAME old_name AS new_name"); + dialects.verified_stmt("SELECT * FROM users |> RENAME id AS user_id, name AS user_name"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> RENAME id user_id", + "SELECT * FROM users |> RENAME id AS user_id", + ); + + // union pipe operator + dialects.verified_stmt("SELECT * FROM users |> UNION ALL (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION (SELECT * FROM admins)"); + + // union pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> UNION ALL (SELECT * FROM admins), (SELECT * FROM guests)", + ); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins), (SELECT * FROM guests), (SELECT * FROM employees)"); + dialects.verified_stmt( + "SELECT * FROM users |> UNION (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // union pipe operator with BY NAME modifier + dialects.verified_stmt("SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION ALL BY NAME (SELECT * FROM admins)"); + dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT BY NAME (SELECT * FROM admins)"); + + // union pipe operator with BY NAME and multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // intersect pipe operator (BigQuery requires DISTINCT modifier for INTERSECT) + dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins)"); + + // intersect pipe operator with BY NAME modifier + dialects + .verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins)"); + + // intersect pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // intersect pipe operator with BY NAME and multiple queries + dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)"); + + // except pipe operator (BigQuery requires DISTINCT modifier for EXCEPT) + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins)"); + + // except pipe operator with BY NAME modifier + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins)"); + + // except pipe operator with multiple queries + dialects.verified_stmt( + "SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)", + ); + + // except pipe operator with BY NAME and multiple queries + dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)"); + + // call pipe operator + dialects.verified_stmt("SELECT * FROM users |> CALL my_function()"); + dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5, 'test')"); + dialects.verified_stmt( + "SELECT * FROM users |> CALL namespace.function_name(col1, col2, 'literal')", + ); + + // call pipe operator with complex arguments + dialects.verified_stmt("SELECT * FROM users |> CALL transform_data(col1 + col2)"); + dialects.verified_stmt("SELECT * FROM users |> CALL analyze_data('param1', 100, true)"); + + // call pipe operator with aliases + dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) AS al"); + dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5) AS result_table"); + dialects.verified_stmt("SELECT * FROM users |> CALL namespace.func() AS my_alias"); + + // multiple call pipe operators in sequence + dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) |> CALL tvf2(arg2, arg3)"); + dialects.verified_stmt( + "SELECT * FROM data |> CALL transform(col1) |> CALL validate() |> CALL process(param)", + ); + + // multiple call pipe operators with aliases + dialects.verified_stmt( + "SELECT * FROM input_table |> CALL tvf1(arg1) AS step1 |> CALL tvf2(arg2) AS step2", + ); + dialects.verified_stmt( + "SELECT * FROM data |> CALL preprocess() AS clean_data |> CALL analyze(mode) AS results", + ); + + // call pipe operators mixed with other pipe operators + dialects.verified_stmt( + "SELECT * FROM users |> CALL transform() |> WHERE status = 'active' |> CALL process(param)", + ); + dialects.verified_stmt( + "SELECT * FROM data |> CALL preprocess() AS clean |> SELECT col1, col2 |> CALL validate()", + ); + + // pivot pipe operator + dialects.verified_stmt( + "SELECT * FROM monthly_sales |> PIVOT(SUM(amount) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))", + ); + dialects.verified_stmt("SELECT * FROM sales_data |> PIVOT(AVG(revenue) FOR region IN ('North', 'South', 'East', 'West'))"); + + // pivot pipe operator with multiple aggregate functions + dialects.verified_stmt("SELECT * FROM data |> PIVOT(SUM(sales) AS total_sales, COUNT(*) AS num_transactions FOR month IN ('Jan', 'Feb', 'Mar'))"); + + // pivot pipe operator with compound column names + dialects.verified_stmt("SELECT * FROM sales |> PIVOT(SUM(amount) FOR product.category IN ('Electronics', 'Clothing'))"); + + // pivot pipe operator mixed with other pipe operators + dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> PIVOT(SUM(revenue) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))"); + + // pivot pipe operator with aliases + dialects.verified_stmt("SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales"); + dialects.verified_stmt("SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category"); + dialects.verified_stmt("SELECT * FROM sales |> PIVOT(COUNT(*) AS transactions, SUM(amount) AS total FOR region IN ('North', 'South')) AS regional_summary"); + + // pivot pipe operator with implicit aliases (without AS keyword) + dialects.verified_query_with_canonical( + "SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) quarterly_sales", + "SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) avg_by_category", + "SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category", + ); + + // unpivot pipe operator basic usage + dialects + .verified_stmt("SELECT * FROM sales |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))"); + dialects.verified_stmt("SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C))"); + dialects.verified_stmt( + "SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory, disk))", + ); + + // unpivot pipe operator with multiple columns + dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (jan, feb, mar, apr, may, jun))"); + dialects.verified_stmt( + "SELECT * FROM report |> UNPIVOT(score FOR subject IN (math, science, english, history))", + ); + + // unpivot pipe operator mixed with other pipe operators + dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))"); + + // unpivot pipe operator with aliases + dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales"); + dialects.verified_stmt( + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data", + ); + dialects.verified_stmt("SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory)) AS metric_measurements"); + + // unpivot pipe operator with implicit aliases (without AS keyword) + dialects.verified_query_with_canonical( + "SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) unpivoted_sales", + "SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) transformed_data", + "SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data", + ); + // many pipes dialects.verified_stmt( "SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC", ); + + // join pipe operator - INNER JOIN + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt("SELECT * FROM users |> INNER JOIN orders ON users.id = orders.user_id"); + + // join pipe operator - LEFT JOIN + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt( + "SELECT * FROM users |> LEFT OUTER JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - RIGHT JOIN + dialects.verified_stmt("SELECT * FROM users |> RIGHT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt( + "SELECT * FROM users |> RIGHT OUTER JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - FULL JOIN + dialects.verified_stmt("SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id"); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> FULL OUTER JOIN orders ON users.id = orders.user_id", + "SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id", + ); + + // join pipe operator - CROSS JOIN + dialects.verified_stmt("SELECT * FROM users |> CROSS JOIN orders"); + + // join pipe operator with USING + dialects.verified_query_with_canonical( + "SELECT * FROM users |> JOIN orders USING (user_id)", + "SELECT * FROM users |> JOIN orders USING(user_id)", + ); + dialects.verified_query_with_canonical( + "SELECT * FROM users |> LEFT JOIN orders USING (user_id, order_date)", + "SELECT * FROM users |> LEFT JOIN orders USING(user_id, order_date)", + ); + + // join pipe operator with alias + dialects.verified_query_with_canonical( + "SELECT * FROM users |> JOIN orders o ON users.id = o.user_id", + "SELECT * FROM users |> JOIN orders AS o ON users.id = o.user_id", + ); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders AS o ON users.id = o.user_id"); + + // join pipe operator with complex ON condition + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id AND orders.status = 'active'"); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id AND orders.amount > 100"); + + // multiple join pipe operators + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> JOIN products ON orders.product_id = products.id"); + dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id |> RIGHT JOIN products ON orders.product_id = products.id"); + + // join pipe operator with other pipe operators + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> WHERE orders.amount > 100"); + dialects.verified_stmt("SELECT * FROM users |> WHERE users.active = true |> LEFT JOIN orders ON users.id = orders.user_id"); + dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> SELECT users.name, orders.amount"); +} + +#[test] +fn parse_pipeline_operator_negative_tests() { + let dialects = all_dialects_where(|d| d.supports_pipe_operator()); + + // Test that plain EXCEPT without DISTINCT fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT ALL fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT ALL (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT BY NAME without DISTINCT fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> EXCEPT BY NAME (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that EXCEPT ALL BY NAME fails + assert_eq!( + ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements( + "SELECT * FROM users |> EXCEPT ALL BY NAME (SELECT * FROM admins)" + ) + .unwrap_err() + ); + + // Test that plain INTERSECT without DISTINCT fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT ALL fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT ALL (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT BY NAME without DISTINCT fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements("SELECT * FROM users |> INTERSECT BY NAME (SELECT * FROM admins)") + .unwrap_err() + ); + + // Test that INTERSECT ALL BY NAME fails + assert_eq!( + ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()), + dialects + .parse_sql_statements( + "SELECT * FROM users |> INTERSECT ALL BY NAME (SELECT * FROM admins)" + ) + .unwrap_err() + ); + + // Test that CALL without function name fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL") + .is_err()); + + // Test that CALL without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function") + .is_err()); + + // Test that CALL with invalid function syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL 123invalid") + .is_err()); + + // Test that CALL with malformed arguments fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function(,)") + .is_err()); + + // Test that CALL with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CALL my_function() AS") + .is_err()); + + // Test that PIVOT without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT SUM(amount) FOR month IN ('Jan')") + .is_err()); + + // Test that PIVOT without FOR keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) month IN ('Jan'))") + .is_err()); + + // Test that PIVOT without IN keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month ('Jan'))") + .is_err()); + + // Test that PIVOT with empty IN list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ())") + .is_err()); + + // Test that PIVOT with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ('Jan')) AS") + .is_err()); + + // Test UNPIVOT negative cases + + // Test that UNPIVOT without parentheses fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT value FOR name IN col1, col2") + .is_err()); + + // Test that UNPIVOT without FOR keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value name IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT without IN keyword fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name (col1, col2))") + .is_err()); + + // Test that UNPIVOT with missing value column fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(FOR name IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT with missing name column fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR IN (col1, col2))") + .is_err()); + + // Test that UNPIVOT with empty IN list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN ())") + .is_err()); + + // Test that UNPIVOT with invalid alias syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)) AS") + .is_err()); + + // Test that UNPIVOT with missing closing parenthesis fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)") + .is_err()); + + // Test that JOIN without table name fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN ON users.id = orders.user_id") + .is_err()); + + // Test that CROSS JOIN with ON condition fails + assert!(dialects + .parse_sql_statements( + "SELECT * FROM users |> CROSS JOIN orders ON users.id = orders.user_id" + ) + .is_err()); + + // Test that CROSS JOIN with USING condition fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> CROSS JOIN orders USING (user_id)") + .is_err()); + + // Test that JOIN with empty USING list fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders USING ()") + .is_err()); + + // Test that JOIN with malformed ON condition fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders ON") + .is_err()); + + // Test that JOIN with invalid USING syntax fails + assert!(dialects + .parse_sql_statements("SELECT * FROM users |> JOIN orders USING user_id") + .is_err()); } #[test] From 9ffc546870a46eebb96a64e127b2190a05fa2baf Mon Sep 17 00:00:00 2001 From: Simon Vandel Sillesen Date: Tue, 1 Jul 2025 13:19:40 +0200 Subject: [PATCH 18/39] Make `GenericDialect` support from-first syntax (#1911) --- src/dialect/generic.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 0b882247..59671e21 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -112,6 +112,10 @@ impl Dialect for GenericDialect { true } + fn supports_from_first_select(&self) -> bool { + true + } + fn supports_asc_desc_in_column_definition(&self) -> bool { true } From f32a41a00451888fea24ee3487e303c85b8204b3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:52:29 +0200 Subject: [PATCH 19/39] Redshift utf8 idents (#1915) --- src/dialect/redshift.rs | 10 ++++++---- tests/sqlparser_common.rs | 9 ++++++++- tests/sqlparser_redshift.rs | 5 +++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index feccca5d..9ad9c5fd 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -80,13 +80,15 @@ impl Dialect for RedshiftSqlDialect { } fn is_identifier_start(&self, ch: char) -> bool { - // Extends Postgres dialect with sharp - PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#' + // Extends Postgres dialect with sharp and UTF-8 multibyte chars + // https://docs.aws.amazon.com/redshift/latest/dg/r_names.html + PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#' || !ch.is_ascii() } fn is_identifier_part(&self, ch: char) -> bool { - // Extends Postgres dialect with sharp - PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' + // Extends Postgres dialect with sharp and UTF-8 multibyte chars + // https://docs.aws.amazon.com/redshift/latest/dg/r_names.html + PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' || !ch.is_ascii() } /// redshift has `CONVERT(type, value)` instead of `CONVERT(value, type)` diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 42bdeae1..10a79252 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11068,10 +11068,17 @@ fn parse_non_latin_identifiers() { Box::new(RedshiftSqlDialect {}), Box::new(MySqlDialect {}), ]); - supported_dialects.verified_stmt("SELECT a.説明 FROM test.public.inter01 AS a"); supported_dialects.verified_stmt("SELECT a.説明 FROM inter01 AS a, inter01_transactions AS b WHERE a.説明 = b.取引 GROUP BY a.説明"); supported_dialects.verified_stmt("SELECT 説明, hühnervögel, garçon, Москва, 東京 FROM inter01"); + + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(MySqlDialect {}), + ]); assert!(supported_dialects .parse_sql_statements("SELECT 💝 FROM table1") .is_err()); diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index be2b6722..d539adf6 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -402,3 +402,8 @@ fn parse_extract_single_quotes() { fn parse_string_literal_backslash_escape() { redshift().one_statement_parses_to(r#"SELECT 'l\'auto'"#, "SELECT 'l''auto'"); } + +#[test] +fn parse_utf8_multibyte_idents() { + redshift().verified_stmt("SELECT 🚀.city AS 🎸 FROM customers AS 🚀"); +} From a3398223d7d6fcc0e5329aec76ceef040aeabaa9 Mon Sep 17 00:00:00 2001 From: Ryan Schneider Date: Wed, 2 Jul 2025 05:57:08 -0700 Subject: [PATCH 20/39] DuckDB: Add support for multiple `TRIM` arguments (#1916) Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 1 - tests/sqlparser_duckdb.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4f8f1b85..a3856d70 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2576,7 +2576,7 @@ impl<'a> Parser<'a> { trim_characters: None, }) } else if self.consume_token(&Token::Comma) - && dialect_of!(self is SnowflakeDialect | BigQueryDialect | GenericDialect) + && dialect_of!(self is DuckDbDialect | SnowflakeDialect | BigQueryDialect | GenericDialect) { let characters = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 10a79252..61a4de40 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7762,7 +7762,6 @@ fn parse_trim() { Box::new(MySqlDialect {}), //Box::new(BigQueryDialect {}), Box::new(SQLiteDialect {}), - Box::new(DuckDbDialect {}), ]); assert_eq!( diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 44cb22ce..371d3aac 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -24,6 +24,7 @@ use test_utils::*; use sqlparser::ast::*; use sqlparser::dialect::{DuckDbDialect, GenericDialect}; +use sqlparser::parser::ParserError; fn duckdb() -> TestedDialects { TestedDialects::new(vec![Box::new(DuckDbDialect {})]) @@ -830,3 +831,32 @@ fn parse_use() { ]))) ); } + +#[test] +fn test_duckdb_trim() { + let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(duckdb().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = duckdb().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value( + Value::SingleQuotedString("xyz".to_owned()).with_empty_span() + )), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value( + Value::SingleQuotedString("a".to_owned()).with_empty_span() + )]), + }, + expr_from_projection(only(&select.projection)) + ); + + // missing comma separation + let error_sql = "SELECT TRIM('xyz' 'a')"; + assert_eq!( + ParserError::ParserError("Expected: ), found: 'a'".to_owned()), + duckdb().parse_sql_statements(error_sql).unwrap_err() + ); +} From 015caca611f67c22d7e1da5df11a5313afd97509 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:16:21 +0200 Subject: [PATCH 21/39] Redshift alter column type no set (#1912) --- src/ast/ddl.rs | 20 +++++++++++++++----- src/ast/spans.rs | 1 + src/dialect/mod.rs | 9 +++++++++ src/dialect/postgresql.rs | 4 ++++ src/parser/mod.rs | 30 ++++++++++++++++++++---------- tests/sqlparser_common.rs | 17 ++++++++--------- tests/sqlparser_postgres.rs | 6 ++---- 7 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index afe9f228..9134412c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -893,7 +893,10 @@ pub enum AlterColumnOperation { data_type: DataType, /// PostgreSQL specific using: Option, + /// Set to true if the statement includes the `SET DATA TYPE` keywords + had_set: bool, }, + /// `ADD GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ]` /// /// Note: this is a PostgreSQL-specific operation. @@ -914,12 +917,19 @@ impl fmt::Display for AlterColumnOperation { AlterColumnOperation::DropDefault => { write!(f, "DROP DEFAULT") } - AlterColumnOperation::SetDataType { data_type, using } => { - if let Some(expr) = using { - write!(f, "SET DATA TYPE {data_type} USING {expr}") - } else { - write!(f, "SET DATA TYPE {data_type}") + AlterColumnOperation::SetDataType { + data_type, + using, + had_set, + } => { + if *had_set { + write!(f, "SET DATA ")?; } + write!(f, "TYPE {data_type}")?; + if let Some(expr) = using { + write!(f, " USING {expr}")?; + } + Ok(()) } AlterColumnOperation::AddGenerated { generated_as, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 78ed772b..00882602 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -924,6 +924,7 @@ impl Spanned for AlterColumnOperation { AlterColumnOperation::SetDataType { data_type: _, using, + had_set: _, } => using.as_ref().map_or(Span::empty(), |u| u.span()), AlterColumnOperation::AddGenerated { .. } => Span::empty(), } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 028aa58a..b214699c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1060,6 +1060,15 @@ pub trait Dialect: Debug + Any { fn supports_space_separated_column_options(&self) -> bool { false } + + /// Returns true if the dialect supports the `USING` clause in an `ALTER COLUMN` statement. + /// Example: + /// ```sql + /// ALTER TABLE tbl ALTER COLUMN col SET DATA TYPE USING ` + /// ``` + fn supports_alter_column_type_using(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index a91ab598..b2d4014c 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -258,4 +258,8 @@ impl Dialect for PostgreSqlDialect { fn supports_set_names(&self) -> bool { true } + + fn supports_alter_column_type_using(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a3856d70..6360817f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8734,16 +8734,10 @@ impl<'a> Parser<'a> { } } else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) { AlterColumnOperation::DropDefault {} - } else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) - || (is_postgresql && self.parse_keyword(Keyword::TYPE)) - { - let data_type = self.parse_data_type()?; - let using = if is_postgresql && self.parse_keyword(Keyword::USING) { - Some(self.parse_expr()?) - } else { - None - }; - AlterColumnOperation::SetDataType { data_type, using } + } else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) { + self.parse_set_data_type(true)? + } else if self.parse_keyword(Keyword::TYPE) { + self.parse_set_data_type(false)? } else if self.parse_keywords(&[Keyword::ADD, Keyword::GENERATED]) { let generated_as = if self.parse_keyword(Keyword::ALWAYS) { Some(GeneratedAs::Always) @@ -8909,6 +8903,22 @@ impl<'a> Parser<'a> { Ok(operation) } + fn parse_set_data_type(&mut self, had_set: bool) -> Result { + let data_type = self.parse_data_type()?; + let using = if self.dialect.supports_alter_column_type_using() + && self.parse_keyword(Keyword::USING) + { + Some(self.parse_expr()?) + } else { + None + }; + Ok(AlterColumnOperation::SetDataType { + data_type, + using, + had_set, + }) + } + fn parse_part_or_partition(&mut self) -> Result { let keyword = self.expect_one_of_keywords(&[Keyword::PART, Keyword::PARTITION])?; match keyword { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 61a4de40..380bd475 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5057,22 +5057,21 @@ fn parse_alter_table_alter_column_type() { AlterColumnOperation::SetDataType { data_type: DataType::Text, using: None, + had_set: true, } ); } _ => unreachable!(), } + verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT")); - let dialect = TestedDialects::new(vec![Box::new(GenericDialect {})]); + let dialects = all_dialects_where(|d| d.supports_alter_column_type_using()); + dialects.verified_stmt(&format!( + "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'" + )); - let res = - dialect.parse_sql_statements(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT")); - assert_eq!( - ParserError::ParserError("Expected: SET/DROP NOT NULL, SET DEFAULT, or SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()), - res.unwrap_err() - ); - - let res = dialect.parse_sql_statements(&format!( + let dialects = all_dialects_except(|d| d.supports_alter_column_type_using()); + let res = dialects.parse_sql_statements(&format!( "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'" )); assert_eq!( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7b0a8c5d..e1a49c69 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -764,10 +764,7 @@ fn parse_drop_extension() { #[test] fn parse_alter_table_alter_column() { - pg().one_statement_parses_to( - "ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'", - "ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", - ); + pg().verified_stmt("ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'"); match alter_table_op( pg().verified_stmt( @@ -783,6 +780,7 @@ fn parse_alter_table_alter_column() { AlterColumnOperation::SetDataType { data_type: DataType::Text, using: Some(using_expr), + had_set: true, } ); } From 418b94227a5cf6ae721629f95a279c40b6a196ad Mon Sep 17 00:00:00 2001 From: carl <44021312+achristmascarl@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:19:26 -0400 Subject: [PATCH 22/39] Postgres: support `ADD CONSTRAINT NOT VALID` and `VALIDATE CONSTRAINT` (#1908) --- src/ast/ddl.rs | 25 +++++++++++++++++--- src/ast/spans.rs | 6 ++++- src/keywords.rs | 1 + src/parser/mod.rs | 9 ++++++- src/test_utils.rs | 31 ++++++++++++++---------- tests/sqlparser_common.rs | 2 +- tests/sqlparser_postgres.rs | 47 ++++++++++++++++++++++++++++++++++--- 7 files changed, 99 insertions(+), 22 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9134412c..9d500203 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -67,8 +67,11 @@ impl fmt::Display for ReplicaIdentity { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum AlterTableOperation { - /// `ADD ` - AddConstraint(TableConstraint), + /// `ADD [NOT VALID]` + AddConstraint { + constraint: TableConstraint, + not_valid: bool, + }, /// `ADD [COLUMN] [IF NOT EXISTS] ` AddColumn { /// `[COLUMN]`. @@ -344,6 +347,10 @@ pub enum AlterTableOperation { equals: bool, value: ValueWithSpan, }, + /// `VALIDATE CONSTRAINT ` + ValidateConstraint { + name: Ident, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -494,7 +501,16 @@ impl fmt::Display for AlterTableOperation { display_separated(new_partitions, " "), ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } ), - AlterTableOperation::AddConstraint(c) => write!(f, "ADD {c}"), + AlterTableOperation::AddConstraint { + not_valid, + constraint, + } => { + write!(f, "ADD {constraint}")?; + if *not_valid { + write!(f, " NOT VALID")?; + } + Ok(()) + } AlterTableOperation::AddColumn { column_keyword, if_not_exists, @@ -772,6 +788,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::ReplicaIdentity { identity } => { write!(f, "REPLICA IDENTITY {identity}") } + AlterTableOperation::ValidateConstraint { name } => { + write!(f, "VALIDATE CONSTRAINT {name}") + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 00882602..26205496 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1075,7 +1075,10 @@ impl Spanned for CreateTableOptions { impl Spanned for AlterTableOperation { fn span(&self) -> Span { match self { - AlterTableOperation::AddConstraint(table_constraint) => table_constraint.span(), + AlterTableOperation::AddConstraint { + constraint, + not_valid: _, + } => constraint.span(), AlterTableOperation::AddColumn { column_keyword: _, if_not_exists: _, @@ -1196,6 +1199,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::AutoIncrement { value, .. } => value.span(), AlterTableOperation::Lock { .. } => Span::empty(), AlterTableOperation::ReplicaIdentity { .. } => Span::empty(), + AlterTableOperation::ValidateConstraint { name } => name.span, } } } diff --git a/src/keywords.rs b/src/keywords.rs index a8bbca3d..49a54e8a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -981,6 +981,7 @@ define_keywords!( UUID, VACUUM, VALID, + VALIDATE, VALIDATION_MODE, VALUE, VALUES, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6360817f..1c40a964 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8477,7 +8477,11 @@ impl<'a> Parser<'a> { pub fn parse_alter_table_operation(&mut self) -> Result { let operation = if self.parse_keyword(Keyword::ADD) { if let Some(constraint) = self.parse_optional_table_constraint()? { - AlterTableOperation::AddConstraint(constraint) + let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]); + AlterTableOperation::AddConstraint { + constraint, + not_valid, + } } else if dialect_of!(self is ClickHouseDialect|GenericDialect) && self.parse_keyword(Keyword::PROJECTION) { @@ -8886,6 +8890,9 @@ impl<'a> Parser<'a> { }; AlterTableOperation::ReplicaIdentity { identity } + } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { + let name = self.parse_identifier()?; + AlterTableOperation::ValidateConstraint { name } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/src/test_utils.rs b/src/test_utils.rs index db7b3dd6..544ceaef 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -479,20 +479,25 @@ pub fn index_column(stmt: Statement) -> Expr { } } Statement::AlterTable { operations, .. } => match operations.first().unwrap() { - AlterTableOperation::AddConstraint(TableConstraint::Index { columns, .. }) => { - columns.first().unwrap().column.expr.clone() + AlterTableOperation::AddConstraint { constraint, .. } => { + match constraint { + TableConstraint::Index { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::Unique { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::PrimaryKey { columns, .. } => { + columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial { + columns, + .. + } => columns.first().unwrap().column.expr.clone(), + _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + } } - AlterTableOperation::AddConstraint(TableConstraint::Unique { columns, .. }) => { - columns.first().unwrap().column.expr.clone() - } - AlterTableOperation::AddConstraint(TableConstraint::PrimaryKey { columns, .. }) => { - columns.first().unwrap().column.expr.clone() - } - AlterTableOperation::AddConstraint(TableConstraint::FulltextOrSpatial { - columns, - .. - }) => columns.first().unwrap().column.expr.clone(), - _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), + _ => panic!("Expected a constraint"), }, _ => panic!("Expected CREATE INDEX, ALTER TABLE, or CREATE TABLE, got: {stmt:?}"), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 380bd475..9ca985b3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4956,7 +4956,7 @@ fn parse_alter_table_constraints() { match alter_table_op(verified_stmt(&format!( "ALTER TABLE tab ADD {constraint_text}" ))) { - AlterTableOperation::AddConstraint(constraint) => { + AlterTableOperation::AddConstraint { constraint, .. } => { assert_eq!(constraint_text, constraint.to_string()); } _ => unreachable!(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e1a49c69..16ba5f23 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -606,9 +606,10 @@ fn parse_alter_table_constraints_unique_nulls_distinct() { .verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)") { Statement::AlterTable { operations, .. } => match &operations[0] { - AlterTableOperation::AddConstraint(TableConstraint::Unique { - nulls_distinct, .. - }) => { + AlterTableOperation::AddConstraint { + constraint: TableConstraint::Unique { nulls_distinct, .. }, + .. + } => { assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct) } _ => unreachable!(), @@ -6229,3 +6230,43 @@ fn parse_ts_datatypes() { _ => unreachable!(), } } + +#[test] +fn parse_alter_table_constraint_not_valid() { + match pg_and_generic().verified_stmt( + "ALTER TABLE foo ADD CONSTRAINT bar FOREIGN KEY (baz) REFERENCES other(ref) NOT VALID", + ) { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::AddConstraint { + constraint: TableConstraint::ForeignKey { + name: Some("bar".into()), + index_name: None, + columns: vec!["baz".into()], + foreign_table: ObjectName::from(vec!["other".into()]), + referred_columns: vec!["ref".into()], + on_delete: None, + on_update: None, + characteristics: None, + }, + not_valid: true, + }] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_validate_constraint() { + match pg_and_generic().verified_stmt("ALTER TABLE foo VALIDATE CONSTRAINT bar") { + Statement::AlterTable { operations, .. } => { + assert_eq!( + operations, + vec![AlterTableOperation::ValidateConstraint { name: "bar".into() }] + ); + } + _ => unreachable!(), + } +} From be2d2f14e740ac753d51609b026dae012b697353 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:22:17 +0200 Subject: [PATCH 23/39] Add support for MySQL MEMBER OF (#1917) --- src/ast/mod.rs | 24 ++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 2 ++ src/parser/mod.rs | 13 +++++++++++++ tests/sqlparser_mysql.rs | 25 +++++++++++++++++++++++++ 5 files changed, 65 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 19966d21..9e502260 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1124,6 +1124,8 @@ pub enum Expr { /// [Databricks](https://docs.databricks.com/en/sql/language-manual/sql-ref-lambda-functions.html) /// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html) Lambda(LambdaFunction), + /// Checks membership of a value in a JSON array + MemberOf(MemberOf), } impl Expr { @@ -1912,6 +1914,7 @@ impl fmt::Display for Expr { } Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Lambda(lambda) => write!(f, "{lambda}"), + Expr::MemberOf(member_of) => write!(f, "{member_of}"), } } } @@ -9831,6 +9834,27 @@ impl fmt::Display for NullInclusion { } } +/// Checks membership of a value in a JSON array +/// +/// Syntax: +/// ```sql +/// MEMBER OF() +/// ``` +/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html#operator_member-of) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MemberOf { + pub value: Box, + pub array: Box, +} + +impl fmt::Display for MemberOf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} MEMBER OF({})", self.value, self.array) + } +} + #[cfg(test)] mod tests { use crate::tokenizer::Location; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 26205496..f8ffeb54 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1624,6 +1624,7 @@ impl Spanned for Expr { Expr::OuterJoin(expr) => expr.span(), Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), + Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b214699c..3345380c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -649,6 +649,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), _ => Ok(self.prec_unknown()), }, Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)), @@ -661,6 +662,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)), Token::Period => Ok(p!(Period)), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1c40a964..bfd75385 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3609,6 +3609,19 @@ impl<'a> Parser<'a> { self.expected("IN or BETWEEN after NOT", self.peek_token()) } } + Keyword::MEMBER => { + if self.parse_keyword(Keyword::OF) { + self.expect_token(&Token::LParen)?; + let array = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Ok(Expr::MemberOf(MemberOf { + value: Box::new(expr), + array: Box::new(array), + })) + } else { + self.expected("OF after MEMBER", self.peek_token()) + } + } // Can only happen if `get_next_precedence` got out of sync with this function _ => parser_err!( format!("No infix parser for token {:?}", tok.token), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index d2feee03..9224a003 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4109,3 +4109,28 @@ fn parse_alter_table_drop_index() { AlterTableOperation::DropIndex { name } if name.value == "idx_index" ); } + +#[test] +fn parse_json_member_of() { + mysql().verified_stmt(r#"SELECT 17 MEMBER OF('[23, "abc", 17, "ab", 10]')"#); + let sql = r#"SELECT 'ab' MEMBER OF('[23, "abc", 17, "ab", 10]')"#; + let stmt = mysql().verified_stmt(sql); + match stmt { + Statement::Query(query) => { + let select = query.body.as_select().unwrap(); + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::MemberOf(MemberOf { + value: Box::new(Expr::Value( + Value::SingleQuotedString("ab".to_string()).into() + )), + array: Box::new(Expr::Value( + Value::SingleQuotedString(r#"[23, "abc", 17, "ab", 10]"#.to_string()) + .into() + )), + }))] + ); + } + _ => panic!("Unexpected statement {stmt}"), + } +} From 9020385c027239979adbef5506c7be4a07cc594c Mon Sep 17 00:00:00 2001 From: feral-dot-io <70718208+feral-dot-io@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:24:51 +0000 Subject: [PATCH 24/39] Add span for `Expr::TypedString` (#1919) --- src/ast/mod.rs | 2 +- src/ast/spans.rs | 1 - src/parser/mod.rs | 5 +- tests/sqlparser_bigquery.rs | 77 ++++++++++++++++++------ tests/sqlparser_common.rs | 106 ++++++++++++++++++++++++++-------- tests/sqlparser_databricks.rs | 6 +- tests/sqlparser_postgres.rs | 5 +- 7 files changed, 155 insertions(+), 47 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9e502260..d7e342bd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -996,7 +996,7 @@ pub enum Expr { data_type: DataType, /// The value of the constant. /// Hint: you can unwrap the string value using `value.into_string()`. - value: Value, + value: ValueWithSpan, }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f8ffeb54..0895b844 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1415,7 +1415,6 @@ impl Spanned for AssignmentTarget { /// f.e. `IS NULL ` reports as `::span`. /// /// Missing spans: -/// - [Expr::TypedString] # missing span for data_type /// - [Expr::MatchAgainst] # MySQL specific /// - [Expr::RLike] # MySQL specific /// - [Expr::Struct] # BigQuery specific diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bfd75385..f38a360e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1521,7 +1521,7 @@ impl<'a> Parser<'a> { DataType::Custom(..) => parser_err!("dummy", loc), data_type => Ok(Expr::TypedString { data_type, - value: parser.parse_value()?.value, + value: parser.parse_value()?, }), } })?; @@ -1708,10 +1708,9 @@ impl<'a> Parser<'a> { } fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result { - let value: Value = self.parse_value()?.value; Ok(Expr::TypedString { data_type: DataType::GeometricType(kind), - value, + value: self.parse_value()?, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 2bcdb4e5..2ba54d3e 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -906,7 +906,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + }, }], fields: vec![StructField { field_name: None, @@ -965,9 +968,12 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() - ) + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -998,7 +1004,12 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString( + "2008-12-25 15:30:00 America/Los_Angeles".into() + ), + span: Span::empty(), + }, }], fields: vec![StructField { field_name: None, @@ -1013,7 +1024,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("15:30:00".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("15:30:00".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1031,7 +1045,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1045,7 +1062,10 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1219,7 +1239,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1278,9 +1301,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() - ) + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1311,7 +1337,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString( + "2008-12-25 15:30:00 America/Los_Angeles".into() + ), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1326,7 +1357,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("15:30:00".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("15:30:00".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1344,7 +1378,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -1358,7 +1395,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString("1".into()) + value: ValueWithSpan { + value: Value::SingleQuotedString("1".into()), + span: Span::empty(), + } }], fields: vec![StructField { field_name: None, @@ -2393,7 +2433,10 @@ fn test_triple_quote_typed_strings() { assert_eq!( Expr::TypedString { data_type: DataType::JSON, - value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()) + value: ValueWithSpan { + value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()), + span: Span::empty(), + } }, expr ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9ca985b3..ac461bb2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5851,7 +5851,10 @@ fn parse_literal_date() { assert_eq!( &Expr::TypedString { data_type: DataType::Date, - value: Value::SingleQuotedString("1999-01-01".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01".into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -5864,7 +5867,10 @@ fn parse_literal_time() { assert_eq!( &Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: Value::SingleQuotedString("01:23:34".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("01:23:34".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -5877,7 +5883,10 @@ fn parse_literal_datetime() { assert_eq!( &Expr::TypedString { data_type: DataType::Datetime(None), - value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -5890,7 +5899,10 @@ fn parse_literal_timestamp_without_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -5905,7 +5917,10 @@ fn parse_literal_timestamp_with_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::Tz), - value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), + span: Span::empty(), + }, }, expr_from_projection(only(&select.projection)), ); @@ -6477,8 +6492,9 @@ fn parse_json_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString( - r#"{ + value: ValueWithSpan { + value: Value::SingleQuotedString( + r#"{ "id": 10, "type": "fruit", "name": "apple", @@ -6498,8 +6514,10 @@ fn parse_json_keyword() { ] } }"# - .to_string() - ) + .to_string() + ), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6511,7 +6529,10 @@ fn parse_typed_strings() { assert_eq!( Expr::TypedString { data_type: DataType::JSON, - value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()), + span: Span::empty(), + } }, expr ); @@ -6529,7 +6550,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"0"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"0"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6540,7 +6564,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"123456"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"123456"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6551,7 +6578,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-3.14"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-3.14"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6562,7 +6592,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-0.54321"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-0.54321"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6573,7 +6606,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"1.23456e05"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"1.23456e05"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -6584,7 +6620,10 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: Value::SingleQuotedString(r#"-9.876e-3"#.into()) + value: ValueWithSpan { + value: Value::SingleQuotedString(r#"-9.876e-3"#.into()), + span: Span::empty(), + } }, expr_from_projection(only(&select.projection)), ); @@ -14833,7 +14872,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Point), - value: Value::SingleQuotedString("1,2".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2".to_string()), + span: Span::empty(), + }, } ); @@ -14842,7 +14884,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Line), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); @@ -14851,7 +14896,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); let sql = "box '1,2,3,4'"; @@ -14859,7 +14907,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); @@ -14868,7 +14919,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Circle), - value: Value::SingleQuotedString("1,2,3".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3".to_string()), + span: Span::empty(), + }, } ); @@ -14877,7 +14931,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::Polygon), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); let sql = "lseg '1,2,3,4'"; @@ -14885,7 +14942,10 @@ fn test_geometry_type() { all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), Expr::TypedString { data_type: DataType::GeometricType(GeometricTypeKind::LineSegment), - value: Value::SingleQuotedString("1,2,3,4".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("1,2,3,4".to_string()), + span: Span::empty(), + }, } ); } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index baf279fa..a27e0699 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -19,6 +19,7 @@ use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::ast::*; use sqlparser::dialect::{DatabricksDialect, GenericDialect}; use sqlparser::parser::ParserError; +use sqlparser::tokenizer::Span; use test_utils::*; #[macro_use] @@ -328,7 +329,10 @@ fn data_type_timestamp_ntz() { databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), Expr::TypedString { data_type: DataType::TimestampNtz, - value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()) + value: ValueWithSpan { + value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), + span: Span::empty(), + } } ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 16ba5f23..db5c1611 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5257,7 +5257,10 @@ fn parse_at_time_zone() { left: Box::new(Expr::AtTimeZone { timestamp: Box::new(Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), + value: ValueWithSpan { + value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), + span: Span::empty(), + }, }), time_zone: Box::new(Expr::Cast { kind: CastKind::DoubleColon, From 239e30a97c5f088d39f1747fb108c8d8783a2e5c Mon Sep 17 00:00:00 2001 From: Sergey Olontsev Date: Thu, 3 Jul 2025 18:04:32 +0100 Subject: [PATCH 25/39] Support for Postgres `CREATE SERVER` (#1914) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 69 ++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 2 + src/parser/mod.rs | 45 +++++++++++++++++++++ tests/sqlparser_postgres.rs | 79 +++++++++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d7e342bd..cffd3292 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3318,6 +3318,8 @@ pub enum Statement { secret_type: Ident, options: Vec, }, + /// A `CREATE SERVER` statement. + CreateServer(CreateServerStatement), /// ```sql /// CREATE POLICY /// ``` @@ -5178,6 +5180,9 @@ impl fmt::Display for Statement { write!(f, " )")?; Ok(()) } + Statement::CreateServer(stmt) => { + write!(f, "{stmt}") + } Statement::CreatePolicy { name, table_name, @@ -7976,6 +7981,70 @@ impl fmt::Display for SecretOption { } } +/// A `CREATE SERVER` statement. +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createserver.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateServerStatement { + pub name: ObjectName, + pub if_not_exists: bool, + pub server_type: Option, + pub version: Option, + pub foreign_data_wrapper: ObjectName, + pub options: Option>, +} + +impl fmt::Display for CreateServerStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let CreateServerStatement { + name, + if_not_exists, + server_type, + version, + foreign_data_wrapper, + options, + } = self; + + write!( + f, + "CREATE SERVER {if_not_exists}{name} ", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + + if let Some(st) = server_type { + write!(f, "TYPE {st} ")?; + } + + if let Some(v) = version { + write!(f, "VERSION {v} ")?; + } + + write!(f, "FOREIGN DATA WRAPPER {foreign_data_wrapper}")?; + + if let Some(o) = options { + write!(f, " OPTIONS ({o})", o = display_comma_separated(o))?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateServerOption { + pub key: Ident, + pub value: Ident, +} + +impl fmt::Display for CreateServerOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.key, self.value) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0895b844..1d790ab6 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -423,6 +423,7 @@ impl Spanned for Statement { Statement::CreateIndex(create_index) => create_index.span(), Statement::CreateRole { .. } => Span::empty(), Statement::CreateSecret { .. } => Span::empty(), + Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), Statement::AlterTable { name, diff --git a/src/keywords.rs b/src/keywords.rs index 49a54e8a..73865150 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -816,6 +816,7 @@ define_keywords!( SERDE, SERDEPROPERTIES, SERIALIZABLE, + SERVER, SERVICE, SESSION, SESSION_USER, @@ -1017,6 +1018,7 @@ define_keywords!( WITHOUT, WITHOUT_ARRAY_WRAPPER, WORK, + WRAPPER, WRITE, XML, XMLNAMESPACES, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f38a360e..289dd6b3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4674,6 +4674,8 @@ impl<'a> Parser<'a> { self.parse_create_procedure(or_alter) } else if self.parse_keyword(Keyword::CONNECTOR) { self.parse_create_connector() + } else if self.parse_keyword(Keyword::SERVER) { + self.parse_pg_create_server() } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -16009,6 +16011,49 @@ impl<'a> Parser<'a> { Ok(sequence_options) } + /// Parse a `CREATE SERVER` statement. + /// + /// See [Statement::CreateServer] + pub fn parse_pg_create_server(&mut self) -> Result { + let ine = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + + let server_type = if self.parse_keyword(Keyword::TYPE) { + Some(self.parse_identifier()?) + } else { + None + }; + + let version = if self.parse_keyword(Keyword::VERSION) { + Some(self.parse_identifier()?) + } else { + None + }; + + self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?; + let foreign_data_wrapper = self.parse_object_name(false)?; + + let mut options = None; + if self.parse_keyword(Keyword::OPTIONS) { + self.expect_token(&Token::LParen)?; + options = Some(self.parse_comma_separated(|p| { + let key = p.parse_identifier()?; + let value = p.parse_identifier()?; + Ok(CreateServerOption { key, value }) + })?); + self.expect_token(&Token::RParen)?; + } + + Ok(Statement::CreateServer(CreateServerStatement { + name, + if_not_exists: ine, + server_type, + version, + foreign_data_wrapper, + options, + })) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index db5c1611..48792025 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6273,3 +6273,82 @@ fn parse_alter_table_validate_constraint() { _ => unreachable!(), } } + +#[test] +fn parse_create_server() { + let test_cases = vec![ + ( + "CREATE SERVER myserver FOREIGN DATA WRAPPER postgres_fdw", + CreateServerStatement { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: false, + server_type: None, + version: None, + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + }, + ), + ( + "CREATE SERVER IF NOT EXISTS myserver TYPE 'server_type' VERSION 'server_version' FOREIGN DATA WRAPPER postgres_fdw", + CreateServerStatement { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: true, + server_type: Some(Ident { + value: "server_type".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + version: Some(Ident { + value: "server_version".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + } + ), + ( + "CREATE SERVER myserver2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foo', dbname 'foodb', port '5432')", + CreateServerStatement { + name: ObjectName::from(vec!["myserver2".into()]), + if_not_exists: false, + server_type: None, + version: None, + foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]), + options: Some(vec![ + CreateServerOption { + key: "host".into(), + value: Ident { + value: "foo".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + CreateServerOption { + key: "dbname".into(), + value: Ident { + value: "foodb".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + CreateServerOption { + key: "port".into(), + value: Ident { + value: "5432".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + ]), + } + ) + ]; + + for (sql, expected) in test_cases { + let Statement::CreateServer(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt, expected); + } +} From 942d747d893ad1c25fc32b002066b3bda72071b6 Mon Sep 17 00:00:00 2001 From: Elia Perantoni Date: Fri, 4 Jul 2025 18:21:31 +0200 Subject: [PATCH 26/39] Change tag and policy names to `ObjectName` (#1892) --- src/ast/ddl.rs | 2 +- src/ast/mod.rs | 4 +-- src/dialect/snowflake.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 64 +++++++++++++++++++++++++++--------- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9d500203..7e46a59f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1709,7 +1709,7 @@ pub struct ColumnPolicyProperty { /// ``` /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table pub with: bool, - pub policy_name: Ident, + pub policy_name: ObjectName, pub using_columns: Option>, } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cffd3292..7db4af11 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9372,12 +9372,12 @@ impl Display for RowAccessPolicy { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Tag { - pub key: Ident, + pub key: ObjectName, pub value: String, } impl Tag { - pub fn new(key: Ident, value: String) -> Self { + pub fn new(key: ObjectName, value: String) -> Self { Self { key, value } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ba28a8ec..ee770b0f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1189,7 +1189,7 @@ fn parse_column_policy_property( parser: &mut Parser, with: bool, ) -> Result { - let policy_name = parser.parse_identifier()?; + let policy_name = parser.parse_object_name(false)?; let using_columns = if parser.parse_keyword(Keyword::USING) { parser.expect_token(&Token::LParen)?; let columns = parser.parse_comma_separated(|p| p.parse_identifier())?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 289dd6b3..612144a6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7884,7 +7884,7 @@ impl<'a> Parser<'a> { } pub(crate) fn parse_tag(&mut self) -> Result { - let name = self.parse_identifier()?; + let name = self.parse_object_name(false)?; self.expect_token(&Token::Eq)?; let value = self.parse_literal_string()?; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 3ffd3339..8a1558b2 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -270,8 +270,8 @@ fn test_snowflake_create_table_with_tag() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(vec![ - Tag::new("A".into(), "TAG A".to_string()), - Tag::new("B".into(), "TAG B".to_string()) + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".to_string()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".to_string()) ]), with_tags ); @@ -291,8 +291,8 @@ fn test_snowflake_create_table_with_tag() { assert_eq!("my_table", name.to_string()); assert_eq!( Some(vec![ - Tag::new("A".into(), "TAG A".to_string()), - Tag::new("B".into(), "TAG B".to_string()) + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".to_string()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".to_string()) ]), with_tags ); @@ -731,7 +731,7 @@ fn test_snowflake_create_table_with_columns_masking_policy() { option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns, } )) @@ -765,7 +765,7 @@ fn test_snowflake_create_table_with_columns_projection_policy() { option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with, - policy_name: "p".into(), + policy_name: ObjectName::from(vec![Ident::new("p")]), using_columns: None, } )) @@ -802,8 +802,14 @@ fn test_snowflake_create_table_with_columns_tags() { option: ColumnOption::Tags(TagsColumnOption { with, tags: vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new( + ObjectName::from(vec![Ident::new("A")]), + "TAG A".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("B")]), + "TAG B".into() + ), ] }), }], @@ -846,7 +852,7 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( ColumnPolicyProperty { with: true, - policy_name: "p1".into(), + policy_name: ObjectName::from(vec![Ident::new("p1")]), using_columns: Some(vec!["a".into(), "b".into()]), } )), @@ -856,8 +862,14 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Tags(TagsColumnOption { with: true, tags: vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new( + ObjectName::from(vec![Ident::new("A")]), + "TAG A".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("B")]), + "TAG B".into() + ), ] }), } @@ -878,7 +890,7 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( ColumnPolicyProperty { with: false, - policy_name: "p2".into(), + policy_name: ObjectName::from(vec![Ident::new("p2")]), using_columns: None, } )), @@ -888,8 +900,14 @@ fn test_snowflake_create_table_with_several_column_options() { option: ColumnOption::Tags(TagsColumnOption { with: false, tags: vec![ - Tag::new("C".into(), "TAG C".into()), - Tag::new("D".into(), "TAG D".into()), + Tag::new( + ObjectName::from(vec![Ident::new("C")]), + "TAG C".into() + ), + Tag::new( + ObjectName::from(vec![Ident::new("D")]), + "TAG D".into() + ), ] }), } @@ -942,8 +960,8 @@ fn test_snowflake_create_iceberg_table_all_options() { with_aggregation_policy.map(|name| name.to_string()) ); assert_eq!(Some(vec![ - Tag::new("A".into(), "TAG A".into()), - Tag::new("B".into(), "TAG B".into()), + Tag::new(ObjectName::from(vec![Ident::new("A")]), "TAG A".into()), + Tag::new(ObjectName::from(vec![Ident::new("B")]), "TAG B".into()), ]), with_tags); } @@ -4172,3 +4190,17 @@ fn test_snowflake_create_view_with_multiple_column_options() { r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#; snowflake().verified_stmt(create_view_with_tag); } + +#[test] +fn test_snowflake_create_view_with_composite_tag() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH TAG (foo.bar.baz.pii='email')) AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} + +#[test] +fn test_snowflake_create_view_with_composite_policy_name() { + let create_view_with_tag = + r#"CREATE VIEW X (COL WITH MASKING POLICY foo.bar.baz) AS SELECT * FROM Y"#; + snowflake().verified_stmt(create_view_with_tag); +} From b0bcc46e227162b4a9147c9c1e0a88bf3e0b5e1f Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 4 Jul 2025 21:04:51 +0200 Subject: [PATCH 27/39] Add support for NULL escape char in pattern match searches (#1913) --- src/ast/mod.rs | 12 ++++++------ src/parser/mod.rs | 4 ++-- tests/sqlparser_common.rs | 25 +++++++++++++++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7db4af11..425e1fb6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -809,7 +809,7 @@ pub enum Expr { any: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// `ILIKE` (case-insensitive `LIKE`) ILike { @@ -819,14 +819,14 @@ pub enum Expr { any: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// SIMILAR TO regex SimilarTo { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// MySQL: RLIKE regex or REGEXP regex RLike { @@ -1488,7 +1488,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}LIKE {}{} ESCAPE '{}'", + "{} {}LIKE {}{} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, if *any { "ANY " } else { "" }, @@ -1513,7 +1513,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}ILIKE {}{} ESCAPE '{}'", + "{} {}ILIKE {}{} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, if *any { "ANY" } else { "" }, @@ -1568,7 +1568,7 @@ impl fmt::Display for Expr { } => match escape_char { Some(ch) => write!( f, - "{} {}SIMILAR TO {} ESCAPE '{}'", + "{} {}SIMILAR TO {} ESCAPE {}", expr, if *negated { "NOT " } else { "" }, pattern, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 612144a6..32f8a97e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3654,9 +3654,9 @@ impl<'a> Parser<'a> { } /// Parse the `ESCAPE CHAR` portion of `LIKE`, `ILIKE`, and `SIMILAR TO` - pub fn parse_escape_char(&mut self) -> Result, ParserError> { + pub fn parse_escape_char(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::ESCAPE) { - Ok(Some(self.parse_literal_string()?)) + Ok(Some(self.parse_value()?.into())) } else { Ok(None) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac461bb2..1bdd302e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2040,7 +2040,7 @@ fn parse_ilike() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -2104,7 +2104,7 @@ fn parse_like() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), any: false, }, select.selection.unwrap() @@ -2167,7 +2167,24 @@ fn parse_similar_to() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), + }, + select.selection.unwrap() + ); + + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE NULL", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), + escape_char: Some(Value::Null), }, select.selection.unwrap() ); @@ -2185,7 +2202,7 @@ fn parse_similar_to() { pattern: Box::new(Expr::Value( (Value::SingleQuotedString("%a".to_string())).with_empty_span() )), - escape_char: Some('^'.to_string()), + escape_char: Some(Value::SingleQuotedString('^'.to_string())), })), select.selection.unwrap() ); From d2466af20a5336c1554bdb82b0c84d04a20db26e Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:18:58 +0200 Subject: [PATCH 28/39] Add support for dropping multiple columns in Snowflake (#1918) --- src/ast/ddl.rs | 8 ++++---- src/ast/spans.rs | 4 ++-- src/dialect/mod.rs | 5 +++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 8 ++++++-- tests/sqlparser_common.rs | 7 +++++-- tests/sqlparser_mysql.rs | 4 ++-- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 7e46a59f..51e05784 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -140,10 +140,10 @@ pub enum AlterTableOperation { name: Ident, drop_behavior: Option, }, - /// `DROP [ COLUMN ] [ IF EXISTS ] [ CASCADE ]` + /// `DROP [ COLUMN ] [ IF EXISTS ] [ , , ... ] [ CASCADE ]` DropColumn { has_column_keyword: bool, - column_name: Ident, + column_names: Vec, if_exists: bool, drop_behavior: Option, }, @@ -631,7 +631,7 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::DropIndex { name } => write!(f, "DROP INDEX {name}"), AlterTableOperation::DropColumn { has_column_keyword, - column_name, + column_names: column_name, if_exists, drop_behavior, } => write!( @@ -639,7 +639,7 @@ impl fmt::Display for AlterTableOperation { "DROP {}{}{}{}", if *has_column_keyword { "COLUMN " } else { "" }, if *if_exists { "IF EXISTS " } else { "" }, - column_name, + display_comma_separated(column_name), match drop_behavior { None => "", Some(DropBehavior::Restrict) => " RESTRICT", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1d790ab6..144de592 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1112,10 +1112,10 @@ impl Spanned for AlterTableOperation { } => name.span, AlterTableOperation::DropColumn { has_column_keyword: _, - column_name, + column_names, if_exists: _, drop_behavior: _, - } => column_name.span, + } => union_spans(column_names.iter().map(|i| i.span)), AlterTableOperation::AttachPartition { partition } => partition.span(), AlterTableOperation::DetachPartition { partition } => partition.span(), AlterTableOperation::FreezePartition { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3345380c..bc3c5555 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1071,6 +1071,11 @@ pub trait Dialect: Debug + Any { fn supports_alter_column_type_using(&self) -> bool { false } + + /// Returns true if the dialect supports `ALTER TABLE tbl DROP COLUMN c1, ..., cn` + fn supports_comma_separated_drop_column_list(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ee770b0f..f56db9b8 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -364,6 +364,10 @@ impl Dialect for SnowflakeDialect { fn supports_space_separated_column_options(&self) -> bool { true } + + fn supports_comma_separated_drop_column_list(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 32f8a97e..839d3645 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8675,11 +8675,15 @@ impl<'a> Parser<'a> { } else { let has_column_keyword = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let column_name = self.parse_identifier()?; + let column_names = if self.dialect.supports_comma_separated_drop_column_list() { + self.parse_comma_separated(Parser::parse_identifier)? + } else { + vec![self.parse_identifier()?] + }; let drop_behavior = self.parse_optional_drop_behavior(); AlterTableOperation::DropColumn { has_column_keyword, - column_name, + column_names, if_exists, drop_behavior, } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1bdd302e..ed9bb704 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4996,15 +4996,18 @@ fn parse_alter_table_drop_column() { "ALTER TABLE tab DROP is_active CASCADE", ); + let dialects = all_dialects_where(|d| d.supports_comma_separated_drop_column_list()); + dialects.verified_stmt("ALTER TABLE tbl DROP COLUMN c1, c2, c3"); + fn check_one(constraint_text: &str) { match alter_table_op(verified_stmt(&format!("ALTER TABLE tab {constraint_text}"))) { AlterTableOperation::DropColumn { has_column_keyword: true, - column_name, + column_names, if_exists, drop_behavior, } => { - assert_eq!("is_active", column_name.to_string()); + assert_eq!("is_active", column_names.first().unwrap().to_string()); assert!(if_exists); match drop_behavior { None => assert!(constraint_text.ends_with(" is_active")), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 9224a003..79e2259b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2876,7 +2876,7 @@ fn parse_alter_table_with_algorithm() { vec![ AlterTableOperation::DropColumn { has_column_keyword: true, - column_name: Ident::new("password_digest"), + column_names: vec![Ident::new("password_digest")], if_exists: false, drop_behavior: None, }, @@ -2924,7 +2924,7 @@ fn parse_alter_table_with_lock() { vec![ AlterTableOperation::DropColumn { has_column_keyword: true, - column_name: Ident::new("password_digest"), + column_names: vec![Ident::new("password_digest")], if_exists: false, drop_behavior: None, }, From ed8757f2f0596c3e27b7f878e012bfc4d6da32a2 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:40:35 +0200 Subject: [PATCH 29/39] Align Snowflake dialect to new test of reserved keywords (#1924) --- src/dialect/snowflake.rs | 5 ++--- tests/sqlparser_snowflake.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f56db9b8..212cf217 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -301,9 +301,8 @@ impl Dialect for SnowflakeDialect { true } - fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { - explicit - || match kw { + fn is_column_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { // The following keywords can be considered an alias as long as // they are not followed by other tokens that may change their meaning // e.g. `SELECT * EXCEPT (col1) FROM tbl` diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 8a1558b2..e7393d3f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3415,10 +3415,38 @@ fn parse_ls_and_rm() { .unwrap(); } +#[test] +fn test_sql_keywords_as_select_item_ident() { + // Some keywords that should be parsed as an alias + let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT", "SORT"]; + for kw in unreserved_kws { + snowflake().verified_stmt(&format!("SELECT 1, {kw}")); + } + + // Some keywords that should not be parsed as an alias + let reserved_kws = vec![ + "FROM", + "GROUP", + "HAVING", + "INTERSECT", + "INTO", + "ORDER", + "SELECT", + "UNION", + "WHERE", + "WITH", + ]; + for kw in reserved_kws { + assert!(snowflake() + .parse_sql_statements(&format!("SELECT 1, {kw}")) + .is_err()); + } +} + #[test] fn test_sql_keywords_as_select_item_aliases() { // Some keywords that should be parsed as an alias - let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT"]; + let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT", "SORT"]; for kw in unreserved_kws { snowflake() .one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {kw}")); From cf9e50474ec6c59027bcc3e2f418c5ff6947920a Mon Sep 17 00:00:00 2001 From: Simon Vandel Sillesen Date: Sun, 6 Jul 2025 08:57:20 +0200 Subject: [PATCH 30/39] Make `GenericDialect` support trailing commas in projections (#1921) --- src/dialect/generic.rs | 4 ++++ tests/sqlparser_common.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 59671e21..5e9f2e4e 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -116,6 +116,10 @@ impl Dialect for GenericDialect { true } + fn supports_projection_trailing_commas(&self) -> bool { + true + } + fn supports_asc_desc_in_column_definition(&self) -> bool { true } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ed9bb704..56b015fc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11184,7 +11184,7 @@ fn parse_trailing_comma() { trailing_commas.verified_stmt(r#"SELECT "from" FROM "from""#); // doesn't allow any trailing commas - let trailing_commas = TestedDialects::new(vec![Box::new(GenericDialect {})]); + let trailing_commas = TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]); assert_eq!( trailing_commas From f2fba48a7a7c2cba43baafa3c87b9f7abe85c869 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 6 Jul 2025 08:58:19 +0200 Subject: [PATCH 31/39] Add support for several Snowflake grant statements (#1922) --- src/ast/mod.rs | 54 ++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 58 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 6 ++++ 3 files changed, 118 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 425e1fb6..93ef7663 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6928,12 +6928,24 @@ pub enum GrantObjects { AllSequencesInSchema { schemas: Vec }, /// Grant privileges on `ALL TABLES IN SCHEMA [, ...]` AllTablesInSchema { schemas: Vec }, + /// Grant privileges on `ALL VIEWS IN SCHEMA [, ...]` + AllViewsInSchema { schemas: Vec }, + /// Grant privileges on `ALL MATERIALIZED VIEWS IN SCHEMA [, ...]` + AllMaterializedViewsInSchema { schemas: Vec }, + /// Grant privileges on `ALL EXTERNAL TABLES IN SCHEMA [, ...]` + AllExternalTablesInSchema { schemas: Vec }, /// Grant privileges on `FUTURE SCHEMAS IN DATABASE [, ...]` FutureSchemasInDatabase { databases: Vec }, /// Grant privileges on `FUTURE TABLES IN SCHEMA [, ...]` FutureTablesInSchema { schemas: Vec }, /// Grant privileges on `FUTURE VIEWS IN SCHEMA [, ...]` FutureViewsInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE EXTERNAL TABLES IN SCHEMA [, ...]` + FutureExternalTablesInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE MATERIALIZED VIEWS IN SCHEMA [, ...]` + FutureMaterializedViewsInSchema { schemas: Vec }, + /// Grant privileges on `FUTURE SEQUENCES IN SCHEMA [, ...]` + FutureSequencesInSchema { schemas: Vec }, /// Grant privileges on specific databases Databases(Vec), /// Grant privileges on specific schemas @@ -7002,6 +7014,27 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::AllExternalTablesInSchema { schemas } => { + write!( + f, + "ALL EXTERNAL TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllViewsInSchema { schemas } => { + write!( + f, + "ALL VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllMaterializedViewsInSchema { schemas } => { + write!( + f, + "ALL MATERIALIZED VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::FutureSchemasInDatabase { databases } => { write!( f, @@ -7016,6 +7049,13 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::FutureExternalTablesInSchema { schemas } => { + write!( + f, + "FUTURE EXTERNAL TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::FutureViewsInSchema { schemas } => { write!( f, @@ -7023,6 +7063,20 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::FutureMaterializedViewsInSchema { schemas } => { + write!( + f, + "FUTURE MATERIALIZED VIEWS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureSequencesInSchema { schemas } => { + write!( + f, + "FUTURE SEQUENCES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::ResourceMonitors(objects) => { write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 839d3645..a94c7b43 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13901,6 +13901,35 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllTablesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::EXTERNAL, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllExternalTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::MATERIALIZED, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllMaterializedViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::FUTURE, Keyword::SCHEMAS, @@ -13919,6 +13948,16 @@ impl<'a> Parser<'a> { Some(GrantObjects::FutureTablesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::EXTERNAL, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureExternalTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::FUTURE, Keyword::VIEWS, @@ -13928,6 +13967,16 @@ impl<'a> Parser<'a> { Some(GrantObjects::FutureViewsInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::MATERIALIZED, + Keyword::VIEWS, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureMaterializedViewsInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::ALL, Keyword::SEQUENCES, @@ -13937,6 +13986,15 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllSequencesInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::FUTURE, + Keyword::SEQUENCES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::FutureSequencesInSchema { + 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), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 56b015fc..0d3c4d82 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9434,6 +9434,9 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO APPLICATION role1"); verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO APPLICATION ROLE role1"); verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO SHARE share1"); + verified_stmt("GRANT SELECT ON ALL VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON ALL MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON ALL EXTERNAL TABLES IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO a:b"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO GROUP group1"); verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST"); @@ -9447,7 +9450,10 @@ fn parse_grant() { .verified_stmt("GRANT SELECT ON [my_table] TO [public]"); verified_stmt("GRANT SELECT ON FUTURE SCHEMAS IN DATABASE db1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE EXTERNAL TABLES IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON FUTURE SEQUENCES IN SCHEMA db1.sc1 TO ROLE role1"); } #[test] From 1a33abda63658f19902026df99826cce74812c02 Mon Sep 17 00:00:00 2001 From: Sergey Olontsev Date: Sun, 6 Jul 2025 08:06:20 +0100 Subject: [PATCH 32/39] Clickhouse: support empty parenthesized options (#1925) --- src/parser/mod.rs | 4 ++-- tests/sqlparser_clickhouse.rs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a94c7b43..61944b8f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16253,9 +16253,9 @@ impl<'a> Parser<'a> { fn parse_parenthesized_identifiers(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(|p| p.parse_identifier())?; + let idents = self.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; self.expect_token(&Token::RParen)?; - Ok(partitions) + Ok(idents) } fn parse_column_position(&mut self) -> Result, ParserError> { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0288c6d2..1d8669a2 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -224,6 +224,10 @@ fn parse_create_table() { clickhouse().verified_stmt( r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#, ); + clickhouse().one_statement_parses_to( + "CREATE TABLE x (a int) ENGINE = MergeTree() ORDER BY a", + "CREATE TABLE x (a INT) ENGINE = MergeTree ORDER BY a", + ); } #[test] From 93450cc2505388f546623c9807f1320acf4fe880 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:13:57 +0200 Subject: [PATCH 33/39] Add Snowflake `COPY/REVOKE CURRENT GRANTS` option (#1926) --- src/ast/mod.rs | 27 +++++++++++++++++++++++++++ src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_common.rs | 2 ++ tests/sqlparser_mysql.rs | 1 + 4 files changed, 40 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 93ef7663..8da6bbe7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3996,6 +3996,7 @@ pub enum Statement { with_grant_option: bool, as_grantor: Option, granted_by: Option, + current_grants: Option, }, /// ```sql /// DENY privileges ON object TO grantees @@ -4312,6 +4313,28 @@ pub enum Statement { Return(ReturnStatement), } +/// ```sql +/// {COPY | REVOKE} CURRENT GRANTS +/// ``` +/// +/// - [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#optional-parameters) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CurrentGrantsKind { + CopyCurrentGrants, + RevokeCurrentGrants, +} + +impl fmt::Display for CurrentGrantsKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CurrentGrantsKind::CopyCurrentGrants => write!(f, "COPY CURRENT GRANTS"), + CurrentGrantsKind::RevokeCurrentGrants => write!(f, "REVOKE CURRENT GRANTS"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5715,6 +5738,7 @@ impl fmt::Display for Statement { with_grant_option, as_grantor, granted_by, + current_grants, } => { write!(f, "GRANT {privileges} ")?; if let Some(objects) = objects { @@ -5724,6 +5748,9 @@ impl fmt::Display for Statement { if *with_grant_option { write!(f, " WITH GRANT OPTION")?; } + if let Some(current_grants) = current_grants { + write!(f, " {current_grants}")?; + } if let Some(grantor) = as_grantor { write!(f, " AS {grantor}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 61944b8f..c4d0508d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13794,6 +13794,15 @@ impl<'a> Parser<'a> { let with_grant_option = self.parse_keywords(&[Keyword::WITH, Keyword::GRANT, Keyword::OPTION]); + let current_grants = + if self.parse_keywords(&[Keyword::COPY, Keyword::CURRENT, Keyword::GRANTS]) { + Some(CurrentGrantsKind::CopyCurrentGrants) + } else if self.parse_keywords(&[Keyword::REVOKE, Keyword::CURRENT, Keyword::GRANTS]) { + Some(CurrentGrantsKind::RevokeCurrentGrants) + } else { + None + }; + let as_grantor = if self.parse_keywords(&[Keyword::AS]) { Some(self.parse_identifier()?) } else { @@ -13813,6 +13822,7 @@ impl<'a> Parser<'a> { with_grant_option, as_grantor, granted_by, + current_grants, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0d3c4d82..27fe09c7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9440,6 +9440,8 @@ fn parse_grant() { verified_stmt("GRANT USAGE ON SCHEMA sc1 TO a:b"); verified_stmt("GRANT USAGE ON SCHEMA sc1 TO GROUP group1"); verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST"); + verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST COPY CURRENT GRANTS"); + verified_stmt("GRANT OWNERSHIP ON ALL TABLES IN SCHEMA DEV_STAS_ROGOZHIN TO ROLE ANALYST REVOKE CURRENT GRANTS"); verified_stmt("GRANT USAGE ON DATABASE db1 TO ROLE role1"); verified_stmt("GRANT USAGE ON WAREHOUSE wh1 TO ROLE role1"); verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1"); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 79e2259b..44f36584 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3616,6 +3616,7 @@ fn parse_grant() { with_grant_option, as_grantor: _, granted_by, + current_grants: _, } = stmt { assert_eq!( From b1a6d11e123ff5432d24e4baf3028176649692f3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:15:50 +0200 Subject: [PATCH 34/39] Add support for Snowflake identifier function (#1929) --- src/ast/mod.rs | 22 +++++ src/ast/spans.rs | 4 + src/dialect/mod.rs | 15 +++- src/dialect/snowflake.rs | 21 ++++- src/parser/mod.rs | 152 ++++++++++++++++++++--------------- tests/sqlparser_common.rs | 6 +- tests/sqlparser_snowflake.rs | 135 +++++++++++++++++++++++++++++++ 7 files changed, 286 insertions(+), 69 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8da6bbe7..619730e2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -344,12 +344,14 @@ impl fmt::Display for ObjectName { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ObjectNamePart { Identifier(Ident), + Function(ObjectNamePartFunction), } impl ObjectNamePart { pub fn as_ident(&self) -> Option<&Ident> { match self { ObjectNamePart::Identifier(ident) => Some(ident), + ObjectNamePart::Function(_) => None, } } } @@ -358,10 +360,30 @@ impl fmt::Display for ObjectNamePart { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ObjectNamePart::Identifier(ident) => write!(f, "{ident}"), + ObjectNamePart::Function(func) => write!(f, "{func}"), } } } +/// An object name part that consists of a function that dynamically +/// constructs identifiers. +/// +/// - [Snowflake](https://docs.snowflake.com/en/sql-reference/identifier-literal) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ObjectNamePartFunction { + pub name: Ident, + pub args: Vec, +} + +impl fmt::Display for ObjectNamePartFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}(", self.name)?; + write!(f, "{})", display_comma_separated(&self.args)) + } +} + /// Represents an Array Expression, either /// `ARRAY[..]`, or `[..]` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 144de592..a1b2e4e0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1671,6 +1671,10 @@ impl Spanned for ObjectNamePart { fn span(&self) -> Span { match self { ObjectNamePart::Identifier(ident) => ident.span, + ObjectNamePart::Function(func) => func + .name + .span + .union(&union_spans(func.args.iter().map(|i| i.span()))), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bc3c5555..8f9dd617 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -49,7 +49,7 @@ pub use self::postgresql::PostgreSqlDialect; pub use self::redshift::RedshiftSqlDialect; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; -use crate::ast::{ColumnOption, Expr, GranteesType, Statement}; +use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement}; pub use crate::keywords; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -1076,6 +1076,19 @@ pub trait Dialect: Debug + Any { fn supports_comma_separated_drop_column_list(&self) -> bool { false } + + /// Returns true if the dialect considers the specified ident as a function + /// that returns an identifier. Typically used to generate identifiers + /// programmatically. + /// + /// - [Snowflake](https://docs.snowflake.com/en/sql-reference/identifier-literal) + fn is_identifier_generating_function_name( + &self, + _ident: &Ident, + _name_parts: &[ObjectNamePart], + ) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 212cf217..e005300e 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -25,8 +25,8 @@ use crate::ast::helpers::stmt_data_loading::{ use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, SqlOption, Statement, - TagsColumnOption, WrappedCollection, + IdentityPropertyOrder, ObjectName, ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption, + Statement, TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -367,6 +367,23 @@ impl Dialect for SnowflakeDialect { fn supports_comma_separated_drop_column_list(&self) -> bool { true } + + fn is_identifier_generating_function_name( + &self, + ident: &Ident, + name_parts: &[ObjectNamePart], + ) -> bool { + ident.quote_style.is_none() + && ident.value.to_lowercase() == "identifier" + && !name_parts + .iter() + .any(|p| matches!(p, ObjectNamePart::Function(_))) + } + + // For example: `SELECT IDENTIFIER('alias1').* FROM tbl AS alias1` + fn supports_select_expr_star(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c4d0508d..5a42bb9e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10353,49 +10353,14 @@ impl<'a> Parser<'a> { } } - /// Parse a possibly qualified, possibly quoted identifier, optionally allowing for wildcards, - /// e.g. *, *.*, `foo`.*, or "foo"."bar" - fn parse_object_name_with_wildcards( - &mut self, - in_table_clause: bool, - allow_wildcards: bool, - ) -> Result { - let mut idents = vec![]; - - if dialect_of!(self is BigQueryDialect) && in_table_clause { - loop { - let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; - idents.push(ident); - if !self.consume_token(&Token::Period) && !end_with_period { - break; - } - } - } else { - loop { - let ident = if allow_wildcards && self.peek_token().token == Token::Mul { - let span = self.next_token().span; - Ident { - value: Token::Mul.to_string(), - quote_style: None, - span, - } - } else { - if self.dialect.supports_object_name_double_dot_notation() - && idents.len() == 1 - && self.consume_token(&Token::Period) - { - // Empty string here means default schema - idents.push(Ident::new("")); - } - self.parse_identifier()? - }; - idents.push(ident); - if !self.consume_token(&Token::Period) { - break; - } - } - } - Ok(ObjectName::from(idents)) + /// Parse a possibly qualified, possibly quoted identifier, e.g. + /// `foo` or `myschema."table" + /// + /// The `in_table_clause` parameter indicates whether the object name is a table in a FROM, JOIN, + /// or similar table clause. Currently, this is used only to support unquoted hyphenated identifiers + /// in this context on BigQuery. + pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { + self.parse_object_name_inner(in_table_clause, false) } /// Parse a possibly qualified, possibly quoted identifier, e.g. @@ -10404,19 +10369,76 @@ impl<'a> Parser<'a> { /// The `in_table_clause` parameter indicates whether the object name is a table in a FROM, JOIN, /// or similar table clause. Currently, this is used only to support unquoted hyphenated identifiers /// in this context on BigQuery. - pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { - let ObjectName(mut idents) = - self.parse_object_name_with_wildcards(in_table_clause, false)?; + /// + /// The `allow_wildcards` parameter indicates whether to allow for wildcards in the object name + /// e.g. *, *.*, `foo`.*, or "foo"."bar" + fn parse_object_name_inner( + &mut self, + in_table_clause: bool, + allow_wildcards: bool, + ) -> Result { + let mut parts = vec![]; + if dialect_of!(self is BigQueryDialect) && in_table_clause { + loop { + let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; + parts.push(ObjectNamePart::Identifier(ident)); + if !self.consume_token(&Token::Period) && !end_with_period { + break; + } + } + } else { + loop { + if allow_wildcards && self.peek_token().token == Token::Mul { + let span = self.next_token().span; + parts.push(ObjectNamePart::Identifier(Ident { + value: Token::Mul.to_string(), + quote_style: None, + span, + })); + } else if dialect_of!(self is BigQueryDialect) && in_table_clause { + let (ident, end_with_period) = self.parse_unquoted_hyphenated_identifier()?; + parts.push(ObjectNamePart::Identifier(ident)); + if !self.consume_token(&Token::Period) && !end_with_period { + break; + } + } else if self.dialect.supports_object_name_double_dot_notation() + && parts.len() == 1 + && matches!(self.peek_token().token, Token::Period) + { + // Empty string here means default schema + parts.push(ObjectNamePart::Identifier(Ident::new(""))); + } else { + let ident = self.parse_identifier()?; + let part = if self + .dialect + .is_identifier_generating_function_name(&ident, &parts) + { + self.expect_token(&Token::LParen)?; + let args: Vec = + self.parse_comma_separated0(Self::parse_function_args, Token::RParen)?; + self.expect_token(&Token::RParen)?; + ObjectNamePart::Function(ObjectNamePartFunction { name: ident, args }) + } else { + ObjectNamePart::Identifier(ident) + }; + parts.push(part); + } + + if !self.consume_token(&Token::Period) { + 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(|part| { + && parts.iter().any(|part| { part.as_ident() .is_some_and(|ident| ident.value.contains('.')) }) { - idents = idents + parts = parts .into_iter() .flat_map(|part| match part.as_ident() { Some(ident) => ident @@ -10435,7 +10457,7 @@ impl<'a> Parser<'a> { .collect() } - Ok(ObjectName(idents)) + Ok(ObjectName(parts)) } /// Parse identifiers @@ -14006,25 +14028,25 @@ impl<'a> Parser<'a> { 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), - )?)) + Some(GrantObjects::ResourceMonitors( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } 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), - )?)) + Some(GrantObjects::ComputePools( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } 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), - )?)) + Some(GrantObjects::FailoverGroup( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } 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), - )?)) + Some(GrantObjects::ReplicationGroup( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } 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), - )?)) + Some(GrantObjects::ExternalVolumes( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )) } else { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, @@ -14041,7 +14063,7 @@ impl<'a> Parser<'a> { Keyword::CONNECTION, ]); let objects = - self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); + self.parse_comma_separated(|p| p.parse_object_name_inner(false, true)); match object_type { Some(Keyword::DATABASE) => Some(GrantObjects::Databases(objects?)), Some(Keyword::SCHEMA) => Some(GrantObjects::Schemas(objects?)), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 27fe09c7..1de1d93f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1232,7 +1232,6 @@ fn parse_select_expr_star() { "SELECT 2. * 3 FROM T", ); dialects.verified_only_select("SELECT myfunc().* FROM T"); - dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T"); // Invalid let res = dialects.parse_sql_statements("SELECT foo.*.* FROM T"); @@ -1240,6 +1239,11 @@ fn parse_select_expr_star() { ParserError::ParserError("Expected: end of statement, found: .".to_string()), res.unwrap_err() ); + + let dialects = all_dialects_where(|d| { + d.supports_select_expr_star() && d.supports_select_wildcard_except() + }); + dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T"); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e7393d3f..6081e318 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4232,3 +4232,138 @@ fn test_snowflake_create_view_with_composite_policy_name() { r#"CREATE VIEW X (COL WITH MASKING POLICY foo.bar.baz) AS SELECT * FROM Y"#; snowflake().verified_stmt(create_view_with_tag); } + +#[test] +fn test_snowflake_identifier_function() { + // Using IDENTIFIER to reference a column + match &snowflake() + .verified_only_select("SELECT identifier('email') FROM customers") + .projection[0] + { + SelectItem::UnnamedExpr(Expr::Function(Function { name, args, .. })) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("email".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a case-sensitive column + match &snowflake() + .verified_only_select(r#"SELECT identifier('"Email"') FROM customers"#) + .projection[0] + { + SelectItem::UnnamedExpr(Expr::Function(Function { name, args, .. })) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("\"Email\"".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference an alias of a table + match &snowflake() + .verified_only_select("SELECT identifier('alias1').* FROM tbl AS alias1") + .projection[0] + { + SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(Expr::Function(Function { name, args, .. })), + _, + ) => { + assert_eq!(*name, ObjectName::from(vec![Ident::new("identifier")])); + assert_eq!( + *args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("alias1".to_string()).into() + )))], + clauses: vec![], + duplicate_treatment: None + }) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a database + match snowflake().verified_stmt("CREATE DATABASE IDENTIFIER('tbl')") { + Statement::CreateDatabase { db_name, .. } => { + assert_eq!( + db_name, + ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("tbl".to_string()).into() + )))] + })]) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a schema + match snowflake().verified_stmt("CREATE SCHEMA IDENTIFIER('db1.sc1')") { + Statement::CreateSchema { schema_name, .. } => { + assert_eq!( + schema_name, + SchemaName::Simple(ObjectName(vec![ObjectNamePart::Function( + ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("db1.sc1".to_string()).into() + )))] + } + )])) + ); + } + _ => unreachable!(), + } + + // Using IDENTIFIER to reference a table + match snowflake().verified_stmt("CREATE TABLE IDENTIFIER('tbl') (id INT)") { + Statement::CreateTable(CreateTable { name, .. }) => { + assert_eq!( + name, + ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction { + name: Ident::new("IDENTIFIER"), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("tbl".to_string()).into() + )))] + })]) + ); + } + _ => unreachable!(), + } + + // Cannot have more than one IDENTIFIER part in an object name + assert_eq!( + snowflake() + .parse_sql_statements( + "CREATE TABLE IDENTIFIER('db1').IDENTIFIER('sc1').IDENTIFIER('tbl') (id INT)" + ) + .is_err(), + true + ); + assert_eq!( + snowflake() + .parse_sql_statements("CREATE TABLE IDENTIFIER('db1')..IDENTIFIER('tbl') (id INT)") + .is_err(), + true + ); +} From 8f1414efffa530af17c0853a8de4f0f0c73edacc Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:28:51 +0200 Subject: [PATCH 35/39] Add support for granting privileges to procedures and functions in Snowflake (#1930) --- src/ast/mod.rs | 33 +++++++++++++++++++++++++++++++++ src/parser/mod.rs | 34 ++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 2 ++ 3 files changed, 69 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 619730e2..75e88f8a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7023,6 +7023,25 @@ pub enum GrantObjects { ReplicationGroup(Vec), /// Grant privileges on external volumes ExternalVolumes(Vec), + /// Grant privileges on a procedure. In dialects that + /// support overloading, the argument types must be specified. + /// + /// For example: + /// `GRANT USAGE ON PROCEDURE foo(varchar) TO ROLE role1` + Procedure { + name: ObjectName, + arg_types: Vec, + }, + + /// Grant privileges on a function. In dialects that + /// support overloading, the argument types must be specified. + /// + /// For example: + /// `GRANT USAGE ON FUNCTION foo(varchar) TO ROLE role1` + Function { + name: ObjectName, + arg_types: Vec, + }, } impl fmt::Display for GrantObjects { @@ -7147,6 +7166,20 @@ impl fmt::Display for GrantObjects { GrantObjects::ExternalVolumes(objects) => { write!(f, "EXTERNAL VOLUME {}", display_comma_separated(objects)) } + GrantObjects::Procedure { name, arg_types } => { + write!(f, "PROCEDURE {name}")?; + if !arg_types.is_empty() { + write!(f, "({})", display_comma_separated(arg_types))?; + } + Ok(()) + } + GrantObjects::Function { name, arg_types } => { + write!(f, "FUNCTION {name}")?; + if !arg_types.is_empty() { + write!(f, "({})", display_comma_separated(arg_types))?; + } + Ok(()) + } } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5a42bb9e..b00cd16d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14061,6 +14061,8 @@ impl<'a> Parser<'a> { Keyword::INTEGRATION, Keyword::USER, Keyword::CONNECTION, + Keyword::PROCEDURE, + Keyword::FUNCTION, ]); let objects = self.parse_comma_separated(|p| p.parse_object_name_inner(false, true)); @@ -14073,6 +14075,13 @@ impl<'a> Parser<'a> { Some(Keyword::VIEW) => Some(GrantObjects::Views(objects?)), Some(Keyword::USER) => Some(GrantObjects::Users(objects?)), Some(Keyword::CONNECTION) => Some(GrantObjects::Connections(objects?)), + kw @ (Some(Keyword::PROCEDURE) | Some(Keyword::FUNCTION)) => { + if let Some(name) = objects?.first() { + self.parse_grant_procedure_or_function(name, &kw)? + } else { + self.expected("procedure or function name", self.peek_token())? + } + } Some(Keyword::TABLE) | None => Some(GrantObjects::Tables(objects?)), _ => unreachable!(), } @@ -14084,6 +14093,31 @@ impl<'a> Parser<'a> { Ok((privileges, objects)) } + fn parse_grant_procedure_or_function( + &mut self, + name: &ObjectName, + kw: &Option, + ) -> Result, ParserError> { + let arg_types = if self.consume_token(&Token::LParen) { + let list = self.parse_comma_separated0(Self::parse_data_type, Token::RParen)?; + self.expect_token(&Token::RParen)?; + list + } else { + vec![] + }; + match kw { + Some(Keyword::PROCEDURE) => Ok(Some(GrantObjects::Procedure { + name: name.clone(), + arg_types, + })), + Some(Keyword::FUNCTION) => Ok(Some(GrantObjects::Function { + name: name.clone(), + arg_types, + })), + _ => self.expected("procedure or function keywords", self.peek_token())?, + } + } + pub fn parse_grant_permission(&mut self) -> Result { fn parse_columns(parser: &mut Parser) -> Result>, ParserError> { let columns = parser.parse_parenthesized_column_list(Optional, false)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1de1d93f..a00405aa 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9460,6 +9460,8 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE MATERIALIZED VIEWS IN SCHEMA db1.sc1 TO ROLE role1"); verified_stmt("GRANT SELECT ON FUTURE SEQUENCES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT USAGE ON PROCEDURE db1.sc1.foo(INT) TO ROLE role1"); + verified_stmt("GRANT USAGE ON FUNCTION db1.sc1.foo(INT) TO ROLE role1"); } #[test] From fd4934ec74ab23cab6ef89750dc161bdeceff25e Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:01:08 +0200 Subject: [PATCH 36/39] Add support for `+` char in Snowflake stage names (#1935) --- src/dialect/snowflake.rs | 1 + tests/sqlparser_snowflake.rs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index e005300e..21bc9593 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -735,6 +735,7 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result ident.push('~'), Token::Mod => ident.push('%'), Token::Div => ident.push('/'), + Token::Plus => ident.push('+'), Token::Word(w) => ident.push_str(&w.to_string()), _ => return parser.expected("stage name identifier", parser.peek_token()), } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 6081e318..389f4703 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2581,6 +2581,26 @@ fn test_snowflake_copy_into() { } _ => unreachable!(), } + + // Test for non-ident characters in stage names + let sql = "COPY INTO a.b FROM @namespace.stage_name/x@x~x%x+"; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { into, from_obj, .. } => { + assert_eq!( + into, + ObjectName::from(vec![Ident::new("a"), Ident::new("b")]) + ); + assert_eq!( + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@namespace"), + Ident::new("stage_name/x@x~x%x+") + ])) + ) + } + _ => unreachable!(), + } } #[test] From 15f35e14765e58724e37704818a8c16426f2f145 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:13:20 +0200 Subject: [PATCH 37/39] Snowflake Reserved SQL Keywords as Implicit Table Alias (#1934) --- src/dialect/mod.rs | 10 +++- src/dialect/snowflake.rs | 88 ++++++++++++++++++++++++++++++++++-- tests/sqlparser_common.rs | 26 ++++++++--- tests/sqlparser_snowflake.rs | 51 +++++++++++++++++++++ 4 files changed, 163 insertions(+), 12 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8f9dd617..861dfe26 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -992,11 +992,17 @@ pub trait Dialect: Debug + Any { explicit || self.is_column_alias(kw, parser) } + /// Returns true if the specified keyword should be parsed as a table identifier. + /// See [keywords::RESERVED_FOR_TABLE_ALIAS] + fn is_table_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + } + /// Returns true if the specified keyword should be parsed as a table factor alias. /// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided /// to enable looking ahead if needed. - fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { - explicit || !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + explicit || self.is_table_alias(kw, parser) } /// Returns true if this dialect supports querying historical table data diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 21bc9593..85c09acb 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -318,9 +318,11 @@ impl Dialect for SnowflakeDialect { } // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` - // which would give it a different meanings, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias - Keyword::FETCH - if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) => + // which would give it a different meanings, for example: + // `SELECT 1 FETCH FIRST 10 ROWS` - not an alias + // `SELECT 1 FETCH 10` - not an alias + Keyword::FETCH if parser.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]).is_some() + || matches!(parser.peek_token().token, Token::Number(_, _)) => { false } @@ -345,6 +347,86 @@ impl Dialect for SnowflakeDialect { } } + fn is_table_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { + // The following keywords can be considered an alias as long as + // they are not followed by other tokens that may change their meaning + Keyword::LIMIT + | Keyword::RETURNING + | Keyword::INNER + | Keyword::USING + | Keyword::PIVOT + | Keyword::UNPIVOT + | Keyword::EXCEPT + | Keyword::MATCH_RECOGNIZE + | Keyword::OFFSET + if !matches!(parser.peek_token_ref().token, Token::SemiColon | Token::EOF) => + { + false + } + + // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` + // which would give it a different meanings, for example: + // `SELECT * FROM tbl FETCH FIRST 10 ROWS` - not an alias + // `SELECT * FROM tbl FETCH 10` - not an alias + Keyword::FETCH + if parser + .peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]) + .is_some() + || matches!(parser.peek_token().token, Token::Number(_, _)) => + { + false + } + + // All sorts of join-related keywords can be considered aliases unless additional + // keywords change their meaning. + Keyword::RIGHT | Keyword::LEFT | Keyword::SEMI | Keyword::ANTI + if parser + .peek_one_of_keywords(&[Keyword::JOIN, Keyword::OUTER]) + .is_some() => + { + false + } + Keyword::GLOBAL if parser.peek_keyword(Keyword::FULL) => false, + + // Reserved keywords by the Snowflake dialect, which seem to be less strictive + // than what is listed in `keywords::RESERVED_FOR_TABLE_ALIAS`. The following + // keywords were tested with the this statement: `SELECT .* FROM tbl `. + Keyword::WITH + | Keyword::ORDER + | Keyword::SELECT + | Keyword::WHERE + | Keyword::GROUP + | Keyword::HAVING + | Keyword::LATERAL + | Keyword::UNION + | Keyword::INTERSECT + | Keyword::MINUS + | Keyword::ON + | Keyword::JOIN + | Keyword::INNER + | Keyword::CROSS + | Keyword::FULL + | Keyword::LEFT + | Keyword::RIGHT + | Keyword::NATURAL + | Keyword::USING + | Keyword::ASOF + | Keyword::MATCH_CONDITION + | Keyword::SET + | Keyword::QUALIFY + | Keyword::FOR + | Keyword::START + | Keyword::CONNECT + | Keyword::SAMPLE + | Keyword::TABLESAMPLE + | Keyword::FROM => false, + + // Any other word is considered an alias + _ => true, + } + } + /// See: fn supports_timestamp_versioning(&self) -> bool { true diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a00405aa..906be9be 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5548,7 +5548,8 @@ fn parse_named_window_functions() { WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)"; supported_dialects.verified_stmt(sql); - let select = verified_only_select(sql); + let select = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))) + .verified_only_select(sql); const EXPECTED_PROJ_QTY: usize = 2; assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); @@ -5578,6 +5579,7 @@ fn parse_named_window_functions() { #[test] fn parse_window_clause() { + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))); let sql = "SELECT * \ FROM mytable \ WINDOW \ @@ -5590,10 +5592,14 @@ fn parse_window_clause() { window7 AS (window1 ROWS UNBOUNDED PRECEDING), \ window8 AS (window1 PARTITION BY a ORDER BY b ROWS UNBOUNDED PRECEDING) \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); let sql = "SELECT * from mytable WINDOW window1 AS window2"; - let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let dialects = all_dialects_except(|d| { + d.is::() + || d.is::() + || d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)) + }); let res = dialects.parse_sql_statements(sql); assert_eq!( ParserError::ParserError("Expected: (, found: window2".to_string()), @@ -5603,6 +5609,7 @@ fn parse_window_clause() { #[test] fn test_parse_named_window() { + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))); let sql = "SELECT \ MIN(c12) OVER window1 AS min1, \ MAX(c12) OVER window2 AS max1 \ @@ -5610,7 +5617,7 @@ fn test_parse_named_window() { WINDOW window1 AS (ORDER BY C12), \ window2 AS (PARTITION BY C11) \ ORDER BY C3"; - let actual_select_only = verified_only_select(sql); + let actual_select_only = dialects.verified_only_select(sql); let expected = Select { select_token: AttachedToken::empty(), distinct: None, @@ -5759,6 +5766,10 @@ fn test_parse_named_window() { #[test] fn parse_window_and_qualify_clause() { + let dialects = all_dialects_except(|d| { + d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)) + || d.is_table_alias(&Keyword::QUALIFY, &mut Parser::new(d)) + }); let sql = "SELECT \ MIN(c12) OVER window1 AS min1 \ FROM aggregate_test_100 \ @@ -5766,7 +5777,7 @@ fn parse_window_and_qualify_clause() { WINDOW window1 AS (ORDER BY C12), \ window2 AS (PARTITION BY C11) \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); let sql = "SELECT \ MIN(c12) OVER window1 AS min1 \ @@ -5775,7 +5786,7 @@ fn parse_window_and_qualify_clause() { window2 AS (PARTITION BY C11) \ QUALIFY ROW_NUMBER() OVER my_window \ ORDER BY C3"; - verified_only_select(sql); + dialects.verified_only_select(sql); } #[test] @@ -7443,7 +7454,8 @@ fn parse_join_syntax_variants() { "SELECT c1 FROM t1 FULL JOIN t2 USING(c1)", ); - let res = parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); + let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::OUTER, &mut Parser::new(d))); + let res = dialects.parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); assert_eq!( ParserError::ParserError("Expected: APPLY, found: JOIN".to_string()), res.unwrap_err() diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 389f4703..65546bee 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3492,6 +3492,57 @@ fn test_sql_keywords_as_select_item_aliases() { } } +#[test] +fn test_sql_keywords_as_table_aliases() { + // Some keywords that should be parsed as an alias implicitly + let unreserved_kws = vec![ + "VIEW", + "EXPLAIN", + "ANALYZE", + "SORT", + "PIVOT", + "UNPIVOT", + "TOP", + "LIMIT", + "OFFSET", + "FETCH", + "EXCEPT", + "CLUSTER", + "DISTRIBUTE", + "GLOBAL", + "ANTI", + "SEMI", + "RETURNING", + "OUTER", + "WINDOW", + "END", + "PARTITION", + "PREWHERE", + "SETTINGS", + "FORMAT", + "MATCH_RECOGNIZE", + "OPEN", + ]; + + for kw in unreserved_kws { + snowflake().verified_stmt(&format!("SELECT * FROM tbl AS {kw}")); + snowflake().one_statement_parses_to( + &format!("SELECT * FROM tbl {kw}"), + &format!("SELECT * FROM tbl AS {kw}"), + ); + } + + // Some keywords that should not be parsed as an alias implicitly + let reserved_kws = vec![ + "FROM", "GROUP", "HAVING", "ORDER", "SELECT", "UNION", "WHERE", "WITH", + ]; + for kw in reserved_kws { + assert!(snowflake() + .parse_sql_statements(&format!("SELECT * FROM tbl {kw}")) + .is_err()); + } +} + #[test] fn test_timetravel_at_before() { snowflake().verified_only_select("SELECT * FROM tbl AT(TIMESTAMP => '2024-12-15 00:00:00')"); From ee31b64f9e9c0959aa6fcada159d8092517966e3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:39:29 +0200 Subject: [PATCH 38/39] Add support for Redshift `SELECT * EXCLUDE` (#1936) --- src/ast/query.rs | 9 +++ src/ast/spans.rs | 1 + src/dialect/duckdb.rs | 4 ++ src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 20 ++++++ src/dialect/redshift.rs | 8 +++ src/dialect/snowflake.rs | 4 ++ src/keywords.rs | 1 + src/parser/mod.rs | 11 +++- tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 115 ++++++++++++++++++++++++++++++++++ tests/sqlparser_duckdb.rs | 2 + tests/sqlparser_mssql.rs | 3 + tests/sqlparser_mysql.rs | 9 ++- tests/sqlparser_postgres.rs | 3 + 15 files changed, 192 insertions(+), 3 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index febf1fc6..7ffb64d9 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -321,6 +321,11 @@ pub struct Select { pub top_before_distinct: bool, /// projection expressions pub projection: Vec, + /// Excluded columns from the projection expression which are not specified + /// directly after a wildcard. + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html) + pub exclude: Option, /// INTO pub into: Option, /// FROM @@ -401,6 +406,10 @@ impl fmt::Display for Select { indented_list(f, &self.projection)?; } + if let Some(exclude) = &self.exclude { + write!(f, " {exclude}")?; + } + if let Some(ref into) = self.into { f.write_str(" ")?; into.fmt(f)?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a1b2e4e0..3e82905e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2220,6 +2220,7 @@ impl Spanned for Select { distinct: _, // todo top: _, // todo, mysql specific projection, + exclude: _, into, from, lateral_views, diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 3366c670..fa18463a 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -94,4 +94,8 @@ impl Dialect for DuckDbDialect { fn supports_order_by_all(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 5e9f2e4e..be2cc007 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -179,4 +179,8 @@ impl Dialect for GenericDialect { fn supports_filter_during_aggregation(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 861dfe26..deb5719d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -570,6 +570,26 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports an exclude option + /// following a wildcard in the projection section. For example: + /// `SELECT * EXCLUDE col1 FROM tbl`. + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html) + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/select) + fn supports_select_wildcard_exclude(&self) -> bool { + false + } + + /// Returns true if the dialect supports an exclude option + /// as the last item in the projection section, not necessarily + /// after a wildcard. For example: + /// `SELECT *, c1, c2 EXCLUDE c3 FROM tbl` + /// + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_EXCLUDE_list.html) + fn supports_select_exclude(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 9ad9c5fd..8ffed98a 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -131,4 +131,12 @@ impl Dialect for RedshiftSqlDialect { fn supports_string_literal_backslash_escape(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } + + fn supports_select_exclude(&self) -> bool { + true + } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 85c09acb..3b1eff39 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -466,6 +466,10 @@ impl Dialect for SnowflakeDialect { fn supports_select_expr_star(&self) -> bool { true } + + fn supports_select_wildcard_exclude(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { diff --git a/src/keywords.rs b/src/keywords.rs index 73865150..9e689a6d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1119,6 +1119,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::FETCH, Keyword::UNION, Keyword::EXCEPT, + Keyword::EXCLUDE, Keyword::INTERSECT, Keyword::MINUS, Keyword::CLUSTER, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b00cd16d..a2d01f13 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11740,6 +11740,7 @@ impl<'a> Parser<'a> { top: None, top_before_distinct: false, projection: vec![], + exclude: None, into: None, from, lateral_views: vec![], @@ -11782,6 +11783,12 @@ impl<'a> Parser<'a> { self.parse_projection()? }; + let exclude = if self.dialect.supports_select_exclude() { + self.parse_optional_select_item_exclude()? + } else { + None + }; + let into = if self.parse_keyword(Keyword::INTO) { Some(self.parse_select_into()?) } else { @@ -11915,6 +11922,7 @@ impl<'a> Parser<'a> { top, top_before_distinct, projection, + exclude, into, from, lateral_views, @@ -15052,8 +15060,7 @@ impl<'a> Parser<'a> { } else { None }; - let opt_exclude = if opt_ilike.is_none() - && dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) + let opt_exclude = if opt_ilike.is_none() && self.dialect.supports_select_wildcard_exclude() { self.parse_optional_select_item_exclude()? } else { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 1d8669a2..9e5b6ce8 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -60,6 +60,7 @@ fn parse_map_access_expr() { ), })], })], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 906be9be..b7b5b630 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -459,6 +459,7 @@ fn parse_update_set_from() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))), ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), @@ -5695,6 +5696,7 @@ fn test_parse_named_window() { }, }, ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -6351,6 +6353,7 @@ fn parse_interval_and_or_xor() { quote_style: None, span: Span::empty(), }))], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -8620,6 +8623,7 @@ fn lateral_function() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -9616,6 +9620,7 @@ fn parse_merge() { projection: vec![SelectItem::Wildcard( WildcardAdditionalOptions::default() )], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![ @@ -11534,6 +11539,7 @@ fn parse_unload() { top: None, top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("tab")])), @@ -11734,6 +11740,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], + exclude: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], @@ -11815,6 +11822,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], + exclude: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], @@ -12748,6 +12756,7 @@ fn test_extract_seconds_ok() { format: None, }), })], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -14820,6 +14829,7 @@ fn test_select_from_first() { distinct: None, top: None, projection, + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -16000,3 +16010,108 @@ fn parse_create_procedure_with_parameter_modes() { _ => unreachable!(), } } + +#[test] +fn test_select_exclude() { + let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); + match &dialects + .verified_only_select("SELECT * EXCLUDE c1 FROM test") + .projection[0] + { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + } + _ => unreachable!(), + } + match &dialects + .verified_only_select("SELECT * EXCLUDE (c1, c2) FROM test") + .projection[0] + { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Multiple(vec![ + Ident::new("c1"), + Ident::new("c2") + ])) + ); + } + _ => unreachable!(), + } + let select = dialects.verified_only_select("SELECT * EXCLUDE c1, c2 FROM test"); + match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + } + _ => unreachable!(), + } + match &select.projection[1] { + SelectItem::UnnamedExpr(Expr::Identifier(ident)) => { + assert_eq!(*ident, Ident::new("c2")); + } + _ => unreachable!(), + } + + let dialects = all_dialects_where(|d| d.supports_select_exclude()); + let select = dialects.verified_only_select("SELECT *, c1 EXCLUDE c1 FROM test"); + match &select.projection[0] { + SelectItem::Wildcard(additional_options) => { + assert_eq!(*additional_options, WildcardAdditionalOptions::default()); + } + _ => unreachable!(), + } + assert_eq!( + select.exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + + let dialects = all_dialects_where(|d| { + d.supports_select_wildcard_exclude() && !d.supports_select_exclude() + }); + let select = dialects.verified_only_select("SELECT * EXCLUDE c1 FROM test"); + match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { opt_exclude, .. }) => { + assert_eq!( + *opt_exclude, + Some(ExcludeSelectItem::Single(Ident::new("c1"))) + ); + } + _ => unreachable!(), + } + + // Dialects that only support the wildcard form and do not accept EXCLUDE as an implicity alias + // will fail when encountered with the `c2` ident + let dialects = all_dialects_where(|d| { + d.supports_select_wildcard_exclude() + && !d.supports_select_exclude() + && d.is_column_alias(&Keyword::EXCLUDE, &mut Parser::new(d)) + }); + assert_eq!( + dialects + .parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test") + .err() + .unwrap(), + ParserError::ParserError("Expected: end of statement, found: c2".to_string()) + ); + + // Dialects that only support the wildcard form and accept EXCLUDE as an implicity alias + // will fail when encountered with the `EXCLUDE` keyword + let dialects = all_dialects_where(|d| { + d.supports_select_wildcard_exclude() + && !d.supports_select_exclude() + && !d.is_column_alias(&Keyword::EXCLUDE, &mut Parser::new(d)) + }); + assert_eq!( + dialects + .parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test") + .err() + .unwrap(), + ParserError::ParserError("Expected: end of statement, found: EXCLUDE".to_string()) + ); +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 371d3aac..fe14b7ba 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -269,6 +269,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { @@ -299,6 +300,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + exclude: None, top_before_distinct: false, into: None, from: vec![TableWithJoins { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ebbec25f..b4a650ce 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -126,6 +126,7 @@ fn parse_create_procedure() { projection: vec![SelectItem::UnnamedExpr(Expr::Value( (number("1")).with_empty_span() ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1368,6 +1369,7 @@ fn parse_substring_in_select() { special: true, shorthand: false, })], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -1516,6 +1518,7 @@ fn parse_mssql_declare() { (Value::Number("4".parse().unwrap(), false)).with_empty_span() )), })], + exclude: None, into: None, from: vec![], lateral_views: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 44f36584..e8c6719e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1403,6 +1403,7 @@ fn parse_escaped_quote_identifiers_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1456,6 +1457,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1503,6 +1505,7 @@ fn parse_escaped_backticks_with_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -1554,6 +1557,7 @@ fn parse_escaped_backticks_with_no_escape() { quote_style: Some('`'), span: Span::empty(), }))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2225,6 +2229,7 @@ fn parse_select_with_numeric_prefix_column_name() { projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( "123col_$@123abc" )))], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::with_quote( @@ -2392,7 +2397,6 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { q.body, Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), - distinct: None, top: None, top_before_distinct: false, @@ -2400,6 +2404,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { SelectItem::UnnamedExpr(Expr::value(number("123e4"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc"))) ], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::with_quote( @@ -3043,6 +3048,7 @@ fn parse_substring_in_select() { special: true, shorthand: false, })], + exclude: None, into: None, from: vec![TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident { @@ -3357,6 +3363,7 @@ fn parse_hex_string_introducer() { ) .into(), })], + exclude: None, from: vec![], lateral_views: vec![], prewhere: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 48792025..0d1d138c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1305,6 +1305,7 @@ fn parse_copy_to() { }, } ], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2948,6 +2949,7 @@ fn parse_array_subquery_expr() { projection: vec![SelectItem::UnnamedExpr(Expr::Value( (number("1")).with_empty_span() ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], @@ -2973,6 +2975,7 @@ fn parse_array_subquery_expr() { projection: vec![SelectItem::UnnamedExpr(Expr::Value( (number("2")).with_empty_span() ))], + exclude: None, into: None, from: vec![], lateral_views: vec![], From bc2c4e263d85a2440c87ea56f0c595a458e40fc8 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:46:48 +0200 Subject: [PATCH 39/39] Support optional semicolon between statements (#1937) --- src/parser/mod.rs | 8 ++++++++ src/test_utils.rs | 5 +++++ tests/sqlparser_common.rs | 22 ++++++++++++++++++++-- tests/sqlparser_mssql.rs | 26 +++++++++++++++++++++++++- tests/sqlparser_mysql.rs | 1 + 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a2d01f13..47b63da8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -222,6 +222,9 @@ pub struct ParserOptions { /// Controls how literal values are unescaped. See /// [`Tokenizer::with_unescape`] for more details. pub unescape: bool, + /// Controls if the parser expects a semi-colon token + /// between statements. Default is `true`. + pub require_semicolon_stmt_delimiter: bool, } impl Default for ParserOptions { @@ -229,6 +232,7 @@ impl Default for ParserOptions { Self { trailing_commas: false, unescape: true, + require_semicolon_stmt_delimiter: true, } } } @@ -467,6 +471,10 @@ impl<'a> Parser<'a> { expecting_statement_delimiter = false; } + if !self.options.require_semicolon_stmt_delimiter { + expecting_statement_delimiter = false; + } + match self.peek_token().token { Token::EOF => break, diff --git a/src/test_utils.rs b/src/test_utils.rs index 544ceaef..654f2723 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -294,6 +294,11 @@ pub fn all_dialects() -> TestedDialects { ]) } +// Returns all available dialects with the specified parser options +pub fn all_dialects_with_options(options: ParserOptions) -> TestedDialects { + TestedDialects::new_with_options(all_dialects().dialects, options) +} + /// Returns all dialects matching the given predicate. pub fn all_dialects_where(predicate: F) -> TestedDialects where diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b7b5b630..15144479 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -40,8 +40,9 @@ use sqlparser::parser::{Parser, ParserError, ParserOptions}; use sqlparser::tokenizer::Tokenizer; use sqlparser::tokenizer::{Location, Span}; use test_utils::{ - all_dialects, all_dialects_where, alter_table_op, assert_eq_vec, call, expr_from_projection, - join, number, only, table, table_alias, table_from_name, TestedDialects, + all_dialects, all_dialects_where, all_dialects_with_options, alter_table_op, assert_eq_vec, + call, expr_from_projection, join, number, only, table, table_alias, table_from_name, + TestedDialects, }; #[macro_use] @@ -16115,3 +16116,20 @@ fn test_select_exclude() { ParserError::ParserError("Expected: end of statement, found: EXCLUDE".to_string()) ); } + +#[test] +fn test_no_semicolon_required_between_statements() { + let sql = r#" +SELECT * FROM tbl1 +SELECT * FROM tbl2 + "#; + + let dialects = all_dialects_with_options(ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }); + let stmts = dialects.parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert!(stmts.iter().all(|s| matches!(s, Statement::Query { .. }))); +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b4a650ce..50c6448d 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -32,7 +32,7 @@ use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MsSqlDialect}; -use sqlparser::parser::{Parser, ParserError}; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; #[test] fn parse_mssql_identifiers() { @@ -2327,6 +2327,18 @@ fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } +// MS SQL dialect with support for optional semi-colon statement delimiters +fn tsql() -> TestedDialects { + TestedDialects::new_with_options( + vec![Box::new(MsSqlDialect {})], + ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }, + ) +} + fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } @@ -2483,3 +2495,15 @@ fn parse_mssql_grant() { fn parse_mssql_deny() { ms().verified_stmt("DENY SELECT ON my_table TO public, db_admin"); } + +#[test] +fn test_tsql_no_semicolon_delimiter() { + let sql = r#" +DECLARE @X AS NVARCHAR(MAX)='x' +DECLARE @Y AS NVARCHAR(MAX)='y' + "#; + + let stmts = tsql().parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert!(stmts.iter().all(|s| matches!(s, Statement::Declare { .. }))); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e8c6719e..9068ed9c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1442,6 +1442,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { ParserOptions { trailing_commas: false, unescape: false, + require_semicolon_stmt_delimiter: true, } ) .verified_stmt(sql),