mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-07-08 01:15:00 +00:00
452 lines
16 KiB
Rust
452 lines
16 KiB
Rust
// Licensed 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::*;
|
|
use sqlparser::dialect::{GenericDialect, MsSqlDialect};
|
|
|
|
#[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_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,
|
|
offset: None,
|
|
fetch: None,
|
|
locks: vec![],
|
|
order_by: vec![],
|
|
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![],
|
|
selection: None,
|
|
group_by: vec![],
|
|
cluster_by: vec![],
|
|
distribute_by: vec![],
|
|
sort_by: vec![],
|
|
having: None,
|
|
named_window: vec![],
|
|
qualify: 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 {
|
|
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(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(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(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(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_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,
|
|
} => {
|
|
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());
|
|
}
|
|
_ => 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")]),
|
|
args: vec![],
|
|
over: None,
|
|
distinct: false,
|
|
special: false,
|
|
order_by: 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_like() {
|
|
fn chk(negated: bool) {
|
|
let sql = &format!(
|
|
"SELECT * FROM customers WHERE name {}LIKE '%a'",
|
|
if negated { "NOT " } else { "" }
|
|
);
|
|
let select = ms_and_generic().verified_only_select(sql);
|
|
assert_eq!(
|
|
Expr::Like {
|
|
expr: Box::new(Expr::Identifier(Ident::new("name"))),
|
|
negated,
|
|
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
|
|
escape_char: None,
|
|
},
|
|
select.selection.unwrap()
|
|
);
|
|
|
|
// Test with escape char
|
|
let sql = &format!(
|
|
"SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'",
|
|
if negated { "NOT " } else { "" }
|
|
);
|
|
let select = ms_and_generic().verified_only_select(sql);
|
|
assert_eq!(
|
|
Expr::Like {
|
|
expr: Box::new(Expr::Identifier(Ident::new("name"))),
|
|
negated,
|
|
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
|
|
escape_char: Some('\\'),
|
|
},
|
|
select.selection.unwrap()
|
|
);
|
|
|
|
// This statement tests that LIKE and NOT LIKE have the same precedence.
|
|
// This was previously mishandled (#81).
|
|
let sql = &format!(
|
|
"SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL",
|
|
if negated { "NOT " } else { "" }
|
|
);
|
|
let select = ms_and_generic().verified_only_select(sql);
|
|
assert_eq!(
|
|
Expr::IsNull(Box::new(Expr::Like {
|
|
expr: Box::new(Expr::Identifier(Ident::new("name"))),
|
|
negated,
|
|
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
|
|
escape_char: None,
|
|
})),
|
|
select.selection.unwrap()
|
|
);
|
|
}
|
|
chk(false);
|
|
chk(true);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_similar_to() {
|
|
fn chk(negated: bool) {
|
|
let sql = &format!(
|
|
"SELECT * FROM customers WHERE name {}SIMILAR TO '%a'",
|
|
if negated { "NOT " } else { "" }
|
|
);
|
|
let select = ms_and_generic().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()))),
|
|
escape_char: None,
|
|
},
|
|
select.selection.unwrap()
|
|
);
|
|
|
|
// Test with escape char
|
|
let sql = &format!(
|
|
"SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'",
|
|
if negated { "NOT " } else { "" }
|
|
);
|
|
let select = ms_and_generic().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()))),
|
|
escape_char: Some('\\'),
|
|
},
|
|
select.selection.unwrap()
|
|
);
|
|
|
|
// This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence.
|
|
let sql = &format!(
|
|
"SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL",
|
|
if negated { "NOT " } else { "" }
|
|
);
|
|
let select = ms_and_generic().verified_only_select(sql);
|
|
assert_eq!(
|
|
Expr::IsNull(Box::new(Expr::SimilarTo {
|
|
expr: Box::new(Expr::Identifier(Ident::new("name"))),
|
|
negated,
|
|
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
|
|
escape_char: Some('\\'),
|
|
})),
|
|
select.selection.unwrap()
|
|
);
|
|
}
|
|
chk(false);
|
|
chk(true);
|
|
}
|
|
|
|
#[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![]
|
|
},
|
|
joins: vec![]
|
|
}],
|
|
lateral_views: vec![],
|
|
selection: None,
|
|
group_by: vec![],
|
|
cluster_by: vec![],
|
|
distribute_by: vec![],
|
|
sort_by: vec![],
|
|
having: None,
|
|
named_window: vec![],
|
|
qualify: None
|
|
}))),
|
|
order_by: vec![],
|
|
limit: None,
|
|
offset: None,
|
|
fetch: None,
|
|
locks: vec![],
|
|
}),
|
|
query
|
|
);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|