| // 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. | |
| #[macro_use] | |
| mod test_utils; | |
| use sqlparser::tokenizer::Span; | |
| use test_utils::*; | |
| use sqlparser::ast::*; | |
| use sqlparser::dialect::GenericDialect; | |
| use sqlparser::dialect::RedshiftSqlDialect; | |
| use sqlparser::parser::ParserError; | |
| #[test] | |
| fn test_square_brackets_over_db_schema_table_name() { | |
| let select = redshift().verified_only_select("SELECT [col1] FROM [test_schema].[test_table]"); | |
| assert_eq!( | |
| select.projection[0], | |
| SelectItem::UnnamedExpr(Expr::Identifier(Ident { | |
| value: "col1".to_string(), | |
| quote_style: Some('['), | |
| span: Span::empty(), | |
| })), | |
| ); | |
| assert_eq!( | |
| select.from[0], | |
| TableWithJoins { | |
| relation: table_from_name(ObjectName::from(vec![ | |
| Ident { | |
| value: "test_schema".to_string(), | |
| quote_style: Some('['), | |
| span: Span::empty(), | |
| }, | |
| Ident { | |
| value: "test_table".to_string(), | |
| quote_style: Some('['), | |
| span: Span::empty(), | |
| } | |
| ])), | |
| joins: vec![], | |
| } | |
| ); | |
| } | |
| #[test] | |
| fn brackets_over_db_schema_table_name_with_whites_paces() { | |
| match redshift().parse_sql_statements("SELECT [ col1 ] FROM [ test_schema].[ test_table]") { | |
| Ok(statements) => { | |
| assert_eq!(statements.len(), 1); | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| #[test] | |
| fn test_double_quotes_over_db_schema_table_name() { | |
| let select = | |
| redshift().verified_only_select("SELECT \"col1\" FROM \"test_schema\".\"test_table\""); | |
| assert_eq!( | |
| select.projection[0], | |
| SelectItem::UnnamedExpr(Expr::Identifier(Ident { | |
| value: "col1".to_string(), | |
| quote_style: Some('"'), | |
| span: Span::empty(), | |
| })), | |
| ); | |
| assert_eq!( | |
| select.from[0], | |
| TableWithJoins { | |
| relation: table_from_name(ObjectName::from(vec![ | |
| Ident { | |
| value: "test_schema".to_string(), | |
| quote_style: Some('"'), | |
| span: Span::empty(), | |
| }, | |
| Ident { | |
| value: "test_table".to_string(), | |
| quote_style: Some('"'), | |
| span: Span::empty(), | |
| } | |
| ])), | |
| joins: vec![], | |
| } | |
| ); | |
| } | |
| #[test] | |
| fn parse_delimited_identifiers() { | |
| // check that quoted identifiers in any position remain quoted after serialization | |
| let select = redshift().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, | |
| .. | |
| } => { | |
| assert_eq!( | |
| ObjectName::from(vec![Ident::with_quote('"', "a table")]), | |
| name | |
| ); | |
| 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::from(vec![Ident::with_quote('"', "myfun")]), | |
| uses_odbc_syntax: false, | |
| 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"), | |
| } | |
| redshift().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); | |
| // An alias starting with a number | |
| redshift().verified_stmt(r#"CREATE TABLE "foo" ("1" INT)"#); | |
| redshift().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); | |
| //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); | |
| } | |
| fn redshift() -> TestedDialects { | |
| TestedDialects::new(vec![Box::new(RedshiftSqlDialect {})]) | |
| } | |
| fn redshift_and_generic() -> TestedDialects { | |
| TestedDialects::new(vec![ | |
| Box::new(RedshiftSqlDialect {}), | |
| Box::new(GenericDialect {}), | |
| ]) | |
| } | |
| #[test] | |
| fn test_sharp() { | |
| let sql = "SELECT #_of_values"; | |
| let select = redshift().verified_only_select(sql); | |
| assert_eq!( | |
| SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("#_of_values"))), | |
| select.projection[0] | |
| ); | |
| } | |
| #[test] | |
| fn test_create_view_with_no_schema_binding() { | |
| redshift_and_generic() | |
| .verified_stmt("CREATE VIEW myevent AS SELECT eventname FROM event WITH NO SCHEMA BINDING"); | |
| } | |
| #[test] | |
| fn test_redshift_json_path() { | |
| let dialects = all_dialects_where(|d| d.supports_partiql()); | |
| let sql = "SELECT cust.c_orders[0].o_orderkey FROM customer_orders_lineitem"; | |
| let select = dialects.verified_only_select(sql); | |
| assert_eq!( | |
| &Expr::JsonAccess { | |
| value: Box::new(Expr::CompoundIdentifier(vec![ | |
| Ident::new("cust"), | |
| Ident::new("c_orders") | |
| ])), | |
| path: JsonPath { | |
| path: vec![ | |
| JsonPathElem::Bracket { | |
| key: Expr::value(number("0")) | |
| }, | |
| JsonPathElem::Dot { | |
| key: "o_orderkey".to_string(), | |
| quoted: false | |
| } | |
| ] | |
| } | |
| }, | |
| expr_from_projection(only(&select.projection)) | |
| ); | |
| let sql = "SELECT cust.c_orders[0]['id'] FROM customer_orders_lineitem"; | |
| let select = dialects.verified_only_select(sql); | |
| assert_eq!( | |
| &Expr::JsonAccess { | |
| value: Box::new(Expr::CompoundIdentifier(vec![ | |
| Ident::new("cust"), | |
| Ident::new("c_orders") | |
| ])), | |
| path: JsonPath { | |
| path: vec![ | |
| JsonPathElem::Bracket { | |
| key: Expr::value(number("0")) | |
| }, | |
| JsonPathElem::Bracket { | |
| key: Expr::Value( | |
| (Value::SingleQuotedString("id".to_owned())).with_empty_span() | |
| ) | |
| } | |
| ] | |
| } | |
| }, | |
| expr_from_projection(only(&select.projection)) | |
| ); | |
| let sql = "SELECT db1.sc1.tbl1.col1[0]['id'] FROM customer_orders_lineitem"; | |
| let select = dialects.verified_only_select(sql); | |
| assert_eq!( | |
| &Expr::JsonAccess { | |
| value: Box::new(Expr::CompoundIdentifier(vec![ | |
| Ident::new("db1"), | |
| Ident::new("sc1"), | |
| Ident::new("tbl1"), | |
| Ident::new("col1") | |
| ])), | |
| path: JsonPath { | |
| path: vec![ | |
| JsonPathElem::Bracket { | |
| key: Expr::value(number("0")) | |
| }, | |
| JsonPathElem::Bracket { | |
| key: Expr::Value( | |
| (Value::SingleQuotedString("id".to_owned())).with_empty_span() | |
| ) | |
| } | |
| ] | |
| } | |
| }, | |
| expr_from_projection(only(&select.projection)) | |
| ); | |
| let sql = r#"SELECT db1.sc1.tbl1.col1[0]."id" FROM customer_orders_lineitem"#; | |
| let select = dialects.verified_only_select(sql); | |
| assert_eq!( | |
| &Expr::JsonAccess { | |
| value: Box::new(Expr::CompoundIdentifier(vec![ | |
| Ident::new("db1"), | |
| Ident::new("sc1"), | |
| Ident::new("tbl1"), | |
| Ident::new("col1") | |
| ])), | |
| path: JsonPath { | |
| path: vec![ | |
| JsonPathElem::Bracket { | |
| key: Expr::value(number("0")) | |
| }, | |
| JsonPathElem::Dot { | |
| key: "id".to_string(), | |
| quoted: true, | |
| } | |
| ] | |
| } | |
| }, | |
| expr_from_projection(only(&select.projection)) | |
| ); | |
| } | |
| #[test] | |
| fn test_parse_json_path_from() { | |
| let dialects = all_dialects_where(|d| d.supports_partiql()); | |
| let select = dialects.verified_only_select("SELECT * FROM src[0].a AS a"); | |
| match &select.from[0].relation { | |
| TableFactor::Table { | |
| name, json_path, .. | |
| } => { | |
| assert_eq!(name, &ObjectName::from(vec![Ident::new("src")])); | |
| assert_eq!( | |
| json_path, | |
| &Some(JsonPath { | |
| path: vec![ | |
| JsonPathElem::Bracket { | |
| key: Expr::value(number("0")) | |
| }, | |
| JsonPathElem::Dot { | |
| key: "a".to_string(), | |
| quoted: false | |
| } | |
| ] | |
| }) | |
| ); | |
| } | |
| _ => panic!(), | |
| } | |
| let select = dialects.verified_only_select("SELECT * FROM src[0].a[1].b AS a"); | |
| match &select.from[0].relation { | |
| TableFactor::Table { | |
| name, json_path, .. | |
| } => { | |
| assert_eq!(name, &ObjectName::from(vec![Ident::new("src")])); | |
| assert_eq!( | |
| json_path, | |
| &Some(JsonPath { | |
| path: vec![ | |
| JsonPathElem::Bracket { | |
| key: Expr::value(number("0")) | |
| }, | |
| JsonPathElem::Dot { | |
| key: "a".to_string(), | |
| quoted: false | |
| }, | |
| JsonPathElem::Bracket { | |
| key: Expr::Value( | |
| (Value::Number("1".parse().unwrap(), false)).with_empty_span() | |
| ) | |
| }, | |
| JsonPathElem::Dot { | |
| key: "b".to_string(), | |
| quoted: false | |
| }, | |
| ] | |
| }) | |
| ); | |
| } | |
| _ => panic!(), | |
| } | |
| let select = dialects.verified_only_select("SELECT * FROM src.a.b"); | |
| match &select.from[0].relation { | |
| TableFactor::Table { | |
| name, json_path, .. | |
| } => { | |
| assert_eq!( | |
| name, | |
| &ObjectName::from(vec![Ident::new("src"), Ident::new("a"), Ident::new("b")]) | |
| ); | |
| assert_eq!(json_path, &None); | |
| } | |
| _ => panic!(), | |
| } | |
| } | |
| #[test] | |
| fn test_parse_select_numbered_columns() { | |
| // An alias starting with a number | |
| redshift_and_generic().verified_stmt(r#"SELECT 1 AS "1" FROM a"#); | |
| redshift_and_generic().verified_stmt(r#"SELECT 1 AS "1abc" FROM a"#); | |
| } | |
| #[test] | |
| fn test_parse_nested_quoted_identifier() { | |
| redshift().verified_stmt(r#"SELECT 1 AS ["1"] FROM a"#); | |
| redshift().verified_stmt(r#"SELECT 1 AS ["[="] FROM a"#); | |
| redshift().verified_stmt(r#"SELECT 1 AS ["=]"] FROM a"#); | |
| redshift().verified_stmt(r#"SELECT 1 AS ["a[b]"] FROM a"#); | |
| // trim spaces | |
| redshift().one_statement_parses_to(r#"SELECT 1 AS [ " 1 " ]"#, r#"SELECT 1 AS [" 1 "]"#); | |
| // invalid query | |
| assert_eq!( | |
| redshift().parse_sql_statements(r#"SELECT 1 AS ["1]"#), | |
| Err(ParserError::TokenizerError( | |
| "Expected close delimiter '\"' before EOF. at Line: 1, Column: 14".to_string() | |
| )) | |
| ); | |
| } | |
| #[test] | |
| fn parse_extract_single_quotes() { | |
| let sql = "SELECT EXTRACT('month' FROM my_timestamp) FROM my_table"; | |
| redshift().verified_stmt(sql); | |
| } | |
| #[test] | |
| 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 🚀"); | |
| } | |
| #[test] | |
| fn parse_vacuum() { | |
| let stmt = redshift().verified_stmt("VACUUM FULL"); | |
| match stmt { | |
| Statement::Vacuum(v) => { | |
| assert!(v.full); | |
| assert_eq!(v.table_name, None); | |
| } | |
| _ => unreachable!(), | |
| } | |
| let stmt = redshift().verified_stmt("VACUUM tbl"); | |
| match stmt { | |
| Statement::Vacuum(v) => { | |
| assert_eq!( | |
| v.table_name, | |
| Some(ObjectName::from(vec![Ident::new("tbl"),])) | |
| ); | |
| } | |
| _ => unreachable!(), | |
| } | |
| let stmt = redshift().verified_stmt( | |
| "VACUUM FULL SORT ONLY DELETE ONLY REINDEX RECLUSTER db1.sc1.tbl1 TO 20 PERCENT BOOST", | |
| ); | |
| match stmt { | |
| Statement::Vacuum(v) => { | |
| assert!(v.full); | |
| assert!(v.sort_only); | |
| assert!(v.delete_only); | |
| assert!(v.reindex); | |
| assert!(v.recluster); | |
| assert_eq!( | |
| v.table_name, | |
| Some(ObjectName::from(vec![ | |
| Ident::new("db1"), | |
| Ident::new("sc1"), | |
| Ident::new("tbl1"), | |
| ])) | |
| ); | |
| assert_eq!(v.threshold, Some(number("20").with_empty_span())); | |
| assert!(v.boost); | |
| } | |
| _ => unreachable!(), | |
| } | |
| } | |
| #[test] | |
| fn test_create_table_diststyle_distkey() { | |
| redshift().verified_stmt( | |
| "CREATE TEMPORARY TABLE tmp_sbk_summary_pp DISTSTYLE KEY DISTKEY(bet_id) AS SELECT 1 AS bet_id", | |
| ); | |
| } | |
| #[test] | |
| fn test_create_table_diststyle() { | |
| redshift().verified_stmt("CREATE TABLE t1 (c1 INT) DISTSTYLE AUTO"); | |
| redshift().verified_stmt("CREATE TABLE t1 (c1 INT) DISTSTYLE EVEN"); | |
| redshift().verified_stmt("CREATE TABLE t1 (c1 INT) DISTSTYLE KEY DISTKEY(c1)"); | |
| redshift().verified_stmt("CREATE TABLE t1 (c1 INT) DISTSTYLE ALL"); | |
| } | |
| #[test] | |
| fn test_copy_credentials() { | |
| redshift().verified_stmt( | |
| "COPY t1 FROM 's3://bucket/file.csv' CREDENTIALS 'aws_access_key_id=AK;aws_secret_access_key=SK' CSV", | |
| ); | |
| } | |
| #[test] | |
| fn test_create_table_sortkey() { | |
| redshift().verified_stmt("CREATE TABLE t1 (c1 INT, c2 INT, c3 TIMESTAMP) SORTKEY(c3)"); | |
| redshift().verified_stmt("CREATE TABLE t1 (c1 INT, c2 INT) SORTKEY(c1, c2)"); | |
| } | |
| #[test] | |
| fn test_create_table_distkey_sortkey_with_ctas() { | |
| redshift().verified_stmt( | |
| "CREATE TABLE t1 DISTKEY(1) SORTKEY(1, 3) AS SELECT eventid, venueid, dateid, eventname FROM event", | |
| ); | |
| } | |
| #[test] | |
| fn test_create_table_diststyle_distkey_sortkey() { | |
| redshift().verified_stmt( | |
| "CREATE TABLE t1 (c1 INT, c2 INT) DISTSTYLE KEY DISTKEY(c1) SORTKEY(c1, c2)", | |
| ); | |
| } | |
| #[test] | |
| fn test_alter_table_alter_sortkey() { | |
| redshift().verified_stmt("ALTER TABLE users ALTER SORTKEY(created_at)"); | |
| redshift().verified_stmt("ALTER TABLE users ALTER SORTKEY(c1, c2)"); | |
| } | |
| #[test] | |
| fn test_create_table_backup() { | |
| redshift().verified_stmt("CREATE TABLE public.users (id INT, name VARCHAR(255)) BACKUP YES"); | |
| redshift().verified_stmt("CREATE TABLE staging.events (event_id INT) BACKUP NO"); | |
| redshift().verified_stmt( | |
| "CREATE TABLE public.users_backup_test BACKUP YES DISTSTYLE AUTO AS SELECT id, name, email FROM public.users", | |
| ); | |
| } | |
| #[test] | |
| fn test_null_treatment_inside_and_outside_window_function() { | |
| redshift().verified_stmt("SELECT FIRST_VALUE(1 IGNORE NULLS) OVER () FROM (SELECT 1) t"); | |
| redshift().verified_stmt("SELECT FIRST_VALUE(1) IGNORE NULLS OVER () FROM (SELECT 1) t"); | |
| } | |
| #[test] | |
| fn test_partiql_from_alias_with_at_index() { | |
| let dialects = all_dialects_where(|d| d.supports_partiql()); | |
| dialects.verified_stmt("SELECT * FROM lineitem AS l (a, b, c) AT idx"); | |
| let sql = | |
| "SELECT index, val FROM (SELECT array('AAA', 'BBB') AS val) AS b, b.val AS val AT index"; | |
| let select = dialects.verified_only_select(sql); | |
| match &select.from[1].relation { | |
| TableFactor::Table { name, alias, .. } => { | |
| assert_eq!( | |
| name, | |
| &ObjectName::from(vec![Ident::new("b"), Ident::new("val")]) | |
| ); | |
| assert_eq!(alias.as_ref().map(|a| &a.name), Some(&Ident::new("val"))); | |
| assert_eq!( | |
| alias.as_ref().and_then(|a| a.at.as_ref()), | |
| Some(&Ident::new("index")) | |
| ); | |
| } | |
| _ => panic!("expected table factor"), | |
| } | |
| } | |
| #[test] | |
| fn parse_unpivot_expression() { | |
| let dialects = all_dialects_where(|d| d.supports_unpivot_expr()); | |
| dialects.verified_stmt( | |
| "SELECT t.id, k, v FROM test_colors AS t, UNPIVOT t.count_by_color AS v AT k", | |
| ); | |
| dialects.verified_stmt("SELECT t.id, k, v FROM test_colors AS t, UNPIVOT t AS v AT k"); | |
| } | |
| #[test] | |
| fn test_interval_as_column_name() { | |
| redshift().verified_stmt("SELECT * FROM table_name WHERE interval = 78"); | |
| } | |
| #[test] | |
| fn parse_approximate_percentile_disc() { | |
| let supporting = all_dialects_where(|d| d.supports_approximate_percentile_disc()); | |
| supporting.verified_stmt( | |
| "SELECT APPROXIMATE PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY totalprice)", | |
| ); | |
| // Without dialect support, `APPROXIMATE` is just a column name and | |
| // `PERCENTILE_DISC` its implicit alias. | |
| let non_supporting = all_dialects_where(|d| !d.supports_approximate_percentile_disc()); | |
| non_supporting.one_statement_parses_to( | |
| "SELECT APPROXIMATE PERCENTILE_DISC FROM t", | |
| "SELECT APPROXIMATE AS PERCENTILE_DISC FROM t", | |
| ); | |
| } |