| // 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. | |
| 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] | |
| mod test_utils; | |
| fn databricks() -> TestedDialects { | |
| TestedDialects::new(vec![Box::new(DatabricksDialect {})]) | |
| } | |
| fn databricks_and_generic() -> TestedDialects { | |
| TestedDialects::new(vec![ | |
| Box::new(DatabricksDialect {}), | |
| Box::new(GenericDialect {}), | |
| ]) | |
| } | |
| #[test] | |
| fn test_databricks_identifiers() { | |
| // databricks uses backtick for delimited identifiers | |
| assert_eq!( | |
| databricks().verified_only_select("SELECT `Ä`").projection[0], | |
| SelectItem::UnnamedExpr(Expr::Identifier(Ident::with_quote('`', "Ä"))) | |
| ); | |
| // double quotes produce string literals, not delimited identifiers | |
| assert_eq!( | |
| databricks() | |
| .verified_only_select(r#"SELECT "Ä""#) | |
| .projection[0], | |
| SelectItem::UnnamedExpr(Expr::Value( | |
| (Value::DoubleQuotedString("Ä".to_owned())).with_empty_span() | |
| )) | |
| ); | |
| } | |
| #[test] | |
| fn test_databricks_exists() { | |
| // exists is a function in databricks | |
| assert_eq!( | |
| databricks().verified_expr("exists(array(1, 2, 3), x -> x IS NULL)"), | |
| call( | |
| "exists", | |
| [ | |
| call( | |
| "array", | |
| [ | |
| Expr::value(number("1")), | |
| Expr::value(number("2")), | |
| Expr::value(number("3")) | |
| ] | |
| ), | |
| Expr::Lambda(LambdaFunction { | |
| params: OneOrManyWithParens::One(LambdaFunctionParameter { | |
| name: Ident::new("x"), | |
| data_type: None | |
| }), | |
| body: Box::new(Expr::IsNull(Box::new(Expr::Identifier(Ident::new("x"))))), | |
| syntax: LambdaSyntax::Arrow, | |
| }) | |
| ] | |
| ), | |
| ); | |
| let res = databricks().parse_sql_statements("SELECT EXISTS ("); | |
| assert_eq!( | |
| // TODO: improve this error message... | |
| ParserError::ParserError("Expected: an expression, found: EOF".to_string()), | |
| res.unwrap_err(), | |
| ); | |
| } | |
| #[test] | |
| fn test_databricks_lambdas() { | |
| #[rustfmt::skip] | |
| let sql = concat!( | |
| "SELECT array_sort(array('Hello', 'World'), ", | |
| "(p1, p2) -> CASE WHEN p1 = p2 THEN 0 ", | |
| "WHEN reverse(p1) < reverse(p2) THEN -1 ", | |
| "ELSE 1 END)", | |
| ); | |
| pretty_assertions::assert_eq!( | |
| SelectItem::UnnamedExpr(call( | |
| "array_sort", | |
| [ | |
| call( | |
| "array", | |
| [ | |
| Expr::value(Value::SingleQuotedString("Hello".to_owned())), | |
| Expr::value(Value::SingleQuotedString("World".to_owned())) | |
| ] | |
| ), | |
| Expr::Lambda(LambdaFunction { | |
| params: OneOrManyWithParens::Many(vec![ | |
| LambdaFunctionParameter { | |
| name: Ident::new("p1"), | |
| data_type: None | |
| }, | |
| LambdaFunctionParameter { | |
| name: Ident::new("p2"), | |
| data_type: None | |
| } | |
| ]), | |
| body: Box::new(Expr::Case { | |
| case_token: AttachedToken::empty(), | |
| end_token: AttachedToken::empty(), | |
| operand: None, | |
| conditions: vec![ | |
| CaseWhen { | |
| condition: Expr::BinaryOp { | |
| left: Box::new(Expr::Identifier(Ident::new("p1"))), | |
| op: BinaryOperator::Eq, | |
| right: Box::new(Expr::Identifier(Ident::new("p2"))) | |
| }, | |
| result: Expr::value(number("0")) | |
| }, | |
| CaseWhen { | |
| condition: Expr::BinaryOp { | |
| left: Box::new(call( | |
| "reverse", | |
| [Expr::Identifier(Ident::new("p1"))] | |
| )), | |
| op: BinaryOperator::Lt, | |
| right: Box::new(call( | |
| "reverse", | |
| [Expr::Identifier(Ident::new("p2"))] | |
| )), | |
| }, | |
| result: Expr::UnaryOp { | |
| op: UnaryOperator::Minus, | |
| expr: Box::new(Expr::value(number("1"))) | |
| } | |
| }, | |
| ], | |
| else_result: Some(Box::new(Expr::value(number("1")))) | |
| }), | |
| syntax: LambdaSyntax::Arrow, | |
| }) | |
| ] | |
| )), | |
| databricks().verified_only_select(sql).projection[0] | |
| ); | |
| databricks().verified_expr( | |
| "map_zip_with(map(1, 'a', 2, 'b'), map(1, 'x', 2, 'y'), (k, v1, v2) -> concat(v1, v2))", | |
| ); | |
| databricks().verified_expr("transform(array(1, 2, 3), x -> x + 1)"); | |
| } | |
| #[test] | |
| fn test_values_clause() { | |
| let values = Values { | |
| value_keyword: false, | |
| explicit_row: false, | |
| rows: vec![ | |
| Parens::with_empty_span(vec![ | |
| Expr::Value((Value::DoubleQuotedString("one".to_owned())).with_empty_span()), | |
| Expr::value(number("1")), | |
| ]), | |
| Parens::with_empty_span(vec![ | |
| Expr::Value((Value::SingleQuotedString("two".to_owned())).with_empty_span()), | |
| Expr::value(number("2")), | |
| ]), | |
| ], | |
| }; | |
| let query = databricks().verified_query(r#"VALUES ("one", 1), ('two', 2)"#); | |
| assert_eq!(SetExpr::Values(values.clone()), *query.body); | |
| // VALUES is permitted in a FROM clause without a subquery | |
| let query = databricks().verified_query_with_canonical( | |
| r#"SELECT * FROM VALUES ("one", 1), ('two', 2)"#, | |
| r#"SELECT * FROM (VALUES ("one", 1), ('two', 2))"#, | |
| ); | |
| let Some(TableFactor::Derived { subquery, .. }) = query | |
| .body | |
| .as_select() | |
| .map(|select| &select.from[0].relation) | |
| else { | |
| panic!("expected subquery"); | |
| }; | |
| assert_eq!(SetExpr::Values(values), *subquery.body); | |
| // values is also a valid table name | |
| let query = databricks_and_generic().verified_query(concat!( | |
| "WITH values AS (SELECT 42) ", | |
| "SELECT * FROM values", | |
| )); | |
| assert_eq!( | |
| Some(&table_from_name(ObjectName::from(vec![Ident::new( | |
| "values" | |
| )]))), | |
| query | |
| .body | |
| .as_select() | |
| .map(|select| &select.from[0].relation) | |
| ); | |
| // TODO: support this example from https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-qry-select-values.html#examples | |
| // databricks().verified_query("VALUES 1, 2, 3"); | |
| } | |
| #[test] | |
| fn parse_use() { | |
| let valid_object_names = ["mydb", "WAREHOUSE", "DEFAULT"]; | |
| let quote_styles = ['"', '`']; | |
| for object_name in &valid_object_names { | |
| // Test single identifier without quotes | |
| assert_eq!( | |
| databricks().verified_stmt(&format!("USE {object_name}")), | |
| Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( | |
| object_name.to_string() | |
| )]))) | |
| ); | |
| for "e in "e_styles { | |
| // Test single identifier with different type of quotes | |
| assert_eq!( | |
| databricks().verified_stmt(&format!("USE {quote}{object_name}{quote}")), | |
| Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( | |
| quote, | |
| object_name.to_string(), | |
| )]))) | |
| ); | |
| } | |
| } | |
| for "e in "e_styles { | |
| // Test single identifier with keyword and different type of quotes | |
| assert_eq!( | |
| 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 {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 {quote}my_schema{quote}")), | |
| Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( | |
| quote, | |
| "my_schema".to_string(), | |
| )]))) | |
| ); | |
| } | |
| // Test single identifier with keyword and no quotes | |
| assert_eq!( | |
| databricks().verified_stmt("USE CATALOG my_catalog"), | |
| Statement::Use(Use::Catalog(ObjectName::from(vec![Ident::new( | |
| "my_catalog" | |
| )]))) | |
| ); | |
| assert_eq!( | |
| databricks().verified_stmt("USE DATABASE my_schema"), | |
| Statement::Use(Use::Database(ObjectName::from(vec![Ident::new( | |
| "my_schema" | |
| )]))) | |
| ); | |
| assert_eq!( | |
| databricks().verified_stmt("USE SCHEMA my_schema"), | |
| Statement::Use(Use::Schema(ObjectName::from(vec![Ident::new("my_schema")]))) | |
| ); | |
| // Test invalid syntax - missing identifier | |
| let invalid_cases = ["USE SCHEMA", "USE DATABASE", "USE CATALOG"]; | |
| for sql in &invalid_cases { | |
| assert_eq!( | |
| databricks().parse_sql_statements(sql).unwrap_err(), | |
| ParserError::ParserError("Expected: identifier, found: EOF".to_string()), | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn parse_show_catalogs() { | |
| databricks().verified_stmt("SHOW CATALOGS"); | |
| databricks().verified_stmt("SHOW TERSE CATALOGS"); | |
| databricks().verified_stmt("SHOW CATALOGS HISTORY"); | |
| databricks().verified_stmt("SHOW CATALOGS LIKE 'pay*'"); | |
| databricks().verified_stmt("SHOW CATALOGS 'pay*'"); | |
| databricks().verified_stmt("SHOW CATALOGS STARTS WITH 'pay'"); | |
| databricks().verified_stmt("SHOW CATALOGS LIMIT 10"); | |
| databricks().verified_stmt("SHOW CATALOGS HISTORY STARTS WITH 'pay'"); | |
| match databricks().verified_stmt("SHOW CATALOGS LIKE 'pay*'") { | |
| Statement::ShowCatalogs { | |
| terse, | |
| history, | |
| show_options, | |
| } => { | |
| assert!(!terse); | |
| assert!(!history); | |
| assert_eq!(show_options.show_in, None); | |
| assert_eq!(show_options.starts_with, None); | |
| assert_eq!(show_options.limit, None); | |
| assert_eq!(show_options.limit_from, None); | |
| assert_eq!( | |
| show_options.filter_position, | |
| Some(ShowStatementFilterPosition::Suffix( | |
| ShowStatementFilter::Like("pay*".to_string()) | |
| )) | |
| ); | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| #[test] | |
| fn parse_show_catalogs_with_show_options() { | |
| databricks().verified_stmt("SHOW TERSE CATALOGS HISTORY IN ACCOUNT"); | |
| match databricks().verified_stmt("SHOW TERSE CATALOGS HISTORY IN ACCOUNT") { | |
| Statement::ShowCatalogs { | |
| terse, | |
| history, | |
| show_options, | |
| } => { | |
| assert!(terse); | |
| assert!(history); | |
| assert_eq!(show_options.filter_position, None); | |
| assert!(matches!( | |
| show_options.show_in, | |
| Some(ShowStatementIn { | |
| parent_type: Some(ShowStatementInParentType::Account), | |
| parent_name: None, | |
| .. | |
| }) | |
| )); | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| #[test] | |
| fn parse_databricks_struct_function() { | |
| assert_eq!( | |
| databricks_and_generic() | |
| .verified_only_select("SELECT STRUCT(1, 'foo')") | |
| .projection[0], | |
| SelectItem::UnnamedExpr(Expr::Struct { | |
| values: vec![ | |
| Expr::value(number("1")), | |
| Expr::Value((Value::SingleQuotedString("foo".to_string())).with_empty_span()) | |
| ], | |
| fields: vec![] | |
| }) | |
| ); | |
| assert_eq!( | |
| databricks_and_generic() | |
| .verified_only_select("SELECT STRUCT(1 AS one, 'foo' AS foo, false)") | |
| .projection[0], | |
| SelectItem::UnnamedExpr(Expr::Struct { | |
| values: vec![ | |
| Expr::Named { | |
| expr: Expr::value(number("1")).into(), | |
| name: Ident::new("one") | |
| }, | |
| Expr::Named { | |
| expr: Expr::Value( | |
| (Value::SingleQuotedString("foo".to_string())).with_empty_span() | |
| ) | |
| .into(), | |
| name: Ident::new("foo") | |
| }, | |
| Expr::Value((Value::Boolean(false)).with_empty_span()) | |
| ], | |
| fields: vec![] | |
| }) | |
| ); | |
| } | |
| #[test] | |
| fn data_type_timestamp_ntz() { | |
| // Literal | |
| assert_eq!( | |
| databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), | |
| Expr::TypedString(TypedString { | |
| data_type: DataType::TimestampNtz(None), | |
| value: ValueWithSpan { | |
| value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), | |
| span: Span::empty(), | |
| }, | |
| uses_odbc_syntax: false | |
| }) | |
| ); | |
| // Cast | |
| assert_eq!( | |
| databricks().verified_expr("(created_at)::TIMESTAMP_NTZ"), | |
| Expr::Cast { | |
| kind: CastKind::DoubleColon, | |
| expr: Box::new(Expr::Nested(Box::new(Expr::Identifier( | |
| "created_at".into() | |
| )))), | |
| data_type: DataType::TimestampNtz(None), | |
| format: None | |
| } | |
| ); | |
| // Column definition | |
| match databricks().verified_stmt("CREATE TABLE foo (x TIMESTAMP_NTZ)") { | |
| Statement::CreateTable(CreateTable { columns, .. }) => { | |
| assert_eq!( | |
| columns, | |
| vec![ColumnDef { | |
| name: "x".into(), | |
| data_type: DataType::TimestampNtz(None), | |
| options: vec![], | |
| }] | |
| ); | |
| } | |
| s => panic!("Unexpected statement: {s:?}"), | |
| } | |
| } | |
| #[test] | |
| fn parse_table_time_travel() { | |
| all_dialects_where(|d| d.supports_table_versioning()) | |
| .verified_only_select("SELECT 1 FROM t1 TIMESTAMP AS OF '2018-10-18T22:15:12.013Z'"); | |
| all_dialects_where(|d| d.supports_table_versioning()).verified_only_select( | |
| "SELECT 1 FROM t1 TIMESTAMP AS OF CURRENT_TIMESTAMP() - INTERVAL 12 HOURS", | |
| ); | |
| all_dialects_where(|d| d.supports_table_versioning()) | |
| .verified_only_select("SELECT 1 FROM t1 VERSION AS OF 1"); | |
| assert_eq!( | |
| databricks().parse_sql_statements("SELECT 1 FROM t1 FOR TIMESTAMP AS OF 'some_timestamp'"), | |
| Err(ParserError::ParserError( | |
| "Expected: one of UPDATE or SHARE, found: TIMESTAMP".to_string() | |
| )) | |
| ); | |
| assert_eq!( | |
| all_dialects_where(|d| d.supports_table_versioning()) | |
| .parse_sql_statements("SELECT 1 FROM t1 VERSION AS OF 1 - 2"), | |
| Err(ParserError::ParserError( | |
| "Expected: end of statement, found: -".to_string() | |
| )) | |
| ); | |
| } | |
| #[test] | |
| fn parse_optimize_table() { | |
| // Basic OPTIMIZE (Databricks style - no TABLE keyword) | |
| databricks().verified_stmt("OPTIMIZE my_table"); | |
| databricks().verified_stmt("OPTIMIZE db.my_table"); | |
| databricks().verified_stmt("OPTIMIZE catalog.db.my_table"); | |
| // With WHERE clause | |
| databricks().verified_stmt("OPTIMIZE my_table WHERE date = '2023-01-01'"); | |
| databricks() | |
| .verified_stmt("OPTIMIZE my_table WHERE date >= '2023-01-01' AND date < '2023-02-01'"); | |
| // With ZORDER BY clause | |
| databricks().verified_stmt("OPTIMIZE my_table ZORDER BY (col1)"); | |
| databricks().verified_stmt("OPTIMIZE my_table ZORDER BY (col1, col2)"); | |
| databricks().verified_stmt("OPTIMIZE my_table ZORDER BY (col1, col2, col3)"); | |
| // Combined WHERE and ZORDER BY | |
| databricks().verified_stmt("OPTIMIZE my_table WHERE date = '2023-01-01' ZORDER BY (col1)"); | |
| databricks() | |
| .verified_stmt("OPTIMIZE my_table WHERE date >= '2023-01-01' ZORDER BY (col1, col2)"); | |
| // Verify AST structure | |
| match databricks() | |
| .verified_stmt("OPTIMIZE my_table WHERE date = '2023-01-01' ZORDER BY (col1, col2)") | |
| { | |
| Statement::OptimizeTable { | |
| name, | |
| has_table_keyword, | |
| on_cluster, | |
| partition, | |
| include_final, | |
| deduplicate, | |
| predicate, | |
| zorder, | |
| } => { | |
| assert_eq!(name.to_string(), "my_table"); | |
| assert!(!has_table_keyword); | |
| assert!(on_cluster.is_none()); | |
| assert!(partition.is_none()); | |
| assert!(!include_final); | |
| assert!(deduplicate.is_none()); | |
| assert!(predicate.is_some()); | |
| assert_eq!( | |
| zorder, | |
| Some(vec![ | |
| Expr::Identifier(Ident::new("col1")), | |
| Expr::Identifier(Ident::new("col2")), | |
| ]) | |
| ); | |
| } | |
| _ => unreachable!(), | |
| } | |
| // Negative cases | |
| assert_eq!( | |
| databricks() | |
| .parse_sql_statements("OPTIMIZE my_table ZORDER BY") | |
| .unwrap_err(), | |
| ParserError::ParserError("Expected: (, found: EOF".to_string()) | |
| ); | |
| assert_eq!( | |
| databricks() | |
| .parse_sql_statements("OPTIMIZE my_table ZORDER BY ()") | |
| .unwrap_err(), | |
| ParserError::ParserError("Expected: an expression, found: )".to_string()) | |
| ); | |
| } | |
| #[test] | |
| fn parse_create_table_partitioned_by() { | |
| // Databricks allows PARTITIONED BY with just column names (referencing existing columns) | |
| // https://docs.databricks.com/en/sql/language-manual/sql-ref-partition.html | |
| // Single partition column without type | |
| databricks().verified_stmt("CREATE TABLE t (col1 STRING, col2 INT) PARTITIONED BY (col1)"); | |
| // Multiple partition columns without types | |
| databricks().verified_stmt( | |
| "CREATE TABLE t (col1 STRING, col2 INT, col3 DATE) PARTITIONED BY (col1, col2)", | |
| ); | |
| // Partition columns with types (new columns not in table spec) | |
| databricks().verified_stmt("CREATE TABLE t (name STRING) PARTITIONED BY (year INT, month INT)"); | |
| // Mixed: some with types, some without | |
| databricks() | |
| .verified_stmt("CREATE TABLE t (id INT, name STRING) PARTITIONED BY (region, year INT)"); | |
| // Verify AST structure for column without type | |
| match databricks().verified_stmt("CREATE TABLE t (col1 STRING) PARTITIONED BY (col1)") { | |
| Statement::CreateTable(CreateTable { | |
| name, | |
| columns, | |
| hive_distribution, | |
| .. | |
| }) => { | |
| assert_eq!(name.to_string(), "t"); | |
| assert_eq!(columns.len(), 1); | |
| assert_eq!(columns[0].name.to_string(), "col1"); | |
| match hive_distribution { | |
| HiveDistributionStyle::PARTITIONED { | |
| columns: partition_cols, | |
| } => { | |
| assert_eq!(partition_cols.len(), 1); | |
| assert_eq!(partition_cols[0].name.to_string(), "col1"); | |
| assert_eq!(partition_cols[0].data_type, DataType::Unspecified); | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| _ => unreachable!(), | |
| } | |
| // Verify AST structure for column with type | |
| match databricks().verified_stmt("CREATE TABLE t (name STRING) PARTITIONED BY (year INT)") { | |
| Statement::CreateTable(CreateTable { | |
| hive_distribution: | |
| HiveDistributionStyle::PARTITIONED { | |
| columns: partition_cols, | |
| }, | |
| .. | |
| }) => { | |
| assert_eq!(partition_cols.len(), 1); | |
| assert_eq!(partition_cols[0].name.to_string(), "year"); | |
| assert_eq!(partition_cols[0].data_type, DataType::Int(None)); | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| #[test] | |
| fn parse_databricks_struct_type() { | |
| // Databricks uses colon-separated struct field syntax (colon is optional) | |
| // https://docs.databricks.com/en/sql/language-manual/data-types/struct-type.html | |
| // Basic struct with colon syntax - parses to canonical form without colons | |
| databricks().one_statement_parses_to( | |
| "CREATE TABLE t (col1 STRUCT<field1: STRING, field2: INT>)", | |
| "CREATE TABLE t (col1 STRUCT<field1 STRING, field2 INT>)", | |
| ); | |
| // Nested array of struct (the original issue case) | |
| databricks().one_statement_parses_to( | |
| "CREATE TABLE t (col1 ARRAY<STRUCT<finish_flag: STRING, survive_flag: STRING, score: INT>>)", | |
| "CREATE TABLE t (col1 ARRAY<STRUCT<finish_flag STRING, survive_flag STRING, score INT>>)", | |
| ); | |
| // Multiple struct columns | |
| databricks().one_statement_parses_to( | |
| "CREATE TABLE t (col1 STRUCT<a: INT, b: STRING>, col2 STRUCT<x: DOUBLE>)", | |
| "CREATE TABLE t (col1 STRUCT<a INT, b STRING>, col2 STRUCT<x DOUBLE>)", | |
| ); | |
| // Deeply nested structs | |
| databricks().one_statement_parses_to( | |
| "CREATE TABLE t (col1 STRUCT<outer: STRUCT<inner: STRING>>)", | |
| "CREATE TABLE t (col1 STRUCT<outer STRUCT<inner STRING>>)", | |
| ); | |
| // Struct with array field | |
| databricks().one_statement_parses_to( | |
| "CREATE TABLE t (col1 STRUCT<items: ARRAY<INT>, name: STRING>)", | |
| "CREATE TABLE t (col1 STRUCT<items ARRAY<INT>, name STRING>)", | |
| ); | |
| // Syntax without colons should also work (BigQuery compatible) | |
| databricks().verified_stmt("CREATE TABLE t (col1 STRUCT<field1 STRING, field2 INT>)"); | |
| // Verify AST structure | |
| match databricks().one_statement_parses_to( | |
| "CREATE TABLE t (col1 STRUCT<field1: STRING, field2: INT>)", | |
| "CREATE TABLE t (col1 STRUCT<field1 STRING, field2 INT>)", | |
| ) { | |
| Statement::CreateTable(CreateTable { columns, .. }) => { | |
| assert_eq!(columns.len(), 1); | |
| assert_eq!(columns[0].name.to_string(), "col1"); | |
| match &columns[0].data_type { | |
| DataType::Struct(fields, StructBracketKind::AngleBrackets) => { | |
| assert_eq!(fields.len(), 2); | |
| assert_eq!( | |
| fields[0].field_name.as_ref().map(|i| i.to_string()), | |
| Some("field1".to_string()) | |
| ); | |
| assert_eq!(fields[0].field_type, DataType::String(None)); | |
| assert_eq!( | |
| fields[1].field_name.as_ref().map(|i| i.to_string()), | |
| Some("field2".to_string()) | |
| ); | |
| assert_eq!(fields[1].field_type, DataType::Int(None)); | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| #[test] | |
| fn parse_databricks_json_accessor() { | |
| // Basic colon accessor — unquoted field names are case-insensitive | |
| databricks().verified_only_select("SELECT raw:owner, RAW:owner FROM store_data"); | |
| // Unquoted field access is case-insensitive; bracket notation is case-sensitive. | |
| databricks().verified_only_select( | |
| "SELECT raw:OWNER AS case_insensitive, raw:['OWNER'] AS case_sensitive FROM store_data", | |
| ); | |
| // Backtick-quoted keys (Databricks delimited identifiers) normalise to double-quoted output. | |
| databricks().one_statement_parses_to( | |
| "SELECT raw:`zip code`, raw:`Zip Code`, raw:['fb:testid'] FROM store_data", | |
| r#"SELECT raw:"zip code", raw:"Zip Code", raw:['fb:testid'] FROM store_data"#, | |
| ); | |
| // Dot notation | |
| databricks().verified_only_select("SELECT raw:store.bicycle FROM store_data"); | |
| // String-key bracket notation after a dot segment | |
| databricks() | |
| .verified_only_select("SELECT raw:store['bicycle'], raw:store['BICYCLE'] FROM store_data"); | |
| // Integer-index bracket notation | |
| databricks() | |
| .verified_only_select("SELECT raw:store.fruit[0], raw:store.fruit[1] FROM store_data"); | |
| // Wildcard [*] — including chained and mixed positions | |
| databricks().verified_only_select( | |
| "SELECT raw:store.basket[*], raw:store.basket[*][0] AS first_of_baskets, \ | |
| raw:store.basket[0][*] AS first_basket, raw:store.basket[*][*] AS all_elements_flattened, \ | |
| raw:store.basket[0][2].b AS subfield FROM store_data", | |
| ); | |
| // Dot access following a wildcard bracket | |
| databricks().verified_only_select("SELECT raw:store.book[*].isbn FROM store_data"); | |
| // Double-colon cast — type keyword normalises to upper case | |
| databricks().one_statement_parses_to( | |
| "SELECT raw:store.bicycle.price::double FROM store_data", | |
| "SELECT raw:store.bicycle.price::DOUBLE FROM store_data", | |
| ); | |
| } | |
| #[test] | |
| fn parse_numeric_prefix_identifier() { | |
| databricks().verified_stmt("SELECT * FROM catalog.schema.1st_table"); | |
| databricks().verified_stmt("SELECT * FROM a.b.1c"); | |
| } | |
| #[test] | |
| fn parse_cte_without_as() { | |
| databricks_and_generic().one_statement_parses_to( | |
| "WITH cte (SELECT 1) SELECT * FROM cte", | |
| "WITH cte AS (SELECT 1) SELECT * FROM cte", | |
| ); | |
| databricks_and_generic().one_statement_parses_to( | |
| "WITH a AS (SELECT 1), b (SELECT 2) SELECT * FROM a, b", | |
| "WITH a AS (SELECT 1), b AS (SELECT 2) SELECT * FROM a, b", | |
| ); | |
| databricks_and_generic().one_statement_parses_to( | |
| "WITH cte (col1, col2) (SELECT 1, 2) SELECT * FROM cte", | |
| "WITH cte (col1, col2) AS (SELECT 1, 2) SELECT * FROM cte", | |
| ); | |
| databricks_and_generic().verified_query("WITH cte AS (SELECT 1) SELECT * FROM cte"); | |
| databricks_and_generic() | |
| .verified_query("WITH cte (col1, col2) AS (SELECT 1, 2) SELECT * FROM cte"); | |
| assert_eq!( | |
| all_dialects_where(|d| !d.supports_cte_without_as()) | |
| .parse_sql_statements("WITH cte (SELECT 1) SELECT * FROM cte"), | |
| Err(ParserError::ParserError( | |
| "Expected: ), found: 1".to_string() | |
| )) | |
| ); | |
| } | |
| #[test] | |
| fn test_databricks_insert_by_name() { | |
| databricks_and_generic().verified_stmt("INSERT INTO target BY NAME SELECT 1 AS a"); | |
| databricks_and_generic().verified_stmt( | |
| "INSERT INTO TABLE lakehouse.dwd.dwd_event_quality_sla_metric_di BY NAME WITH day AS (SELECT 1 AS event_data_id) SELECT event_data_id FROM day", | |
| ); | |
| } | |
| #[test] | |
| fn parse_databricks_query_entry_points() { | |
| databricks().verified_stmt("CREATE TABLE t (attrs MAP<STRING, ARRAY<INT>>)"); | |
| databricks().one_statement_parses_to(r#"SELECT 'it\'s'"#, "SELECT 'it''s'"); | |
| databricks().verified_stmt("SELECT * REPLACE (upper(name) AS name) FROM source"); | |
| databricks().verified_stmt( | |
| "CREATE VIEW cross_product AS FROM main.raw.left_table, main.raw.right_table", | |
| ); | |
| databricks() | |
| .verified_stmt("CREATE VIEW filtered AS FROM main.raw.source |> WHERE id > 0 |> SELECT id"); | |
| } | |
| #[test] | |
| fn parse_databricks_collated_data_types() { | |
| databricks().verified_stmt( | |
| "CREATE TABLE t (name STRING COLLATE UTF8_BINARY, values ARRAY<STRING COLLATE UTF8_LCASE>, attrs MAP<STRING COLLATE UTF8_BINARY, STRING COLLATE UTF8_LCASE>)", | |
| ); | |
| databricks().verified_stmt("CREATE TABLE t (s STRUCT<a STRING COLLATE UTF8_LCASE>)"); | |
| databricks().verified_stmt("CREATE TABLE t (c ARRAY<ARRAY<STRING>> COLLATE UTF8_LCASE)"); | |
| assert_eq!( | |
| TestedDialects::new(vec![Box::new(GenericDialect {})]) | |
| .parse_sql_statements("CREATE TABLE t (c ARRAY<STRING COLLATE UTF8_LCASE>)"), | |
| Err(ParserError::ParserError( | |
| "Expected: >, found: COLLATE".to_string() | |
| )) | |
| ); | |
| } |