| // 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. |
| |
| #![warn(clippy::all)] |
| //! Test SQL syntax specific to Microsoft's T-SQL. The parser based on the |
| //! generic dialect is also tested (on the inputs it can handle). |
| |
| #[macro_use] |
| mod test_utils; |
| |
| use test_utils::*; |
| |
| use sqlparser::ast::DataType::{Int, Text}; |
| use sqlparser::ast::DeclareAssignment::MsSqlAssignment; |
| use sqlparser::ast::Value::SingleQuotedString; |
| use sqlparser::ast::*; |
| use sqlparser::dialect::{GenericDialect, MsSqlDialect}; |
| use sqlparser::parser::{Parser, ParserError}; |
| |
| #[test] |
| fn parse_mssql_identifiers() { |
| let sql = "SELECT @@version, _foo$123 FROM ##temp"; |
| let select = ms_and_generic().verified_only_select(sql); |
| assert_eq!( |
| &Expr::Identifier(Ident::new("@@version")), |
| expr_from_projection(&select.projection[0]), |
| ); |
| assert_eq!( |
| &Expr::Identifier(Ident::new("_foo$123")), |
| expr_from_projection(&select.projection[1]), |
| ); |
| assert_eq!(2, select.projection.len()); |
| match &only(&select.from).relation { |
| TableFactor::Table { name, .. } => { |
| assert_eq!("##temp".to_string(), name.to_string()); |
| } |
| _ => unreachable!(), |
| }; |
| } |
| |
| #[test] |
| fn parse_table_time_travel() { |
| let version = "2023-08-18 23:08:18".to_string(); |
| let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); |
| let select = ms().verified_only_select(&sql); |
| assert_eq!( |
| select.from, |
| vec![TableWithJoins { |
| relation: TableFactor::Table { |
| name: ObjectName(vec![Ident::new("t1")]), |
| alias: None, |
| args: None, |
| with_hints: vec![], |
| version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( |
| Value::SingleQuotedString(version) |
| ))), |
| partitions: vec![], |
| with_ordinality: false, |
| }, |
| joins: vec![] |
| },] |
| ); |
| |
| let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string(); |
| assert!(ms().parse_sql_statements(&sql).is_err()); |
| } |
| |
| #[test] |
| fn parse_mssql_single_quoted_aliases() { |
| let _ = ms_and_generic().one_statement_parses_to("SELECT foo 'alias'", "SELECT foo AS 'alias'"); |
| } |
| |
| #[test] |
| fn parse_mssql_delimited_identifiers() { |
| let _ = ms().one_statement_parses_to( |
| "SELECT [a.b!] [FROM] FROM foo [WHERE]", |
| "SELECT [a.b!] AS [FROM] FROM foo AS [WHERE]", |
| ); |
| } |
| |
| #[test] |
| fn parse_create_procedure() { |
| let sql = "CREATE OR ALTER PROCEDURE test (@foo INT, @bar VARCHAR(256)) AS BEGIN SELECT 1 END"; |
| |
| assert_eq!( |
| ms().verified_stmt(sql), |
| Statement::CreateProcedure { |
| or_alter: true, |
| body: vec![Statement::Query(Box::new(Query { |
| with: None, |
| limit: None, |
| limit_by: vec![], |
| offset: None, |
| fetch: None, |
| locks: vec![], |
| for_clause: None, |
| order_by: None, |
| settings: None, |
| format_clause: None, |
| body: Box::new(SetExpr::Select(Box::new(Select { |
| distinct: None, |
| top: None, |
| projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], |
| into: None, |
| from: vec![], |
| lateral_views: vec![], |
| prewhere: None, |
| selection: None, |
| group_by: GroupByExpr::Expressions(vec![], vec![]), |
| cluster_by: vec![], |
| distribute_by: vec![], |
| sort_by: vec![], |
| having: None, |
| named_window: vec![], |
| window_before_qualify: false, |
| qualify: None, |
| value_table_mode: None, |
| connect_by: None, |
| }))) |
| }))], |
| params: Some(vec![ |
| ProcedureParam { |
| name: Ident { |
| value: "@foo".into(), |
| quote_style: None |
| }, |
| data_type: DataType::Int(None) |
| }, |
| ProcedureParam { |
| name: Ident { |
| value: "@bar".into(), |
| quote_style: None |
| }, |
| data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { |
| length: 256, |
| unit: None |
| })) |
| } |
| ]), |
| name: ObjectName(vec![Ident { |
| value: "test".into(), |
| quote_style: None |
| }]) |
| } |
| ) |
| } |
| |
| #[test] |
| fn parse_mssql_create_procedure() { |
| let _ = ms_and_generic().verified_stmt("CREATE OR ALTER PROCEDURE foo AS BEGIN SELECT 1 END"); |
| let _ = ms_and_generic().verified_stmt("CREATE PROCEDURE foo AS BEGIN SELECT 1 END"); |
| let _ = ms().verified_stmt( |
| "CREATE PROCEDURE foo AS BEGIN SELECT [myColumn] FROM [myschema].[mytable] END", |
| ); |
| let _ = ms_and_generic().verified_stmt( |
| "CREATE PROCEDURE foo (@CustomerName NVARCHAR(50)) AS BEGIN SELECT * FROM DEV END", |
| ); |
| let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test' END"); |
| // Test a statement with END in it |
| let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN SELECT [foo], CASE WHEN [foo] IS NULL THEN 'empty' ELSE 'notempty' END AS [foo] END"); |
| // Multiple statements |
| let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; SELECT [foo] FROM BAR WHERE [FOO] > 10 END"); |
| } |
| |
| #[test] |
| fn parse_mssql_apply_join() { |
| let _ = ms_and_generic().verified_only_select( |
| "SELECT * FROM sys.dm_exec_query_stats AS deqs \ |
| CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle)", |
| ); |
| let _ = ms_and_generic().verified_only_select( |
| "SELECT * FROM sys.dm_exec_query_stats AS deqs \ |
| OUTER APPLY sys.dm_exec_query_plan(deqs.plan_handle)", |
| ); |
| let _ = ms_and_generic().verified_only_select( |
| "SELECT * FROM foo \ |
| OUTER APPLY (SELECT foo.x + 1) AS bar", |
| ); |
| } |
| |
| #[test] |
| fn parse_mssql_top_paren() { |
| let sql = "SELECT TOP (5) * FROM foo"; |
| let select = ms_and_generic().verified_only_select(sql); |
| let top = select.top.unwrap(); |
| assert_eq!( |
| Some(TopQuantity::Expr(Expr::Value(number("5")))), |
| top.quantity |
| ); |
| assert!(!top.percent); |
| } |
| |
| #[test] |
| fn parse_mssql_top_percent() { |
| let sql = "SELECT TOP (5) PERCENT * FROM foo"; |
| let select = ms_and_generic().verified_only_select(sql); |
| let top = select.top.unwrap(); |
| assert_eq!( |
| Some(TopQuantity::Expr(Expr::Value(number("5")))), |
| top.quantity |
| ); |
| assert!(top.percent); |
| } |
| |
| #[test] |
| fn parse_mssql_top_with_ties() { |
| let sql = "SELECT TOP (5) WITH TIES * FROM foo"; |
| let select = ms_and_generic().verified_only_select(sql); |
| let top = select.top.unwrap(); |
| assert_eq!( |
| Some(TopQuantity::Expr(Expr::Value(number("5")))), |
| top.quantity |
| ); |
| assert!(top.with_ties); |
| } |
| |
| #[test] |
| fn parse_mssql_top_percent_with_ties() { |
| let sql = "SELECT TOP (10) PERCENT WITH TIES * FROM foo"; |
| let select = ms_and_generic().verified_only_select(sql); |
| let top = select.top.unwrap(); |
| assert_eq!( |
| Some(TopQuantity::Expr(Expr::Value(number("10")))), |
| top.quantity |
| ); |
| assert!(top.percent); |
| } |
| |
| #[test] |
| fn parse_mssql_top() { |
| let sql = "SELECT TOP 5 bar, baz FROM foo"; |
| let _ = ms_and_generic().one_statement_parses_to(sql, "SELECT TOP 5 bar, baz FROM foo"); |
| } |
| |
| #[test] |
| fn parse_mssql_bin_literal() { |
| let _ = ms_and_generic().one_statement_parses_to("SELECT 0xdeadBEEF", "SELECT X'deadBEEF'"); |
| } |
| |
| #[test] |
| fn parse_mssql_create_role() { |
| let sql = "CREATE ROLE mssql AUTHORIZATION helena"; |
| match ms().verified_stmt(sql) { |
| Statement::CreateRole { |
| names, |
| authorization_owner, |
| .. |
| } => { |
| assert_eq_vec(&["mssql"], &names); |
| assert_eq!( |
| authorization_owner, |
| Some(ObjectName(vec![Ident { |
| value: "helena".into(), |
| quote_style: None |
| }])) |
| ); |
| } |
| _ => unreachable!(), |
| } |
| } |
| |
| #[test] |
| fn parse_alter_role() { |
| let sql = "ALTER ROLE old_name WITH NAME = new_name"; |
| assert_eq!( |
| ms().parse_sql_statements(sql).unwrap(), |
| [Statement::AlterRole { |
| name: Ident { |
| value: "old_name".into(), |
| quote_style: None |
| }, |
| operation: AlterRoleOperation::RenameRole { |
| role_name: Ident { |
| value: "new_name".into(), |
| quote_style: None |
| } |
| }, |
| }] |
| ); |
| |
| let sql = "ALTER ROLE role_name ADD MEMBER new_member"; |
| assert_eq!( |
| ms().verified_stmt(sql), |
| Statement::AlterRole { |
| name: Ident { |
| value: "role_name".into(), |
| quote_style: None |
| }, |
| operation: AlterRoleOperation::AddMember { |
| member_name: Ident { |
| value: "new_member".into(), |
| quote_style: None |
| } |
| }, |
| } |
| ); |
| |
| let sql = "ALTER ROLE role_name DROP MEMBER old_member"; |
| assert_eq!( |
| ms().verified_stmt(sql), |
| Statement::AlterRole { |
| name: Ident { |
| value: "role_name".into(), |
| quote_style: None |
| }, |
| operation: AlterRoleOperation::DropMember { |
| member_name: Ident { |
| value: "old_member".into(), |
| quote_style: None |
| } |
| }, |
| } |
| ); |
| } |
| |
| #[test] |
| fn parse_delimited_identifiers() { |
| // check that quoted identifiers in any position remain quoted after serialization |
| let select = ms_and_generic().verified_only_select( |
| r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, |
| ); |
| // check FROM |
| match only(select.from).relation { |
| TableFactor::Table { |
| name, |
| alias, |
| args, |
| with_hints, |
| version, |
| with_ordinality: _, |
| partitions: _, |
| } => { |
| assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); |
| assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); |
| assert!(args.is_none()); |
| assert!(with_hints.is_empty()); |
| assert!(version.is_none()); |
| } |
| _ => panic!("Expecting TableFactor::Table"), |
| } |
| // check SELECT |
| assert_eq!(3, select.projection.len()); |
| assert_eq!( |
| &Expr::CompoundIdentifier(vec![ |
| Ident::with_quote('"', "alias"), |
| Ident::with_quote('"', "bar baz"), |
| ]), |
| expr_from_projection(&select.projection[0]), |
| ); |
| assert_eq!( |
| &Expr::Function(Function { |
| name: ObjectName(vec![Ident::with_quote('"', "myfun")]), |
| parameters: FunctionArguments::None, |
| args: FunctionArguments::List(FunctionArgumentList { |
| duplicate_treatment: None, |
| args: vec![], |
| clauses: vec![], |
| }), |
| null_treatment: None, |
| filter: None, |
| over: None, |
| within_group: vec![], |
| }), |
| expr_from_projection(&select.projection[1]), |
| ); |
| match &select.projection[2] { |
| SelectItem::ExprWithAlias { expr, alias } => { |
| assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); |
| assert_eq!(&Ident::with_quote('"', "column alias"), alias); |
| } |
| _ => panic!("Expected ExprWithAlias"), |
| } |
| |
| ms_and_generic().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); |
| ms_and_generic().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); |
| //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); |
| } |
| |
| #[test] |
| fn parse_table_name_in_square_brackets() { |
| let select = ms().verified_only_select(r#"SELECT [a column] FROM [a schema].[a table]"#); |
| if let TableFactor::Table { name, .. } = only(select.from).relation { |
| assert_eq!( |
| vec![ |
| Ident::with_quote('[', "a schema"), |
| Ident::with_quote('[', "a table") |
| ], |
| name.0 |
| ); |
| } else { |
| panic!("Expecting TableFactor::Table"); |
| } |
| assert_eq!( |
| &Expr::Identifier(Ident::with_quote('[', "a column")), |
| expr_from_projection(&select.projection[0]), |
| ); |
| } |
| |
| #[test] |
| fn parse_for_clause() { |
| ms_and_generic().verified_stmt("SELECT a FROM t FOR JSON PATH"); |
| ms_and_generic().verified_stmt("SELECT b FROM t FOR JSON AUTO"); |
| ms_and_generic().verified_stmt("SELECT c FROM t FOR JSON AUTO, WITHOUT_ARRAY_WRAPPER"); |
| ms_and_generic().verified_stmt("SELECT 1 FROM t FOR JSON PATH, ROOT('x'), INCLUDE_NULL_VALUES"); |
| ms_and_generic().verified_stmt("SELECT 2 FROM t FOR XML AUTO"); |
| ms_and_generic().verified_stmt("SELECT 3 FROM t FOR XML AUTO, TYPE, ELEMENTS"); |
| ms_and_generic().verified_stmt("SELECT * FROM t WHERE x FOR XML AUTO, ELEMENTS"); |
| ms_and_generic().verified_stmt("SELECT x FROM t ORDER BY y FOR XML AUTO, ELEMENTS"); |
| ms_and_generic().verified_stmt("SELECT y FROM t FOR XML PATH('x'), ROOT('y'), ELEMENTS"); |
| ms_and_generic().verified_stmt("SELECT z FROM t FOR XML EXPLICIT, BINARY BASE64"); |
| ms_and_generic().verified_stmt("SELECT * FROM t FOR XML RAW('x')"); |
| ms_and_generic().verified_stmt("SELECT * FROM t FOR BROWSE"); |
| } |
| |
| #[test] |
| fn dont_parse_trailing_for() { |
| assert!(ms() |
| .run_parser_method("SELECT * FROM foo FOR", |p| p.parse_query()) |
| .is_err()); |
| } |
| |
| #[test] |
| fn parse_for_json_expect_ast() { |
| assert_eq!( |
| ms().verified_query("SELECT * FROM t FOR JSON PATH, ROOT('root')") |
| .for_clause |
| .unwrap(), |
| ForClause::Json { |
| for_json: ForJson::Path, |
| root: Some("root".into()), |
| without_array_wrapper: false, |
| include_null_values: false, |
| } |
| ); |
| } |
| |
| #[test] |
| fn parse_ampersand_arobase() { |
| // In SQL Server, a&@b means (a) & (@b), in PostgreSQL it means (a) &@ (b) |
| ms().expr_parses_to("a&@b", "a & @b"); |
| } |
| |
| #[test] |
| fn parse_cast_varchar_max() { |
| ms_and_generic().verified_expr("CAST('foo' AS VARCHAR(MAX))"); |
| ms_and_generic().verified_expr("CAST('foo' AS NVARCHAR(MAX))"); |
| } |
| |
| #[test] |
| fn parse_convert() { |
| let sql = "CONVERT(INT, 1, 2, 3, NULL)"; |
| let Expr::Convert { |
| expr, |
| data_type, |
| charset, |
| target_before_value, |
| styles, |
| } = ms().verified_expr(sql) |
| else { |
| unreachable!() |
| }; |
| assert_eq!(Expr::Value(number("1")), *expr); |
| assert_eq!(Some(DataType::Int(None)), data_type); |
| assert!(charset.is_none()); |
| assert!(target_before_value); |
| assert_eq!( |
| vec![ |
| Expr::Value(number("2")), |
| Expr::Value(number("3")), |
| Expr::Value(Value::Null), |
| ], |
| styles |
| ); |
| |
| ms().verified_expr("CONVERT(VARCHAR(MAX), 'foo')"); |
| ms().verified_expr("CONVERT(VARCHAR(10), 'foo')"); |
| ms().verified_expr("CONVERT(DECIMAL(10,5), 12.55)"); |
| |
| let error_sql = "SELECT CONVERT(INT, 'foo',) FROM T"; |
| assert_eq!( |
| ParserError::ParserError("Expected: an expression, found: )".to_owned()), |
| ms().parse_sql_statements(error_sql).unwrap_err() |
| ); |
| } |
| |
| #[test] |
| fn parse_substring_in_select() { |
| let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; |
| match ms().one_statement_parses_to( |
| sql, |
| "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test", |
| ) { |
| Statement::Query(query) => { |
| assert_eq!( |
| Box::new(Query { |
| with: None, |
| |
| body: Box::new(SetExpr::Select(Box::new(Select { |
| distinct: Some(Distinct::Distinct), |
| top: None, |
| projection: vec![SelectItem::UnnamedExpr(Expr::Substring { |
| expr: Box::new(Expr::Identifier(Ident { |
| value: "description".to_string(), |
| quote_style: None |
| })), |
| substring_from: Some(Box::new(Expr::Value(number("0")))), |
| substring_for: Some(Box::new(Expr::Value(number("1")))), |
| special: true, |
| })], |
| into: None, |
| from: vec![TableWithJoins { |
| relation: TableFactor::Table { |
| name: ObjectName(vec![Ident { |
| value: "test".to_string(), |
| quote_style: None |
| }]), |
| alias: None, |
| args: None, |
| with_hints: vec![], |
| version: None, |
| partitions: vec![], |
| with_ordinality: false, |
| }, |
| joins: vec![] |
| }], |
| lateral_views: vec![], |
| prewhere: None, |
| selection: None, |
| group_by: GroupByExpr::Expressions(vec![], vec![]), |
| cluster_by: vec![], |
| distribute_by: vec![], |
| sort_by: vec![], |
| having: None, |
| named_window: vec![], |
| qualify: None, |
| window_before_qualify: false, |
| value_table_mode: None, |
| connect_by: None, |
| }))), |
| order_by: None, |
| limit: None, |
| limit_by: vec![], |
| offset: None, |
| fetch: None, |
| locks: vec![], |
| for_clause: None, |
| settings: None, |
| format_clause: None, |
| }), |
| query |
| ); |
| } |
| _ => unreachable!(), |
| } |
| } |
| |
| #[test] |
| fn parse_mssql_declare() { |
| let sql = "DECLARE @foo CURSOR, @bar INT, @baz AS TEXT = 'foobar';"; |
| let ast = Parser::parse_sql(&MsSqlDialect {}, sql).unwrap(); |
| |
| assert_eq!( |
| vec![Statement::Declare { |
| stmts: vec![ |
| Declare { |
| names: vec![Ident { |
| value: "@foo".to_string(), |
| quote_style: None |
| }], |
| data_type: None, |
| assignment: None, |
| declare_type: Some(DeclareType::Cursor), |
| binary: None, |
| sensitive: None, |
| scroll: None, |
| hold: None, |
| for_query: None |
| }, |
| Declare { |
| names: vec![Ident { |
| value: "@bar".to_string(), |
| quote_style: None |
| }], |
| data_type: Some(Int(None)), |
| assignment: None, |
| declare_type: None, |
| binary: None, |
| sensitive: None, |
| scroll: None, |
| hold: None, |
| for_query: None |
| }, |
| Declare { |
| names: vec![Ident { |
| value: "@baz".to_string(), |
| quote_style: None |
| }], |
| data_type: Some(Text), |
| assignment: Some(MsSqlAssignment(Box::new(Expr::Value(SingleQuotedString( |
| "foobar".to_string() |
| ))))), |
| declare_type: None, |
| binary: None, |
| sensitive: None, |
| scroll: None, |
| hold: None, |
| for_query: None |
| } |
| ] |
| }], |
| ast |
| ); |
| } |
| |
| #[test] |
| fn parse_use() { |
| let valid_object_names = [ |
| "mydb", |
| "SCHEMA", |
| "DATABASE", |
| "CATALOG", |
| "WAREHOUSE", |
| "DEFAULT", |
| ]; |
| let quote_styles = ['\'', '"']; |
| for object_name in &valid_object_names { |
| // Test single identifier without quotes |
| assert_eq!( |
| ms().verified_stmt(&format!("USE {}", object_name)), |
| Statement::Use(Use::Object(ObjectName(vec![Ident::new( |
| object_name.to_string() |
| )]))) |
| ); |
| for "e in "e_styles { |
| // Test single identifier with different type of quotes |
| assert_eq!( |
| ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), |
| Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( |
| quote, |
| object_name.to_string(), |
| )]))) |
| ); |
| } |
| } |
| } |
| |
| #[test] |
| fn parse_create_table_with_valid_options() { |
| let options = [ |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, PARTITION (column_a RANGE FOR VALUES (10, 11)))", |
| vec![ |
| SqlOption::KeyValue { |
| key: Ident { |
| value: "DISTRIBUTION".to_string(), |
| quote_style: None, |
| }, |
| value: Expr::Identifier(Ident { |
| value: "ROUND_ROBIN".to_string(), |
| quote_style: None, |
| }) |
| }, |
| SqlOption::Partition { |
| column_name: "column_a".into(), |
| range_direction: None, |
| for_values: vec![Expr::Value(test_utils::number("10")), Expr::Value(test_utils::number("11"))] , |
| }, |
| ], |
| ), |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (PARTITION (column_a RANGE LEFT FOR VALUES (10, 11)))", |
| vec![ |
| SqlOption::Partition { |
| column_name: "column_a".into(), |
| range_direction: Some(PartitionRangeDirection::Left), |
| for_values: vec![ |
| Expr::Value(test_utils::number("10")), |
| Expr::Value(test_utils::number("11")), |
| ], |
| } |
| ], |
| ), |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX)", |
| vec![SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndex)], |
| ), |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX ORDER (column_a, column_b))", |
| vec![ |
| SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndexOrder(vec![ |
| "column_a".into(), |
| "column_b".into(), |
| ])) |
| ], |
| ), |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED INDEX (column_a ASC, column_b DESC, column_c))", |
| vec![ |
| SqlOption::Clustered(TableOptionsClustered::Index(vec![ |
| ClusteredIndex { |
| name: Ident { |
| value: "column_a".to_string(), |
| quote_style: None, |
| }, |
| asc: Some(true), |
| }, |
| ClusteredIndex { |
| name: Ident { |
| value: "column_b".to_string(), |
| quote_style: None, |
| }, |
| asc: Some(false), |
| }, |
| ClusteredIndex { |
| name: Ident { |
| value: "column_c".to_string(), |
| quote_style: None, |
| }, |
| asc: None, |
| }, |
| ])) |
| ], |
| ), |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = HASH(column_a, column_b), HEAP)", |
| vec![ |
| SqlOption::KeyValue { |
| key: Ident { |
| value: "DISTRIBUTION".to_string(), |
| quote_style: None, |
| }, |
| value: Expr::Function( |
| Function { |
| name: ObjectName( |
| vec![ |
| Ident { |
| value: "HASH".to_string(), |
| quote_style: None, |
| }, |
| ], |
| ), |
| parameters: FunctionArguments::None, |
| args: FunctionArguments::List( |
| FunctionArgumentList { |
| duplicate_treatment: None, |
| args: vec![ |
| FunctionArg::Unnamed( |
| FunctionArgExpr::Expr( |
| Expr::Identifier( |
| Ident { |
| value: "column_a".to_string(), |
| quote_style: None, |
| }, |
| ), |
| ), |
| ), |
| FunctionArg::Unnamed( |
| FunctionArgExpr::Expr( |
| Expr::Identifier( |
| Ident { |
| value: "column_b".to_string(), |
| quote_style: None, |
| }, |
| ), |
| ), |
| ), |
| ], |
| clauses: vec![], |
| }, |
| ), |
| filter: None, |
| null_treatment: None, |
| over: None, |
| within_group: vec![], |
| }, |
| ), |
| }, |
| SqlOption::Ident("HEAP".into()), |
| ], |
| ), |
| ]; |
| |
| for (sql, with_options) in options { |
| assert_eq!( |
| ms_and_generic().verified_stmt(sql), |
| Statement::CreateTable(CreateTable { |
| or_replace: false, |
| temporary: false, |
| external: false, |
| global: None, |
| if_not_exists: false, |
| transient: false, |
| volatile: false, |
| name: ObjectName(vec![Ident { |
| value: "mytable".to_string(), |
| quote_style: None, |
| },],), |
| columns: vec![ |
| ColumnDef { |
| name: Ident { |
| value: "column_a".to_string(), |
| quote_style: None, |
| }, |
| data_type: Int(None,), |
| collation: None, |
| options: vec![], |
| }, |
| ColumnDef { |
| name: Ident { |
| value: "column_b".to_string(), |
| quote_style: None, |
| }, |
| data_type: Int(None,), |
| collation: None, |
| options: vec![], |
| }, |
| ColumnDef { |
| name: Ident { |
| value: "column_c".to_string(), |
| quote_style: None, |
| }, |
| data_type: Int(None,), |
| collation: None, |
| options: vec![], |
| }, |
| ], |
| constraints: vec![], |
| hive_distribution: HiveDistributionStyle::NONE, |
| hive_formats: Some(HiveFormat { |
| row_format: None, |
| serde_properties: None, |
| storage: None, |
| location: None, |
| },), |
| table_properties: vec![], |
| with_options, |
| file_format: None, |
| location: None, |
| query: None, |
| without_rowid: false, |
| like: None, |
| clone: None, |
| engine: None, |
| comment: None, |
| auto_increment_offset: None, |
| default_charset: None, |
| collation: None, |
| on_commit: None, |
| on_cluster: None, |
| primary_key: None, |
| order_by: None, |
| partition_by: None, |
| cluster_by: None, |
| clustered_by: None, |
| options: None, |
| strict: false, |
| copy_grants: false, |
| enable_schema_evolution: None, |
| change_tracking: None, |
| data_retention_time_in_days: None, |
| max_data_extension_time_in_days: None, |
| default_ddl_collation: None, |
| with_aggregation_policy: None, |
| with_row_access_policy: None, |
| with_tags: None, |
| }) |
| ); |
| } |
| } |
| |
| #[test] |
| fn parse_create_table_with_invalid_options() { |
| let invalid_cases = vec![ |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX ORDER ())", |
| "Expected: identifier, found: )", |
| ), |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE)", |
| "invalid CLUSTERED sequence", |
| ), |
| ( |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (HEAP INDEX)", |
| "Expected: ), found: INDEX", |
| ), |
| ( |
| |
| "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (PARTITION (RANGE LEFT FOR VALUES (10, 11)))", |
| "Expected: RANGE, found: LEFT", |
| ), |
| ]; |
| |
| for (sql, expected_error) in invalid_cases { |
| let res = ms_and_generic().parse_sql_statements(sql); |
| assert_eq!( |
| format!("sql parser error: {expected_error}"), |
| res.unwrap_err().to_string() |
| ); |
| } |
| } |
| |
| #[test] |
| fn parse_create_table_with_identity_column() { |
| let with_column_options = [ |
| ( |
| r#"CREATE TABLE mytable (columnA INT IDENTITY NOT NULL)"#, |
| vec![ |
| ColumnOptionDef { |
| name: None, |
| option: ColumnOption::Identity(None), |
| }, |
| ColumnOptionDef { |
| name: None, |
| option: ColumnOption::NotNull, |
| }, |
| ], |
| ), |
| ( |
| r#"CREATE TABLE mytable (columnA INT IDENTITY(1, 1) NOT NULL)"#, |
| vec![ |
| ColumnOptionDef { |
| name: None, |
| #[cfg(not(feature = "bigdecimal"))] |
| option: ColumnOption::Identity(Some(IdentityProperty { |
| seed: Expr::Value(Value::Number("1".to_string(), false)), |
| increment: Expr::Value(Value::Number("1".to_string(), false)), |
| })), |
| #[cfg(feature = "bigdecimal")] |
| option: ColumnOption::Identity(Some(IdentityProperty { |
| seed: Expr::Value(Value::Number(bigdecimal::BigDecimal::from(1), false)), |
| increment: Expr::Value(Value::Number( |
| bigdecimal::BigDecimal::from(1), |
| false, |
| )), |
| })), |
| }, |
| ColumnOptionDef { |
| name: None, |
| option: ColumnOption::NotNull, |
| }, |
| ], |
| ), |
| ]; |
| |
| for (sql, column_options) in with_column_options { |
| assert_eq!( |
| ms_and_generic().verified_stmt(sql), |
| Statement::CreateTable(CreateTable { |
| or_replace: false, |
| temporary: false, |
| external: false, |
| global: None, |
| if_not_exists: false, |
| transient: false, |
| volatile: false, |
| name: ObjectName(vec![Ident { |
| value: "mytable".to_string(), |
| quote_style: None, |
| },],), |
| columns: vec![ColumnDef { |
| name: Ident { |
| value: "columnA".to_string(), |
| quote_style: None, |
| }, |
| data_type: Int(None,), |
| collation: None, |
| options: column_options, |
| },], |
| constraints: vec![], |
| hive_distribution: HiveDistributionStyle::NONE, |
| hive_formats: Some(HiveFormat { |
| row_format: None, |
| serde_properties: None, |
| storage: None, |
| location: None, |
| },), |
| table_properties: vec![], |
| with_options: vec![], |
| file_format: None, |
| location: None, |
| query: None, |
| without_rowid: false, |
| like: None, |
| clone: None, |
| engine: None, |
| comment: None, |
| auto_increment_offset: None, |
| default_charset: None, |
| collation: None, |
| on_commit: None, |
| on_cluster: None, |
| primary_key: None, |
| order_by: None, |
| partition_by: None, |
| cluster_by: None, |
| clustered_by: None, |
| options: None, |
| strict: false, |
| copy_grants: false, |
| enable_schema_evolution: None, |
| change_tracking: None, |
| data_retention_time_in_days: None, |
| max_data_extension_time_in_days: None, |
| default_ddl_collation: None, |
| with_aggregation_policy: None, |
| with_row_access_policy: None, |
| with_tags: None, |
| }), |
| ); |
| } |
| } |
| |
| fn ms() -> TestedDialects { |
| TestedDialects { |
| dialects: vec![Box::new(MsSqlDialect {})], |
| options: None, |
| } |
| } |
| fn ms_and_generic() -> TestedDialects { |
| TestedDialects { |
| dialects: vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})], |
| options: None, |
| } |
| } |