diff --git a/CHANGELOG.md b/CHANGELOG.md index aee2df636..bd258e6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,38 @@ Given that the parser produces a typed AST, any changes to the AST will technica Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.17.0] 2022-05-09 + +### Added + + + +* Support `#` as first character in field name for `RedShift` dialect (#485) - Thanks @yuval-illumex +* Support for postgres composite types (#466) - Thanks @poonai +* Support `TABLE` keyword with SELECT INTO (#487) - Thanks @MazterQyou +* Support `ANY`/`ALL` operators (#477) - Thanks @ovr +* Support `ArrayIndex` in `GenericDialect` (#480) - Thanks @ovr +* Support `Redshift` dialect, handle square brackets properly (#471) - Thanks @mskrzypkows +* Support `KILL` statement (#479) - Thanks @ovr +* Support `QUALIFY` clause on `SELECT` for `Snowflake` dialect (#465) - Thanks @mobuchowski +* Support `POSITION(x IN y)` function syntax (#463) @yuval-illumex +* Support `global`,`local`, `on commit` for `create temporary table` (#456) - Thanks @gandronchik +* Support `NVARCHAR` data type (#462) - Thanks @yuval-illumex +* Support for postgres json operators `->`, `->>`, `#>`, and `#>>` (#458) - Thanks @poonai +* Support `SET ROLE` statement (#455) - Thanks @slhmy + +### Changed: +* Improve docstrings for `KILL` statement (#481) - Thanks @alamb +* Add negative tests for `POSITION` (#469) - Thanks @alamb +* Add negative tests for `IN` parsing (#468) - Thanks @alamb +* Suppport table names (as well as subqueries) as source in `MERGE` statements (#483) - Thanks @mskrzypkows + + +### Fixed: +* `INTO` keyword is optional for `INSERT`, `MERGE` (#473) - Thanks @mobuchowski +* Support `IS TRUE` and `IS FALSE` expressions in boolean filter (#474) - Thanks @yuval-illumex +* Support fully qualified object names in `SET VARIABLE` (#484) - Thanks mobuchowski + ## [0.16.0] 2022-04-03 ### Added @@ -65,8 +97,6 @@ Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented * export all methods of parser (#397) - Thanks @neverchanje! * Clarify maintenance status on README (#416) - @alamb -@panarch - ### Fixed * Fix new clippy errors (#412) - @alamb * Fix panic with `GRANT/REVOKE` in `CONNECT`, `CREATE`, `EXECUTE` or `TEMPORARY` - Thanks @evgenyx00 diff --git a/Cargo.toml b/Cargo.toml index 94fd9f394..00b4d1a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.16.0" +version = "0.17.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/examples/cli.rs b/examples/cli.rs index 38b3de841..fcaf1b144 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -38,11 +38,13 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] let dialect: Box = match std::env::args().nth(2).unwrap_or_default().as_ref() { "--ansi" => Box::new(AnsiDialect {}), + "--bigquery" => Box::new(BigQueryDialect {}), "--postgres" => Box::new(PostgreSqlDialect {}), "--ms" => Box::new(MsSqlDialect {}), "--mysql" => Box::new(MySqlDialect {}), "--snowflake" => Box::new(SnowflakeDialect {}), "--hive" => Box::new(HiveDialect {}), + "--redshift" => Box::new(RedshiftSqlDialect {}), "--generic" | "" => Box::new(GenericDialect {}), s => panic!("Unexpected parameter: {}", s), }; diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index d07d1ebdf..9ea193d98 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -71,6 +71,8 @@ pub enum DataType { Date, /// Time Time, + /// Datetime + Datetime, /// Timestamp Timestamp, /// Interval @@ -143,6 +145,7 @@ impl fmt::Display for DataType { DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), DataType::Time => write!(f, "TIME"), + DataType::Datetime => write!(f, "DATETIME"), DataType::Timestamp => write!(f, "TIMESTAMP"), DataType::Interval => write!(f, "INTERVAL"), DataType::Regclass => write!(f, "REGCLASS"), diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4d8b3b5bf..1847f2518 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -14,7 +14,7 @@ //! (commonly referred to as Data Definition Language, or DDL) #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, string::ToString, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; use core::fmt; #[cfg(feature = "serde")] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7300ed488..75d97f7f8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -23,7 +23,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use core::fmt::{self, Write}; +use core::fmt; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -128,16 +128,8 @@ impl fmt::Display for Ident { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.quote_style { Some(q) if q == '"' || q == '\'' || q == '`' => { - f.write_char(q)?; - let mut first = true; - for s in self.value.split_inclusive(q) { - if !first { - f.write_char(q)?; - } - first = false; - f.write_str(s)?; - } - f.write_char(q) + let escaped = value::escape_quoted_string(&self.value, q); + write!(f, "{}{}{}", q, escaped, q) } Some(q) if q == '[' => write!(f, "[{}]", self.value), None => f.write_str(&self.value), @@ -231,6 +223,12 @@ pub enum Expr { operator: JsonOperator, right: Box, }, + /// CompositeAccess (postgres) eg: SELECT (information_schema._pg_expandarray(array['i','i'])).n + CompositeAccess { expr: Box, key: Ident }, + /// `IS FALSE` operator + IsFalse(Box), + /// `IS TRUE` operator + IsTrue(Box), /// `IS NULL` operator IsNull(Box), /// `IS NOT NULL` operator @@ -270,11 +268,12 @@ pub enum Expr { op: BinaryOperator, right: Box, }, + /// Any operation e.g. `1 ANY (1)` or `foo > ANY(bar)`, It will be wrapped in the right side of BinaryExpr + AnyOp(Box), + /// ALL operation e.g. `1 ALL (1)` or `foo > ALL(bar)`, It will be wrapped in the right side of BinaryExpr + AllOp(Box), /// Unary operation e.g. `NOT foo` - UnaryOp { - op: UnaryOperator, - expr: Box, - }, + UnaryOp { op: UnaryOperator, expr: Box }, /// CAST an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))` Cast { expr: Box, @@ -293,10 +292,7 @@ pub enum Expr { expr: Box, }, /// POSITION( in ) - Position { - expr: Box, - r#in: Box, - }, + Position { expr: Box, r#in: Box }, /// SUBSTRING( [FROM ] [FOR ]) Substring { expr: Box, @@ -323,14 +319,12 @@ pub enum Expr { /// A constant of form ` 'value'`. /// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`), /// as well as constants of other types (a non-standard PostgreSQL extension). - TypedString { - data_type: DataType, - value: String, - }, - MapAccess { - column: Box, - keys: Vec, - }, + TypedString { data_type: DataType, value: String }, + /// Access a map-like object by field (e.g. `column['field']` or `column[4]` + /// Note that depending on the dialect, struct like accesses may be + /// parsed as [`ArrayIndex`] or [`MapAccess`] + /// + MapAccess { column: Box, keys: Vec }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), /// `CASE [] WHEN THEN ... [ELSE ] END` @@ -361,10 +355,7 @@ pub enum Expr { /// ROW / TUPLE a single value, such as `SELECT (1, 2)` Tuple(Vec), /// An array index expression e.g. `(ARRAY[1, 2])[1]` or `(current_schemas(FALSE))[1]` - ArrayIndex { - obj: Box, - indexs: Vec, - }, + ArrayIndex { obj: Box, indexes: Vec }, /// An array expression e.g. `ARRAY[1, 2]` Array(Array), } @@ -385,6 +376,8 @@ impl fmt::Display for Expr { Ok(()) } Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), + Expr::IsTrue(ast) => write!(f, "{} IS TRUE", ast), + Expr::IsFalse(ast) => write!(f, "{} IS FALSE", ast), Expr::IsNull(ast) => write!(f, "{} IS NULL", ast), Expr::IsNotNull(ast) => write!(f, "{} IS NOT NULL", ast), Expr::InList { @@ -434,6 +427,8 @@ impl fmt::Display for Expr { high ), Expr::BinaryOp { left, op, right } => write!(f, "{} {} {}", left, op, right), + Expr::AnyOp(expr) => write!(f, "ANY({})", expr), + Expr::AllOp(expr) => write!(f, "ALL({})", expr), Expr::UnaryOp { op, expr } => { if op == &UnaryOperator::PGPostfixFactorial { write!(f, "{}{}", expr, op) @@ -545,9 +540,9 @@ impl fmt::Display for Expr { Expr::Tuple(exprs) => { write!(f, "({})", display_comma_separated(exprs)) } - Expr::ArrayIndex { obj, indexs } => { + Expr::ArrayIndex { obj, indexes } => { write!(f, "{}", obj)?; - for i in indexs { + for i in indexes { write!(f, "[{}]", i)?; } Ok(()) @@ -562,6 +557,9 @@ impl fmt::Display for Expr { } => { write!(f, "{} {} {}", left, operator, right) } + Expr::CompositeAccess { expr, key } => { + write!(f, "{}.{}", expr, key) + } } } } @@ -767,6 +765,8 @@ pub enum Statement { Insert { /// Only for Sqlite or: Option, + /// INTO - optional keyword + into: bool, /// TABLE table_name: ObjectName, /// COLUMNS @@ -898,6 +898,22 @@ pub enum Statement { /// deleted along with the dropped table purge: bool, }, + /// FETCH - retrieve rows from a query using a cursor + /// + /// Note: this is a PostgreSQL-specific statement, + /// but may also compatible with other SQL. + Fetch { + /// Cursor name + name: Ident, + direction: FetchDirection, + /// Optional, It's possible to fetch rows form cursor to the table + into: Option, + }, + /// DISCARD [ ALL | PLANS | SEQUENCES | TEMPORARY | TEMP ] + /// + /// Note: this is a PostgreSQL-specific statement, + /// but may also compatible with other SQL. + Discard { object_type: DiscardObject }, /// SET [ SESSION | LOCAL ] ROLE role_name /// /// Note: this is a PostgreSQL-specific statement, @@ -916,7 +932,7 @@ pub enum Statement { SetVariable { local: bool, hivevar: bool, - variable: Ident, + variable: ObjectName, value: Vec, }, /// SHOW @@ -980,6 +996,15 @@ pub enum Statement { location: Option, managed_location: Option, }, + /// CREATE FUNCTION + /// + /// Hive: https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction + CreateFunction { + temporary: bool, + name: ObjectName, + class_name: String, + using: Option, + }, /// `ASSERT [AS ]` Assert { condition: Expr, @@ -1017,6 +1042,15 @@ pub enum Statement { data_types: Vec, statement: Box, }, + /// KILL [CONNECTION | QUERY | MUTATION] + /// + /// See + /// See + Kill { + modifier: Option, + // processlist_id + id: u64, + }, /// EXPLAIN TABLE /// Note: this is a MySQL-specific statement. See ExplainTable { @@ -1040,12 +1074,12 @@ pub enum Statement { Savepoint { name: Ident }, // MERGE INTO statement, based on Snowflake. See Merge { + // optional INTO keyword + into: bool, // Specifies the table to merge table: TableFactor, // Specifies the table or subquery to join with the target table - source: Box, - // Specifies alias to the table that is joined with target table - alias: Option, + source: TableFactor, // Specifies the expression on which to join the target table and source on: Box, // Specifies the actions to perform when values match or do not match. @@ -1059,6 +1093,15 @@ impl fmt::Display for Statement { #[allow(clippy::cognitive_complexity)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + Statement::Kill { modifier, id } => { + write!(f, "KILL ")?; + + if let Some(m) = modifier { + write!(f, "{} ", m)?; + } + + write!(f, "{}", id) + } Statement::ExplainTable { describe_alias, table_name, @@ -1094,6 +1137,21 @@ impl fmt::Display for Statement { write!(f, "{}", statement) } Statement::Query(s) => write!(f, "{}", s), + Statement::Fetch { + name, + direction, + into, + } => { + write!(f, "FETCH {} ", direction)?; + + write!(f, "IN {}", name)?; + + if let Some(into) = into { + write!(f, " INTO {}", into)?; + } + + Ok(()) + } Statement::Directory { overwrite, local, @@ -1176,6 +1234,7 @@ impl fmt::Display for Statement { } Statement::Insert { or, + into, table_name, overwrite, partitioned, @@ -1190,9 +1249,10 @@ impl fmt::Display for Statement { } else { write!( f, - "INSERT {act}{tbl} {table_name} ", + "INSERT{over}{int}{tbl} {table_name} ", table_name = table_name, - act = if *overwrite { "OVERWRITE" } else { "INTO" }, + over = if *overwrite { " OVERWRITE" } else { "" }, + int = if *into { " INTO" } else { "" }, tbl = if *table { " TABLE" } else { "" } )?; } @@ -1299,6 +1359,22 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateFunction { + temporary, + name, + class_name, + using, + } => { + write!( + f, + "CREATE {temp}FUNCTION {name} AS '{class_name}'", + temp = if *temporary { "TEMPORARY " } else { "" }, + )?; + if let Some(u) = using { + write!(f, " {}", u)?; + } + Ok(()) + } Statement::CreateView { name, or_replace, @@ -1551,6 +1627,10 @@ impl fmt::Display for Statement { if *cascade { " CASCADE" } else { "" }, if *purge { " PURGE" } else { "" } ), + Statement::Discard { object_type } => { + write!(f, "DISCARD {object_type}", object_type = object_type)?; + Ok(()) + } Statement::SetRole { local, session, @@ -1761,16 +1841,17 @@ impl fmt::Display for Statement { write!(f, "{}", name) } Statement::Merge { + into, table, source, - alias, on, clauses, } => { - write!(f, "MERGE INTO {} USING {} ", table, source)?; - if let Some(a) = alias { - write!(f, "as {} ", a)?; - }; + write!( + f, + "MERGE{int} {table} USING {source} ", + int = if *into { " INTO" } else { "" } + )?; write!(f, "ON {} ", on)?; write!(f, "{}", display_separated(clauses, " ")) } @@ -1834,6 +1915,69 @@ impl fmt::Display for Privileges { } } +/// Specific direction for FETCH statement +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum FetchDirection { + Count { limit: Value }, + Next, + Prior, + First, + Last, + Absolute { limit: Value }, + Relative { limit: Value }, + All, + // FORWARD + // FORWARD count + Forward { limit: Option }, + ForwardAll, + // BACKWARD + // BACKWARD count + Backward { limit: Option }, + BackwardAll, +} + +impl fmt::Display for FetchDirection { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FetchDirection::Count { limit } => f.write_str(&limit.to_string())?, + FetchDirection::Next => f.write_str("NEXT")?, + FetchDirection::Prior => f.write_str("PRIOR")?, + FetchDirection::First => f.write_str("FIRST")?, + FetchDirection::Last => f.write_str("LAST")?, + FetchDirection::Absolute { limit } => { + f.write_str("ABSOLUTE ")?; + f.write_str(&limit.to_string())?; + } + FetchDirection::Relative { limit } => { + f.write_str("RELATIVE ")?; + f.write_str(&limit.to_string())?; + } + FetchDirection::All => f.write_str("ALL")?, + FetchDirection::Forward { limit } => { + f.write_str("FORWARD")?; + + if let Some(l) = limit { + f.write_str(" ")?; + f.write_str(&l.to_string())?; + } + } + FetchDirection::ForwardAll => f.write_str("FORWARD ALL")?, + FetchDirection::Backward { limit } => { + f.write_str("BACKWARD")?; + + if let Some(l) = limit { + f.write_str(" ")?; + f.write_str(&l.to_string())?; + } + } + FetchDirection::BackwardAll => f.write_str("BACKWARD ALL")?, + }; + + Ok(()) + } +} + /// A privilege on a database object (table, sequence, etc.). #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -2127,6 +2271,26 @@ impl fmt::Display for ObjectType { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KillType { + Connection, + Query, + Mutation, +} + +impl fmt::Display for KillType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + // MySQL + KillType::Connection => "CONNECTION", + KillType::Query => "QUERY", + // Clickhouse supports Mutation + KillType::Mutation => "MUTATION", + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HiveDistributionStyle { @@ -2458,7 +2622,7 @@ impl fmt::Display for CopyLegacyCsvOption { } } -/// +/// #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MergeClause { @@ -2520,6 +2684,45 @@ impl fmt::Display for MergeClause { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum DiscardObject { + ALL, + PLANS, + SEQUENCES, + TEMP, +} + +impl fmt::Display for DiscardObject { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DiscardObject::ALL => f.write_str("ALL"), + DiscardObject::PLANS => f.write_str("PLANS"), + DiscardObject::SEQUENCES => f.write_str("SEQUENCES"), + DiscardObject::TEMP => f.write_str("TEMP"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CreateFunctionUsing { + Jar(String), + File(String), + Archive(String), +} + +impl fmt::Display for CreateFunctionUsing { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "USING ")?; + match self { + CreateFunctionUsing::Jar(uri) => write!(f, "JAR '{uri}'"), + CreateFunctionUsing::File(uri) => write!(f, "FILE '{uri}'"), + CreateFunctionUsing::Archive(uri) => write!(f, "ARCHIVE '{uri}'"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/query.rs b/src/ast/query.rs index 82341f632..6212469f4 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -154,6 +154,8 @@ pub struct Select { pub sort_by: Vec, /// HAVING pub having: Option, + /// QUALIFY (Snowflake) + pub qualify: Option, } impl fmt::Display for Select { @@ -202,6 +204,9 @@ impl fmt::Display for Select { if let Some(ref having) = self.having { write!(f, " HAVING {}", having)?; } + if let Some(ref qualify) = self.qualify { + write!(f, " QUALIFY {}", qualify)?; + } Ok(()) } } @@ -332,7 +337,11 @@ pub enum TableFactor { /// Arguments of a table-valued function, as supported by Postgres /// and MSSQL. Note that deprecated MSSQL `FROM foo (NOLOCK)` syntax /// will also be parsed as `args`. - args: Vec, + /// + /// This field's value is `Some(v)`, where `v` is a (possibly empty) + /// vector of arguments, in the case of a table-valued function call, + /// whereas it's `None` in the case of a regular table name. + args: Option>, /// MSSQL-specific `WITH (...)` hints such as NOLOCK. with_hints: Vec, }, @@ -346,6 +355,19 @@ pub enum TableFactor { expr: Expr, alias: Option, }, + /// SELECT * FROM UNNEST ([10,20,30]) as numbers WITH OFFSET; + /// +---------+--------+ + /// | numbers | offset | + /// +---------+--------+ + /// | 10 | 0 | + /// | 20 | 1 | + /// | 30 | 2 | + /// +---------+--------+ + UNNEST { + alias: Option, + array_expr: Box, + with_offset: bool, + }, /// Represents a parenthesized table factor. The SQL spec only allows a /// join expression (`(foo bar [ baz ... ])`) to be nested, /// possibly several times. @@ -365,7 +387,7 @@ impl fmt::Display for TableFactor { with_hints, } => { write!(f, "{}", name)?; - if !args.is_empty() { + if let Some(args) = args { write!(f, "({})", display_comma_separated(args))?; } if let Some(alias) = alias { @@ -397,6 +419,20 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::UNNEST { + alias, + array_expr, + with_offset, + } => { + write!(f, "UNNEST({})", array_expr)?; + if let Some(alias) = alias { + write!(f, " AS {}", alias)?; + } + if *with_offset { + write!(f, " WITH OFFSET")?; + } + Ok(()) + } TableFactor::NestedJoin(table_reference) => write!(f, "({})", table_reference), } } @@ -649,6 +685,7 @@ impl fmt::Display for Values { pub struct SelectInto { pub temporary: bool, pub unlogged: bool, + pub table: bool, pub name: ObjectName, } @@ -656,7 +693,8 @@ impl fmt::Display for SelectInto { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let temporary = if self.temporary { " TEMPORARY" } else { "" }; let unlogged = if self.unlogged { " UNLOGGED" } else { "" }; + let table = if self.table { " TABLE" } else { "" }; - write!(f, "INTO{}{} {}", temporary, unlogged, self.name) + write!(f, "INTO{}{}{} {}", temporary, unlogged, table, self.name) } } diff --git a/src/ast/value.rs b/src/ast/value.rs index 1401855f1..9c32f27d5 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -30,6 +30,9 @@ pub enum Value { Number(BigDecimal, bool), /// 'string value' SingleQuotedString(String), + /// e'string value' (postgres extension) + /// write!(f, "{}{long}", v, long = if *l { "L" } else { "" }), Value::DoubleQuotedString(v) => write!(f, "\"{}\"", v), Value::SingleQuotedString(v) => write!(f, "'{}'", escape_single_quote_string(v)), + Value::EscapedStringLiteral(v) => write!(f, "E'{}'", escape_escaped_string(v)), Value::NationalStringLiteral(v) => write!(f, "N'{}'", v), Value::HexStringLiteral(v) => write!(f, "X'{}'", v), Value::Boolean(v) => write!(f, "{}", v), @@ -174,13 +178,16 @@ impl fmt::Display for DateTimeField { } } -pub struct EscapeSingleQuoteString<'a>(&'a str); +pub struct EscapeQuotedString<'a> { + string: &'a str, + quote: char, +} -impl<'a> fmt::Display for EscapeSingleQuoteString<'a> { +impl<'a> fmt::Display for EscapeQuotedString<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for c in self.0.chars() { - if c == '\'' { - write!(f, "\'\'")?; + for c in self.string.chars() { + if c == self.quote { + write!(f, "{q}{q}", q = self.quote)?; } else { write!(f, "{}", c)?; } @@ -189,8 +196,46 @@ impl<'a> fmt::Display for EscapeSingleQuoteString<'a> { } } -pub fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> { - EscapeSingleQuoteString(s) +pub fn escape_quoted_string(string: &str, quote: char) -> EscapeQuotedString<'_> { + EscapeQuotedString { string, quote } +} + +pub fn escape_single_quote_string(s: &str) -> EscapeQuotedString<'_> { + escape_quoted_string(s, '\'') +} + +pub struct EscapeEscapedStringLiteral<'a>(&'a str); + +impl<'a> fmt::Display for EscapeEscapedStringLiteral<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for c in self.0.chars() { + match c { + '\'' => { + write!(f, r#"\'"#)?; + } + '\\' => { + write!(f, r#"\\"#)?; + } + '\n' => { + write!(f, r#"\n"#)?; + } + '\t' => { + write!(f, r#"\t"#)?; + } + '\r' => { + write!(f, r#"\r"#)?; + } + _ => { + write!(f, "{}", c)?; + } + } + } + Ok(()) + } +} + +pub fn escape_escaped_string(s: &str) -> EscapeEscapedStringLiteral<'_> { + EscapeEscapedStringLiteral(s) } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs new file mode 100644 index 000000000..e42676b22 --- /dev/null +++ b/src/dialect/bigquery.rs @@ -0,0 +1,35 @@ +// 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. + +use crate::dialect::Dialect; + +#[derive(Debug, Default)] +pub struct BigQueryDialect; + +impl Dialect for BigQueryDialect { + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers + fn is_delimited_identifier_start(&self, ch: char) -> bool { + ch == '`' + } + + fn is_identifier_start(&self, ch: char) -> bool { + ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' + } + + fn is_identifier_part(&self, ch: char) -> bool { + ('a'..='z').contains(&ch) + || ('A'..='Z').contains(&ch) + || ('0'..='9').contains(&ch) + || ch == '_' + || ch == '-' + } +} diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 008b099d2..63821dd74 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -11,25 +11,31 @@ // limitations under the License. mod ansi; +mod bigquery; mod clickhouse; mod generic; mod hive; mod mssql; mod mysql; mod postgresql; +mod redshift; mod snowflake; mod sqlite; use core::any::{Any, TypeId}; use core::fmt::Debug; +use core::iter::Peekable; +use core::str::Chars; pub use self::ansi::AnsiDialect; +pub use self::bigquery::BigQueryDialect; pub use self::clickhouse::ClickHouseDialect; pub use self::generic::GenericDialect; pub use self::hive::HiveDialect; pub use self::mssql::MsSqlDialect; pub use self::mysql::MySqlDialect; pub use self::postgresql::PostgreSqlDialect; +pub use self::redshift::RedshiftSqlDialect; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; pub use crate::keywords; @@ -51,6 +57,10 @@ pub trait Dialect: Debug + Any { fn is_delimited_identifier_start(&self, ch: char) -> bool { ch == '"' } + /// Determine if quoted characters are proper for identifier + fn is_proper_identifier_inside_quotes(&self, mut _chars: Peekable>) -> bool { + true + } /// Determine if a character is a valid start character for an unquoted identifier fn is_identifier_start(&self, ch: char) -> bool; /// Determine if a character is a valid unquoted identifier character diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs new file mode 100644 index 000000000..c85f3dc20 --- /dev/null +++ b/src/dialect/redshift.rs @@ -0,0 +1,55 @@ +// 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. + +use crate::dialect::Dialect; +use core::iter::Peekable; +use core::str::Chars; + +use super::PostgreSqlDialect; + +#[derive(Debug)] +pub struct RedshiftSqlDialect {} + +// In most cases the redshift dialect is identical to [`PostgresSqlDialect`]. +// +// Notable differences: +// 1. Redshift treats brackets `[` and `]` differently. For example, `SQL SELECT a[1][2] FROM b` +// in the Postgres dialect, the query will be parsed as an array, while in the Redshift dialect it will +// be a json path +impl Dialect for RedshiftSqlDialect { + fn is_delimited_identifier_start(&self, ch: char) -> bool { + ch == '"' || ch == '[' + } + + /// Determine if quoted characters are proper for identifier + /// It's needed to distinguish treating square brackets as quotes from + /// treating them as json path. If there is identifier then we assume + /// there is no json path. + fn is_proper_identifier_inside_quotes(&self, mut chars: Peekable>) -> bool { + chars.next(); + let mut not_white_chars = chars.skip_while(|ch| ch.is_whitespace()).peekable(); + if let Some(&ch) = not_white_chars.peek() { + return self.is_identifier_start(ch); + } + false + } + + fn is_identifier_start(&self, ch: char) -> bool { + // Extends Postgres dialect with sharp + PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#' + } + + fn is_identifier_part(&self, ch: char) -> bool { + // Extends Postgres dialect with sharp + PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' + } +} diff --git a/src/keywords.rs b/src/keywords.rs index ad31bda5c..99cb83155 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -67,6 +67,7 @@ macro_rules! define_keywords { define_keywords!( ABORT, ABS, + ABSOLUTE, ACTION, ADD, ALL, @@ -76,6 +77,7 @@ define_keywords!( AND, ANY, APPLY, + ARCHIVE, ARE, ARRAY, ARRAY_AGG, @@ -92,6 +94,7 @@ define_keywords!( AUTO_INCREMENT, AVG, AVRO, + BACKWARD, BEGIN, BEGIN_FRAME, BEGIN_PARTITION, @@ -136,6 +139,7 @@ define_keywords!( COMPUTE, CONDITION, CONNECT, + CONNECTION, CONSTRAINT, CONTAINS, CONVERT, @@ -167,6 +171,7 @@ define_keywords!( DATA, DATABASE, DATE, + DATETIME, DAY, DEALLOCATE, DEC, @@ -183,6 +188,7 @@ define_keywords!( DESCRIBE, DETERMINISTIC, DIRECTORY, + DISCARD, DISCONNECT, DISTINCT, DISTRIBUTE, @@ -221,6 +227,7 @@ define_keywords!( FALSE, FETCH, FIELDS, + FILE, FILTER, FIRST, FIRST_VALUE, @@ -234,6 +241,7 @@ define_keywords!( FORCE_QUOTE, FOREIGN, FORMAT, + FORWARD, FRAME_ROW, FREE, FREEZE, @@ -275,10 +283,12 @@ define_keywords!( ISODOW, ISOLATION, ISOYEAR, + JAR, JOIN, JSONFILE, JULIAN, KEY, + KILL, LAG, LANGUAGE, LARGE, @@ -319,6 +329,7 @@ define_keywords!( MONTH, MSCK, MULTISET, + MUTATION, NATIONAL, NATURAL, NCHAR, @@ -368,6 +379,7 @@ define_keywords!( PERCENTILE_DISC, PERCENT_RANK, PERIOD, + PLANS, PORTION, POSITION, POSITION_REGEX, @@ -378,11 +390,14 @@ define_keywords!( PREPARE, PRESERVE, PRIMARY, + PRIOR, PRIVILEGES, PROCEDURE, PROGRAM, PURGE, + QUALIFY, QUARTER, + QUERY, QUOTE, RANGE, RANK, @@ -404,6 +419,7 @@ define_keywords!( REGR_SXX, REGR_SXY, REGR_SYY, + RELATIVE, RELEASE, RENAME, REPAIR, @@ -584,6 +600,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) Keyword::OUTER, Keyword::SET, + Keyword::QUALIFY, ]; /// Can't be used as a column alias, so that `SELECT alias` diff --git a/src/parser.rs b/src/parser.rs index 04024f8df..0c66e399d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -154,6 +154,7 @@ impl<'a> Parser<'a> { pub fn parse_statement(&mut self) -> Result { match self.next_token() { Token::Word(w) => match w.keyword { + Keyword::KILL => Ok(self.parse_kill()?), Keyword::DESCRIBE => Ok(self.parse_explain(true)?), Keyword::EXPLAIN => Ok(self.parse_explain(false)?), Keyword::ANALYZE => Ok(self.parse_analyze()?), @@ -165,6 +166,8 @@ impl<'a> Parser<'a> { Keyword::MSCK => Ok(self.parse_msck()?), Keyword::CREATE => Ok(self.parse_create()?), Keyword::DROP => Ok(self.parse_drop()?), + Keyword::DISCARD => Ok(self.parse_discard()?), + Keyword::FETCH => Ok(self.parse_fetch_statement()?), Keyword::DELETE => Ok(self.parse_delete()?), Keyword::INSERT => Ok(self.parse_insert()?), Keyword::UPDATE => Ok(self.parse_update()?), @@ -505,6 +508,11 @@ impl<'a> Parser<'a> { expr: Box::new(self.parse_subexpr(Self::PLUS_MINUS_PREC)?), }) } + Token::EscapedStringLiteral(_) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + { + self.prev_token(); + Ok(Expr::Value(self.parse_value()?)) + } Token::Number(_, _) | Token::SingleQuotedString(_) | Token::NationalStringLiteral(_) @@ -527,7 +535,19 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - Ok(expr) + if !self.consume_token(&Token::Period) { + Ok(expr) + } else { + let tok = self.next_token(); + let key = match tok { + Token::Word(word) => word.to_ident(), + _ => return parser_err!(format!("Expected identifier, found: {}", tok)), + }; + Ok(Expr::CompositeAccess { + expr: Box::new(expr), + key, + }) + } } Token::Placeholder(_) => { self.prev_token(); @@ -909,6 +929,7 @@ impl<'a> Parser<'a> { None } Token::SingleQuotedString(_) + | Token::EscapedStringLiteral(_) | Token::NationalStringLiteral(_) | Token::HexStringLiteral(_) => Some(Box::new(self.parse_expr()?)), unexpected => { @@ -1079,6 +1100,7 @@ impl<'a> Parser<'a> { /// Parse an operator following an expression pub fn parse_infix(&mut self, expr: Expr, precedence: u8) -> Result { let tok = self.next_token(); + let regular_binary_operator = match &tok { Token::Spaceship => Some(BinaryOperator::Spaceship), Token::DoubleEq => Some(BinaryOperator::Eq), @@ -1131,11 +1153,29 @@ impl<'a> Parser<'a> { }; if let Some(op) = regular_binary_operator { - Ok(Expr::BinaryOp { - left: Box::new(expr), - op, - right: Box::new(self.parse_subexpr(precedence)?), - }) + if let Some(keyword) = self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL]) { + self.expect_token(&Token::LParen)?; + let right = self.parse_subexpr(precedence)?; + self.expect_token(&Token::RParen)?; + + let right = match keyword { + Keyword::ALL => Box::new(Expr::AllOp(Box::new(right))), + Keyword::ANY => Box::new(Expr::AnyOp(Box::new(right))), + _ => unreachable!(), + }; + + Ok(Expr::BinaryOp { + left: Box::new(expr), + op, + right, + }) + } else { + Ok(Expr::BinaryOp { + left: Box::new(expr), + op, + right: Box::new(self.parse_subexpr(precedence)?), + }) + } } else if let Token::Word(w) = &tok { match w.keyword { Keyword::IS => { @@ -1143,6 +1183,10 @@ impl<'a> Parser<'a> { Ok(Expr::IsNull(Box::new(expr))) } else if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { Ok(Expr::IsNotNull(Box::new(expr))) + } else if self.parse_keywords(&[Keyword::TRUE]) { + Ok(Expr::IsTrue(Box::new(expr))) + } else if self.parse_keywords(&[Keyword::FALSE]) { + Ok(Expr::IsFalse(Box::new(expr))) } else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::FROM]) { let expr2 = self.parse_expr()?; Ok(Expr::IsDistinctFrom(Box::new(expr), Box::new(expr2))) @@ -1152,7 +1196,7 @@ impl<'a> Parser<'a> { Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2))) } else { self.expected( - "[NOT] NULL or [NOT] DISTINCT FROM after IS", + "[NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS", self.peek_token(), ) } @@ -1180,7 +1224,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), }) } else if Token::LBracket == tok { - if dialect_of!(self is PostgreSqlDialect) { + if dialect_of!(self is PostgreSqlDialect | GenericDialect) { // parse index return self.parse_array_index(expr); } @@ -1211,15 +1255,15 @@ impl<'a> Parser<'a> { pub fn parse_array_index(&mut self, expr: Expr) -> Result { let index = self.parse_expr()?; self.expect_token(&Token::RBracket)?; - let mut indexs: Vec = vec![index]; + let mut indexes: Vec = vec![index]; while self.consume_token(&Token::LBracket) { let index = self.parse_expr()?; self.expect_token(&Token::RBracket)?; - indexs.push(index); + indexes.push(index); } Ok(Expr::ArrayIndex { obj: Box::new(expr), - indexs, + indexes, }) } @@ -1593,6 +1637,8 @@ impl<'a> Parser<'a> { self.parse_create_schema() } else if self.parse_keyword(Keyword::DATABASE) { self.parse_create_database() + } else if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::FUNCTION) { + self.parse_create_function(temporary) } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -1649,6 +1695,42 @@ impl<'a> Parser<'a> { }) } + pub fn parse_optional_create_function_using( + &mut self, + ) -> Result, ParserError> { + if !self.parse_keyword(Keyword::USING) { + return Ok(None); + }; + let keyword = + self.expect_one_of_keywords(&[Keyword::JAR, Keyword::FILE, Keyword::ARCHIVE])?; + + let uri = self.parse_literal_string()?; + + match keyword { + Keyword::JAR => Ok(Some(CreateFunctionUsing::Jar(uri))), + Keyword::FILE => Ok(Some(CreateFunctionUsing::File(uri))), + Keyword::ARCHIVE => Ok(Some(CreateFunctionUsing::Archive(uri))), + _ => self.expected( + "JAR, FILE or ARCHIVE, got {:?}", + Token::make_keyword(format!("{:?}", keyword).as_str()), + ), + } + } + + pub fn parse_create_function(&mut self, temporary: bool) -> Result { + let name = self.parse_object_name()?; + self.expect_keyword(Keyword::AS)?; + let class_name = self.parse_literal_string()?; + let using = self.parse_optional_create_function_using()?; + + Ok(Statement::CreateFunction { + temporary, + name, + class_name, + using, + }) + } + pub fn parse_create_external_table( &mut self, or_replace: bool, @@ -1764,6 +1846,85 @@ impl<'a> Parser<'a> { }) } + // FETCH [ direction { FROM | IN } ] cursor INTO target; + pub fn parse_fetch_statement(&mut self) -> Result { + let direction = if self.parse_keyword(Keyword::NEXT) { + FetchDirection::Next + } else if self.parse_keyword(Keyword::PRIOR) { + FetchDirection::Prior + } else if self.parse_keyword(Keyword::FIRST) { + FetchDirection::First + } else if self.parse_keyword(Keyword::LAST) { + FetchDirection::Last + } else if self.parse_keyword(Keyword::ABSOLUTE) { + FetchDirection::Absolute { + limit: self.parse_number_value()?, + } + } else if self.parse_keyword(Keyword::RELATIVE) { + FetchDirection::Relative { + limit: self.parse_number_value()?, + } + } else if self.parse_keyword(Keyword::FORWARD) { + if self.parse_keyword(Keyword::ALL) { + FetchDirection::ForwardAll + } else { + FetchDirection::Forward { + // TODO: Support optional + limit: Some(self.parse_number_value()?), + } + } + } else if self.parse_keyword(Keyword::BACKWARD) { + if self.parse_keyword(Keyword::ALL) { + FetchDirection::BackwardAll + } else { + FetchDirection::Backward { + // TODO: Support optional + limit: Some(self.parse_number_value()?), + } + } + } else if self.parse_keyword(Keyword::ALL) { + FetchDirection::All + } else { + FetchDirection::Count { + limit: self.parse_number_value()?, + } + }; + + self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; + + let name = self.parse_identifier()?; + + let into = if self.parse_keyword(Keyword::INTO) { + Some(self.parse_object_name()?) + } else { + None + }; + + Ok(Statement::Fetch { + name, + direction, + into, + }) + } + + pub fn parse_discard(&mut self) -> Result { + let object_type = if self.parse_keyword(Keyword::ALL) { + DiscardObject::ALL + } else if self.parse_keyword(Keyword::PLANS) { + DiscardObject::PLANS + } else if self.parse_keyword(Keyword::SEQUENCES) { + DiscardObject::SEQUENCES + } else if self.parse_keyword(Keyword::TEMP) || self.parse_keyword(Keyword::TEMPORARY) { + DiscardObject::TEMP + } else { + return self.expected( + "ALL, PLANS, SEQUENCES, TEMP or TEMPORARY after DISCARD", + self.peek_token(), + ); + }; + Ok(Statement::Discard { object_type }) + } + pub fn parse_create_index(&mut self, unique: bool) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let index_name = self.parse_object_name()?; @@ -2543,6 +2704,7 @@ impl<'a> Parser<'a> { }, Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), Token::NationalStringLiteral(ref s) => Ok(Value::NationalStringLiteral(s.to_string())), + Token::EscapedStringLiteral(ref s) => Ok(Value::EscapedStringLiteral(s.to_string())), Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), Token::Placeholder(ref s) => Ok(Value::Placeholder(s.to_string())), unexpected => self.expected("a value", unexpected), @@ -2552,6 +2714,7 @@ impl<'a> Parser<'a> { pub fn parse_number_value(&mut self) -> Result { match self.parse_value()? { v @ Value::Number(_, _) => Ok(v), + v @ Value::Placeholder(_) => Ok(v), _ => { self.prev_token(); self.expected("literal number", self.peek_token()) @@ -2574,6 +2737,9 @@ impl<'a> Parser<'a> { match self.next_token() { Token::Word(Word { value, keyword, .. }) if keyword == Keyword::NoKeyword => Ok(value), Token::SingleQuotedString(s) => Ok(s), + Token::EscapedStringLiteral(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Ok(s) + } unexpected => self.expected("literal string", unexpected), } } @@ -2657,6 +2823,7 @@ impl<'a> Parser<'a> { } Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), + Keyword::DATETIME => Ok(DataType::Datetime), Keyword::TIMESTAMP => { // TBD: we throw away "with/without timezone" information if self.parse_keyword(Keyword::WITH) || self.parse_keyword(Keyword::WITHOUT) { @@ -2685,6 +2852,15 @@ impl<'a> Parser<'a> { } Keyword::ENUM => Ok(DataType::Enum(self.parse_string_values()?)), Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), + Keyword::ARRAY => { + // Hive array syntax. Note that nesting arrays - or other Hive syntax + // that ends with > will fail due to "C++" problem - >> is parsed as + // Token::ShiftRight + self.expect_token(&Token::Lt)?; + let inside_type = self.parse_data_type()?; + self.expect_token(&Token::Gt)?; + Ok(DataType::Array(Box::new(inside_type))) + } _ => { self.prev_token(); let type_name = self.parse_object_name()?; @@ -2694,7 +2870,8 @@ impl<'a> Parser<'a> { unexpected => self.expected("a data type name", unexpected), }?; - // Parse array data types. Note: this is postgresql-specific + // Parse array data types. Note: this is postgresql-specific and different from + // Keyword::ARRAY syntax from above while self.consume_token(&Token::LBracket) { self.expect_token(&Token::RBracket)?; data = DataType::Array(Box::new(data)) @@ -2897,6 +3074,32 @@ impl<'a> Parser<'a> { }) } + // KILL [CONNECTION | QUERY | MUTATION] processlist_id + pub fn parse_kill(&mut self) -> Result { + let modifier_keyword = + self.parse_one_of_keywords(&[Keyword::CONNECTION, Keyword::QUERY, Keyword::MUTATION]); + + let id = self.parse_literal_uint()?; + + let modifier = match modifier_keyword { + Some(Keyword::CONNECTION) => Some(KillType::Connection), + Some(Keyword::QUERY) => Some(KillType::Query), + Some(Keyword::MUTATION) => { + if dialect_of!(self is ClickHouseDialect | GenericDialect) { + Some(KillType::Mutation) + } else { + self.expected( + "Unsupported type for KILL, allowed: CONNECTION | QUERY", + self.peek_token(), + )? + } + } + _ => None, + }; + + Ok(Statement::Kill { modifier, id }) + } + pub fn parse_explain(&mut self, describe_alias: bool) -> Result { let analyze = self.parse_keyword(Keyword::ANALYZE); let verbose = self.parse_keyword(Keyword::VERBOSE); @@ -3111,10 +3314,12 @@ impl<'a> Parser<'a> { .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) .is_some(); let unlogged = self.parse_keyword(Keyword::UNLOGGED); + let table = self.parse_keyword(Keyword::TABLE); let name = self.parse_object_name()?; Some(SelectInto { temporary, unlogged, + table, name, }) } else { @@ -3131,6 +3336,7 @@ impl<'a> Parser<'a> { } else { vec![] }; + let mut lateral_views = vec![]; loop { if self.parse_keywords(&[Keyword::LATERAL, Keyword::VIEW]) { @@ -3198,6 +3404,12 @@ impl<'a> Parser<'a> { None }; + let qualify = if self.parse_keyword(Keyword::QUALIFY) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(Select { distinct, top, @@ -3211,6 +3423,7 @@ impl<'a> Parser<'a> { distribute_by, sort_by, having, + qualify, }) } @@ -3232,7 +3445,7 @@ impl<'a> Parser<'a> { }); } - let variable = self.parse_identifier()?; + let variable = self.parse_object_name()?; if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { let mut values = vec![]; loop { @@ -3240,6 +3453,16 @@ impl<'a> Parser<'a> { let value = match (self.parse_value(), token) { (Ok(value), _) => SetVariableValue::Literal(value), (Err(_), Token::Word(ident)) => SetVariableValue::Ident(ident.to_ident()), + (Err(_), Token::Minus) => { + let next_token = self.next_token(); + match next_token { + Token::Word(ident) => SetVariableValue::Ident(Ident { + quote_style: ident.quote_style, + value: format!("-{}", ident.value), + }), + _ => self.expected("word", next_token)?, + } + } (Err(_), unexpected) => self.expected("variable value", unexpected)?, }; values.push(value); @@ -3253,14 +3476,14 @@ impl<'a> Parser<'a> { value: values, }); } - } else if variable.value == "CHARACTERISTICS" { + } else if variable.to_string() == "CHARACTERISTICS" { self.expect_keywords(&[Keyword::AS, Keyword::TRANSACTION])?; Ok(Statement::SetTransaction { modes: self.parse_transaction_modes()?, snapshot: None, session: true, }) - } else if variable.value == "TRANSACTION" && modifier.is_none() { + } else if variable.to_string() == "TRANSACTION" && modifier.is_none() { if self.parse_keyword(Keyword::SNAPSHOT) { let snaphot_id = self.parse_value()?; return Ok(Statement::SetTransaction { @@ -3377,7 +3600,6 @@ impl<'a> Parser<'a> { pub fn parse_table_and_joins(&mut self) -> Result { let relation = self.parse_table_factor()?; - // Note that for keywords to be properly handled here, they need to be // added to `RESERVED_FOR_TABLE_ALIAS`, otherwise they may be parsed as // a table alias. @@ -3522,6 +3744,7 @@ impl<'a> Parser<'a> { match &mut table_and_joins.relation { TableFactor::Derived { alias, .. } | TableFactor::Table { alias, .. } + | TableFactor::UNNEST { alias, .. } | TableFactor::TableFunction { alias, .. } => { // but not `FROM (mytable AS alias1) AS alias2`. if let Some(inner_alias) = alias { @@ -3545,13 +3768,36 @@ impl<'a> Parser<'a> { // appearing alone in parentheses (e.g. `FROM (mytable)`) self.expected("joined table", self.peek_token()) } + } else if dialect_of!(self is BigQueryDialect | GenericDialect) + && self.parse_keyword(Keyword::UNNEST) + { + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + + let alias = match self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS) { + Ok(Some(alias)) => Some(alias), + Ok(None) => None, + Err(e) => return Err(e), + }; + + let with_offset = match self.expect_keywords(&[Keyword::WITH, Keyword::OFFSET]) { + Ok(()) => true, + Err(_) => false, + }; + + Ok(TableFactor::UNNEST { + alias, + array_expr: Box::new(expr), + with_offset, + }) } else { let name = self.parse_object_name()?; // Postgres, MSSQL: table-valued functions: let args = if self.consume_token(&Token::LParen) { - self.parse_optional_args()? + Some(self.parse_optional_args()?) } else { - vec![] + None }; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; // MSSQL-specific table hints: @@ -3783,8 +4029,11 @@ impl<'a> Parser<'a> { } else { None }; - let action = self.expect_one_of_keywords(&[Keyword::INTO, Keyword::OVERWRITE])?; - let overwrite = action == Keyword::OVERWRITE; + + let action = self.parse_one_of_keywords(&[Keyword::INTO, Keyword::OVERWRITE]); + let into = action == Some(Keyword::INTO); + let overwrite = action == Some(Keyword::OVERWRITE); + let local = self.parse_keyword(Keyword::LOCAL); if self.parse_keyword(Keyword::DIRECTORY) { @@ -3835,6 +4084,7 @@ impl<'a> Parser<'a> { Ok(Statement::Insert { or, table_name, + into, overwrite, partitioned, columns, @@ -4172,7 +4422,7 @@ impl<'a> Parser<'a> { pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { let mut clauses: Vec = vec![]; loop { - if self.peek_token() == Token::EOF { + if self.peek_token() == Token::EOF || self.peek_token() == Token::SemiColon { break; } self.expect_keyword(Keyword::WHEN)?; @@ -4247,21 +4497,20 @@ impl<'a> Parser<'a> { } pub fn parse_merge(&mut self) -> Result { - self.expect_keyword(Keyword::INTO)?; + let into = self.parse_keyword(Keyword::INTO); let table = self.parse_table_factor()?; self.expect_keyword(Keyword::USING)?; - let source = self.parse_query_body(0)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let source = self.parse_table_factor()?; self.expect_keyword(Keyword::ON)?; let on = self.parse_expr()?; let clauses = self.parse_merge_clauses()?; Ok(Statement::Merge { + into, table, - source: Box::new(source), - alias, + source, on: Box::new(on), clauses, }) @@ -4280,7 +4529,7 @@ impl Word { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::all_dialects; + use crate::test_utils::{all_dialects, TestedDialects}; #[test] fn test_prev_index() { @@ -4302,4 +4551,38 @@ mod tests { parser.prev_token(); }); } + + #[test] + fn test_parse_limit() { + let sql = "SELECT * FROM user LIMIT 1"; + all_dialects().run_parser_method(sql, |parser| { + let ast = parser.parse_query().unwrap(); + assert_eq!(ast.to_string(), sql.to_string()); + }); + + let sql = "SELECT * FROM user LIMIT $1 OFFSET $2"; + let dialects = TestedDialects { + dialects: vec![ + Box::new(PostgreSqlDialect {}), + Box::new(ClickHouseDialect {}), + Box::new(GenericDialect {}), + Box::new(MsSqlDialect {}), + Box::new(SnowflakeDialect {}), + ], + }; + + dialects.run_parser_method(sql, |parser| { + let ast = parser.parse_query().unwrap(); + assert_eq!(ast.to_string(), sql.to_string()); + }); + + let sql = "SELECT * FROM user LIMIT ? OFFSET ?"; + let dialects = TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + }; + dialects.run_parser_method(sql, |parser| { + let ast = parser.parse_query().unwrap(); + assert_eq!(ast.to_string(), sql.to_string()); + }); + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 27eba1408..deb3e2580 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -141,6 +141,7 @@ pub fn all_dialects() -> TestedDialects { Box::new(AnsiDialect {}), Box::new(SnowflakeDialect {}), Box::new(HiveDialect {}), + Box::new(RedshiftSqlDialect {}), ], } } @@ -176,7 +177,7 @@ pub fn table(name: impl Into) -> TableFactor { TableFactor::Table { name: ObjectName(vec![Ident::new(name.into())]), alias: None, - args: vec![], + args: None, with_hints: vec![], } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 71870dfe3..75241f496 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -51,6 +51,8 @@ pub enum Token { SingleQuotedString(String), /// "National" string literal: i.e: N'string' NationalStringLiteral(String), + /// "escaped" string literal, which are an extension to the SQL standard: i.e: e'first \n second' or E 'first \n second' + EscapedStringLiteral(String), /// Hexadecimal string literal: i.e.: X'deadbeef' HexStringLiteral(String), /// Comma @@ -162,6 +164,7 @@ impl fmt::Display for Token { Token::Char(ref c) => write!(f, "{}", c), Token::SingleQuotedString(ref s) => write!(f, "'{}'", s), Token::NationalStringLiteral(ref s) => write!(f, "N'{}'", s), + Token::EscapedStringLiteral(ref s) => write!(f, "E'{}'", s), Token::HexStringLiteral(ref s) => write!(f, "X'{}'", s), Token::Comma => f.write_str(","), Token::Whitespace(ws) => write!(f, "{}", ws), @@ -395,6 +398,21 @@ impl<'a> Tokenizer<'a> { } } } + // PostgreSQL accepts "escape" string constants, which are an extension to the SQL standard. + x @ 'e' | x @ 'E' => { + chars.next(); // consume, to check the next char + match chars.peek() { + Some('\'') => { + let s = self.tokenize_escaped_single_quoted_string(chars)?; + Ok(Some(Token::EscapedStringLiteral(s))) + } + _ => { + // regular identifier starting with an "E" or "e" + let s = self.tokenize_word(x, chars); + Ok(Some(Token::make_word(&s, None))) + } + } + } // The spec only allows an uppercase 'X' to introduce a hex // string, but PostgreSQL, at least, allows a lowercase 'x' too. x @ 'x' | x @ 'X' => { @@ -434,7 +452,12 @@ impl<'a> Tokenizer<'a> { Ok(Some(Token::SingleQuotedString(s))) } // delimited (quoted) identifier - quote_start if self.dialect.is_delimited_identifier_start(quote_start) => { + quote_start + if self.dialect.is_delimited_identifier_start(ch) + && self + .dialect + .is_proper_identifier_inside_quotes(chars.clone()) => + { chars.next(); // consume the opening quote let quote_end = Word::matching_end_quote(quote_start); let (s, last_char) = parse_quoted_ident(chars, quote_end); @@ -657,6 +680,10 @@ impl<'a> Tokenizer<'a> { ); Ok(Some(Token::Placeholder(String::from("$") + &s))) } + //whitespace check (including unicode chars) should be last as it covers some of the chars above + ch if ch.is_whitespace() => { + self.consume_and_return(chars, Token::Whitespace(Whitespace::Space)) + } other => self.consume_and_return(chars, Token::Char(other)), }, None => Ok(None), @@ -690,6 +717,66 @@ impl<'a> Tokenizer<'a> { s } + /// Read a single quoted string, starting with the opening quote. + fn tokenize_escaped_single_quoted_string( + &self, + chars: &mut Peekable>, + ) -> Result { + let mut s = String::new(); + chars.next(); // consume the opening quote + + // slash escaping + let mut is_escaped = false; + while let Some(&ch) = chars.peek() { + macro_rules! escape_control_character { + ($ESCAPED:expr) => {{ + if is_escaped { + s.push($ESCAPED); + is_escaped = false; + } else { + s.push(ch); + } + + chars.next(); + }}; + } + + match ch { + '\'' => { + chars.next(); // consume + if is_escaped { + s.push(ch); + is_escaped = false; + } else if chars.peek().map(|c| *c == '\'').unwrap_or(false) { + s.push(ch); + chars.next(); + } else { + return Ok(s); + } + } + '\\' => { + if is_escaped { + s.push('\\'); + is_escaped = false; + } else { + is_escaped = true; + } + + chars.next(); + } + 'r' => escape_control_character!('\r'), + 'n' => escape_control_character!('\n'), + 't' => escape_control_character!('\t'), + _ => { + is_escaped = false; + chars.next(); // consume + s.push(ch); + } + } + } + self.tokenizer_error("Unterminated encoded string literal") + } + /// Read a single quoted string, starting with the opening quote. fn tokenize_single_quoted_string( &self, @@ -1258,6 +1345,21 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_unicode_whitespace() { + let sql = String::from(" \u{2003}\n"); + + let dialect = GenericDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + let expected = vec![ + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::Newline), + ]; + compare(expected, tokens); + } + #[test] fn tokenize_mismatched_quotes() { let sql = String::from("\"foo"); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs new file mode 100644 index 000000000..2b49abf6f --- /dev/null +++ b/tests/sqlparser_bigquery.rs @@ -0,0 +1,86 @@ +// 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. + +#[macro_use] +mod test_utils; + +use test_utils::*; + +use sqlparser::ast::*; +use sqlparser::dialect::BigQueryDialect; + +#[test] +fn parse_table_identifiers() { + fn test_table_ident(ident: &str, expected: Vec) { + let sql = format!("SELECT 1 FROM {}", ident); + let select = bigquery().verified_only_select(&sql); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(expected), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![] + },] + ); + } + fn test_table_ident_err(ident: &str) { + let sql = format!("SELECT 1 FROM {}", ident); + assert!(bigquery().parse_sql_statements(&sql).is_err()); + } + + test_table_ident("da-sh-es", vec![Ident::new("da-sh-es")]); + + test_table_ident("`spa ce`", vec![Ident::with_quote('`', "spa ce")]); + + test_table_ident( + "`!@#$%^&*()-=_+`", + vec![Ident::with_quote('`', "!@#$%^&*()-=_+")], + ); + + test_table_ident( + "_5abc.dataField", + vec![Ident::new("_5abc"), Ident::new("dataField")], + ); + test_table_ident( + "`5abc`.dataField", + vec![Ident::with_quote('`', "5abc"), Ident::new("dataField")], + ); + + test_table_ident_err("5abc.dataField"); + + test_table_ident( + "abc5.dataField", + vec![Ident::new("abc5"), Ident::new("dataField")], + ); + + test_table_ident_err("abc5!.dataField"); + + test_table_ident( + "`GROUP`.dataField", + vec![Ident::with_quote('`', "GROUP"), Ident::new("dataField")], + ); + + // TODO: this should be error + // test_table_ident_err("GROUP.dataField"); + + test_table_ident("abc5.GROUP", vec![Ident::new("abc5"), Ident::new("GROUP")]); +} + +fn bigquery() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(BigQueryDialect {})], + } +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e2e5168a2..c4a734813 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23,7 +23,8 @@ mod test_utils; use matches::assert_matches; use sqlparser::ast::*; use sqlparser::dialect::{ - AnsiDialect, GenericDialect, MsSqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, + AnsiDialect, BigQueryDialect, GenericDialect, HiveDialect, MsSqlDialect, PostgreSqlDialect, + SQLiteDialect, SnowflakeDialect, }; use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError}; @@ -41,6 +42,9 @@ fn parse_insert_values() { let rows1 = vec![row.clone()]; let rows2 = vec![row.clone(), row]; + let sql = "INSERT customer VALUES (1, 2, 3)"; + check_one(sql, "customer", &[], &rows1); + let sql = "INSERT INTO customer VALUES (1, 2, 3)"; check_one(sql, "customer", &[], &rows1); @@ -91,16 +95,6 @@ fn parse_insert_values() { verified_stmt("INSERT INTO customer WITH foo AS (SELECT 1) SELECT * FROM foo UNION VALUES (1)"); } -#[test] -fn parse_insert_invalid() { - let sql = "INSERT public.customer (id, name, active) VALUES (1, 2, 3)"; - let res = parse_sql_statements(sql); - assert_eq!( - ParserError::ParserError("Expected one of INTO or OVERWRITE, found: public".to_string()), - res.unwrap_err() - ); -} - #[test] fn parse_insert_sqlite() { let dialect = SQLiteDialect {}; @@ -206,7 +200,7 @@ fn parse_update_with_table_alias() { name: Ident::new("u"), columns: vec![] }), - args: vec![], + args: None, with_hints: vec![], }, joins: vec![] @@ -390,13 +384,17 @@ fn parse_select_into() { &SelectInto { temporary: false, unlogged: false, + table: false, name: ObjectName(vec![Ident::new("table0")]) }, only(&select.into) ); - let sql = "SELECT * INTO TEMPORARY UNLOGGED table0 FROM table1"; - one_statement_parses_to(sql, "SELECT * INTO TEMPORARY UNLOGGED table0 FROM table1"); + let sql = "SELECT * INTO TEMPORARY UNLOGGED TABLE table0 FROM table1"; + one_statement_parses_to( + sql, + "SELECT * INTO TEMPORARY UNLOGGED TABLE table0 FROM table1", + ); // Do not allow aliases here let sql = "SELECT * INTO table0 asdf FROM table1"; @@ -563,6 +561,15 @@ fn parse_collate() { ); } +#[test] +fn parse_collate_after_parens() { + let sql = "SELECT (name) COLLATE \"de_DE\" FROM customer"; + assert_matches!( + only(&all_dialects().verified_only_select(sql).projection), + SelectItem::UnnamedExpr(Expr::Collate { .. }) + ); +} + #[test] fn parse_select_string_predicate() { let sql = "SELECT id, fname, lname FROM customer \ @@ -998,6 +1005,32 @@ fn parse_bitwise_ops() { } } +#[test] +fn parse_binary_any() { + let select = verified_only_select("SELECT a = ANY(b)"); + assert_eq!( + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::AnyOp(Box::new(Expr::Identifier(Ident::new("b"))))), + }), + select.projection[0] + ); +} + +#[test] +fn parse_binary_all() { + let select = verified_only_select("SELECT a = ALL(b)"); + assert_eq!( + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::AllOp(Box::new(Expr::Identifier(Ident::new("b"))))), + }), + select.projection[0] + ); +} + #[test] fn parse_logical_xor() { let sql = "SELECT true XOR true, false XOR false, true XOR false, false XOR true"; @@ -1322,6 +1355,45 @@ fn parse_select_having() { assert!(select.having.is_some()); } +#[cfg(feature = "bigdecimal")] +#[test] +fn parse_select_qualify() { + let sql = "SELECT i, p, o FROM qt QUALIFY ROW_NUMBER() OVER (PARTITION BY p ORDER BY o) = 1"; + let select = verified_only_select(sql); + assert_eq!( + Some(Expr::BinaryOp { + left: Box::new(Expr::Function(Function { + name: ObjectName(vec![Ident::new("ROW_NUMBER")]), + args: vec![], + over: Some(WindowSpec { + partition_by: vec![Expr::Identifier(Ident::new("p"))], + order_by: vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("o")), + asc: None, + nulls_first: None + }], + window_frame: None + }), + distinct: false + })), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("1"))) + }), + select.qualify + ); + + let sql = "SELECT i, p, o, ROW_NUMBER() OVER (PARTITION BY p ORDER BY o) AS row_num FROM qt QUALIFY row_num = 1"; + let select = verified_only_select(sql); + assert_eq!( + Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("row_num"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("1"))) + }), + select.qualify + ); +} + #[test] fn parse_limit_accepts_all() { one_statement_parses_to( @@ -1695,6 +1767,53 @@ fn parse_create_table() { .contains("Expected constraint details after CONSTRAINT ")); } +#[test] +fn parse_create_table_hive_array() { + // Parsing [] type arrays does not work in MsSql since [ is used in is_delimited_identifier_start + let dialects = TestedDialects { + dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(HiveDialect {})], + }; + let sql = "CREATE TABLE IF NOT EXISTS something (key int, val array)"; + match dialects.one_statement_parses_to( + sql, + "CREATE TABLE IF NOT EXISTS something (key INT, val INT[])", + ) { + Statement::CreateTable { + if_not_exists, + name, + columns, + .. + } => { + assert!(if_not_exists); + assert_eq!(name, ObjectName(vec!["something".into()])); + assert_eq!( + columns, + vec![ + ColumnDef { + name: Ident::new("key"), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::new("val"), + data_type: DataType::Array(Box::new(DataType::Int(None))), + collation: None, + options: vec![], + }, + ], + ) + } + _ => unreachable!(), + } + + let res = parse_sql_statements("CREATE TABLE IF NOT EXISTS something (key int, val array, found: )")); +} + #[test] fn parse_create_table_with_multiple_on_delete_in_constraint_fails() { parse_sql_statements( @@ -2499,6 +2618,19 @@ fn parse_literal_time() { ); } +#[test] +fn parse_literal_datetime() { + let sql = "SELECT DATETIME '1999-01-01 01:23:34.45'"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::Datetime, + value: "1999-01-01 01:23:34.45".into() + }, + expr_from_projection(only(&select.projection)), + ); +} + #[test] fn parse_literal_timestamp() { let sql = "SELECT TIMESTAMP '1999-01-01 01:23:34'"; @@ -2670,6 +2802,84 @@ fn parse_table_function() { ); } +#[test] +fn parse_unnest() { + fn chk(alias: bool, with_offset: bool, dialects: &TestedDialects, want: Vec) { + let sql = &format!( + "SELECT * FROM UNNEST(expr){}{}", + if alias { " AS numbers" } else { "" }, + if with_offset { " WITH OFFSET" } else { "" }, + ); + let select = dialects.verified_only_select(sql); + assert_eq!(select.from, want); + } + let dialects = TestedDialects { + dialects: vec![Box::new(BigQueryDialect {}), Box::new(GenericDialect {})], + }; + // 1. both Alias and WITH OFFSET clauses. + chk( + true, + true, + &dialects, + vec![TableWithJoins { + relation: TableFactor::UNNEST { + alias: Some(TableAlias { + name: Ident::new("numbers"), + columns: vec![], + }), + array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + with_offset: true, + }, + joins: vec![], + }], + ); + // 2. neither Alias nor WITH OFFSET clause. + chk( + false, + false, + &dialects, + vec![TableWithJoins { + relation: TableFactor::UNNEST { + alias: None, + array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + with_offset: false, + }, + joins: vec![], + }], + ); + // 3. Alias but no WITH OFFSET clause. + chk( + false, + true, + &dialects, + vec![TableWithJoins { + relation: TableFactor::UNNEST { + alias: None, + array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + with_offset: true, + }, + joins: vec![], + }], + ); + // 4. WITH OFFSET but no Alias. + chk( + true, + false, + &dialects, + vec![TableWithJoins { + relation: TableFactor::UNNEST { + alias: Some(TableAlias { + name: Ident::new("numbers"), + columns: vec![], + }), + array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + with_offset: false, + }, + joins: vec![], + }], + ); +} + #[test] fn parse_delimited_identifiers() { // check that quoted identifiers in any position remain quoted after serialization @@ -2686,7 +2896,7 @@ fn parse_delimited_identifiers() { } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); - assert!(args.is_empty()); + assert!(args.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), @@ -2805,6 +3015,12 @@ fn parse_from_advanced() { let _select = verified_only_select(sql); } +#[test] +fn parse_nullary_table_valued_function() { + let sql = "SELECT * FROM fn()"; + let _select = verified_only_select(sql); +} + #[test] fn parse_implicit_join() { let sql = "SELECT * FROM t1, t2"; @@ -2815,7 +3031,7 @@ fn parse_implicit_join() { relation: TableFactor::Table { name: ObjectName(vec!["t1".into()]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, joins: vec![], @@ -2824,7 +3040,7 @@ fn parse_implicit_join() { relation: TableFactor::Table { name: ObjectName(vec!["t2".into()]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, joins: vec![], @@ -2841,14 +3057,14 @@ fn parse_implicit_join() { relation: TableFactor::Table { name: ObjectName(vec!["t1a".into()]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, joins: vec![Join { relation: TableFactor::Table { name: ObjectName(vec!["t1b".into()]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -2858,14 +3074,14 @@ fn parse_implicit_join() { relation: TableFactor::Table { name: ObjectName(vec!["t2a".into()]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, joins: vec![Join { relation: TableFactor::Table { name: ObjectName(vec!["t2b".into()]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -2885,7 +3101,7 @@ fn parse_cross_join() { relation: TableFactor::Table { name: ObjectName(vec![Ident::new("t2")]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, join_operator: JoinOperator::CrossJoin @@ -2905,7 +3121,7 @@ fn parse_joins_on() { relation: TableFactor::Table { name: ObjectName(vec![Ident::new(relation.into())]), alias, - args: vec![], + args: None, with_hints: vec![], }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -2958,7 +3174,7 @@ fn parse_joins_using() { relation: TableFactor::Table { name: ObjectName(vec![Ident::new(relation.into())]), alias, - args: vec![], + args: None, with_hints: vec![], }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), @@ -3003,7 +3219,7 @@ fn parse_natural_join() { relation: TableFactor::Table { name: ObjectName(vec![Ident::new("t2")]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, join_operator: f(JoinConstraint::Natural), @@ -3241,7 +3457,7 @@ fn parse_derived_tables() { relation: TableFactor::Table { name: ObjectName(vec!["t2".into()]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -4294,15 +4510,28 @@ fn test_revoke() { #[test] fn parse_merge() { - let sql = "MERGE INTO s.bar AS dest USING (SELECT * FROM s.foo) as stg ON dest.D = stg.D AND dest.E = stg.E WHEN NOT MATCHED THEN INSERT (A, B, C) VALUES (stg.A, stg.B, stg.C) WHEN MATCHED AND dest.A = 'a' THEN UPDATE SET dest.F = stg.F, dest.G = stg.G WHEN MATCHED THEN DELETE"; - match verified_stmt(sql) { - Statement::Merge { - table, - source, - alias, - on, - clauses, - } => { + let sql = "MERGE INTO s.bar AS dest USING (SELECT * FROM s.foo) AS stg ON dest.D = stg.D AND dest.E = stg.E WHEN NOT MATCHED THEN INSERT (A, B, C) VALUES (stg.A, stg.B, stg.C) WHEN MATCHED AND dest.A = 'a' THEN UPDATE SET dest.F = stg.F, dest.G = stg.G WHEN MATCHED THEN DELETE"; + let sql_no_into = "MERGE s.bar AS dest USING (SELECT * FROM s.foo) AS stg ON dest.D = stg.D AND dest.E = stg.E WHEN NOT MATCHED THEN INSERT (A, B, C) VALUES (stg.A, stg.B, stg.C) WHEN MATCHED AND dest.A = 'a' THEN UPDATE SET dest.F = stg.F, dest.G = stg.G WHEN MATCHED THEN DELETE"; + match (verified_stmt(sql), verified_stmt(sql_no_into)) { + ( + Statement::Merge { + into, + table, + source, + on, + clauses, + }, + Statement::Merge { + into: no_into, + table: table_no_into, + source: source_no_into, + on: on_no_into, + clauses: clauses_no_into, + }, + ) => { + assert!(into); + assert!(!no_into); + assert_eq!( table, TableFactor::Table { @@ -4311,50 +4540,58 @@ fn parse_merge() { name: Ident::new("dest"), columns: vec![] }), - args: vec![], + args: None, with_hints: vec![] } ); + assert_eq!(table, table_no_into); + assert_eq!( source, - Box::new(SetExpr::Query(Box::new(Query { - with: None, - body: SetExpr::Select(Box::new(Select { - distinct: false, - top: None, - projection: vec![SelectItem::Wildcard], - into: None, - from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("s"), Ident::new("foo")]), - alias: None, - args: vec![], - with_hints: vec![], - }, - joins: vec![] - }], - lateral_views: vec![], - selection: None, - group_by: vec![], - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None - })), - order_by: vec![], - limit: None, - offset: None, - fetch: None, - lock: None - }))) - ); - assert_eq!( - alias, - Some(TableAlias { - name: Ident::new("stg"), - columns: vec![] - }) + TableFactor::Derived { + lateral: false, + subquery: Box::new(Query { + with: None, + body: SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![SelectItem::Wildcard], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("s"), Ident::new("foo")]), + 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, + qualify: None, + })), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + lock: None + }), + alias: Some(TableAlias { + name: Ident { + value: "stg".to_string(), + quote_style: None + }, + columns: vec![] + }) + } ); + assert_eq!(source, source_no_into); + assert_eq!( on, Box::new(Expr::BinaryOp { @@ -4383,6 +4620,8 @@ fn parse_merge() { }) }) ); + assert_eq!(on, on_no_into); + assert_eq!( clauses, vec![ @@ -4425,12 +4664,40 @@ fn parse_merge() { }, MergeClause::MatchedDelete(None) ] - ) + ); + assert_eq!(clauses, clauses_no_into); } _ => unreachable!(), } } +#[test] +fn test_merge_into_using_table() { + let sql = "MERGE INTO target_table USING source_table \ + ON target_table.id = source_table.oooid \ + WHEN MATCHED THEN \ + UPDATE SET target_table.description = source_table.description \ + WHEN NOT MATCHED THEN \ + INSERT (ID, description) VALUES (source_table.id, source_table.description)"; + + verified_stmt(sql); +} + +#[test] +fn test_merge_with_delimiter() { + let sql = "MERGE INTO target_table USING source_table \ + ON target_table.id = source_table.oooid \ + WHEN MATCHED THEN \ + UPDATE SET target_table.description = source_table.description \ + WHEN NOT MATCHED THEN \ + INSERT (ID, description) VALUES (source_table.id, source_table.description);"; + + match parse_sql_statements(sql) { + Ok(_) => {} + _ => unreachable!(), + } +} + #[test] fn test_lock() { let sql = "SELECT * FROM student WHERE id = '1' FOR UPDATE"; @@ -4461,6 +4728,7 @@ fn test_placeholder() { Box::new(PostgreSqlDialect {}), Box::new(MsSqlDialect {}), Box::new(AnsiDialect {}), + Box::new(BigQueryDialect {}), Box::new(SnowflakeDialect {}), // Note: `$` is the starting word for the HiveDialect identifier // Box::new(sqlparser::dialect::HiveDialect {}), @@ -4621,3 +4889,61 @@ fn parse_position_negative() { res.unwrap_err() ); } + +#[test] +fn parse_is_boolean() { + use self::Expr::*; + + let sql = "a IS FALSE"; + assert_eq!( + IsFalse(Box::new(Identifier(Ident::new("a")))), + verified_expr(sql) + ); + + let sql = "a IS TRUE"; + assert_eq!( + IsTrue(Box::new(Identifier(Ident::new("a")))), + verified_expr(sql) + ); + + verified_stmt("SELECT f FROM foo WHERE field IS TRUE"); + + verified_stmt("SELECT f FROM foo WHERE field IS FALSE"); + + let sql = "SELECT f from foo where field is 0"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: 0" + .to_string() + ), + res.unwrap_err() + ); +} + +#[test] +fn parse_discard() { + let sql = "DISCARD ALL"; + match verified_stmt(sql) { + Statement::Discard { object_type, .. } => assert_eq!(object_type, DiscardObject::ALL), + _ => unreachable!(), + } + + let sql = "DISCARD PLANS"; + match verified_stmt(sql) { + Statement::Discard { object_type, .. } => assert_eq!(object_type, DiscardObject::PLANS), + _ => unreachable!(), + } + + let sql = "DISCARD SEQUENCES"; + match verified_stmt(sql) { + Statement::Discard { object_type, .. } => assert_eq!(object_type, DiscardObject::SEQUENCES), + _ => unreachable!(), + } + + let sql = "DISCARD TEMP"; + match verified_stmt(sql) { + Statement::Discard { object_type, .. } => assert_eq!(object_type, DiscardObject::TEMP), + _ => unreachable!(), + } +} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index d933f0f25..fa2486120 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -15,7 +15,9 @@ //! Test SQL syntax specific to Hive. The parser based on the generic dialect //! is also tested (on the inputs it can handle). -use sqlparser::dialect::HiveDialect; +use sqlparser::ast::{CreateFunctionUsing, Ident, ObjectName, SetVariableValue, Statement}; +use sqlparser::dialect::{GenericDialect, HiveDialect}; +use sqlparser::parser::ParserError; use sqlparser::test_utils::*; #[test] @@ -205,6 +207,72 @@ fn from_cte() { println!("{}", hive().verified_stmt(rename)); } +#[test] +fn set_statement_with_minus() { + assert_eq!( + hive().verified_stmt("SET hive.tez.java.opts = -Xmx4g"), + Statement::SetVariable { + local: false, + hivevar: false, + variable: ObjectName(vec![ + Ident::new("hive"), + Ident::new("tez"), + Ident::new("java"), + Ident::new("opts") + ]), + value: vec![SetVariableValue::Ident("-Xmx4g".into())], + } + ); + + assert_eq!( + hive().parse_sql_statements("SET hive.tez.java.opts = -"), + Err(ParserError::ParserError( + "Expected word, found: EOF".to_string() + )) + ) +} + +#[test] +fn parse_create_function() { + let sql = "CREATE TEMPORARY FUNCTION mydb.myfunc AS 'org.random.class.Name' USING JAR 'hdfs://somewhere.com:8020/very/far'"; + match hive().verified_stmt(sql) { + Statement::CreateFunction { + temporary, + name, + class_name, + using, + } => { + assert!(temporary); + assert_eq!("mydb.myfunc", name.to_string()); + assert_eq!("org.random.class.Name", class_name); + assert_eq!( + using, + Some(CreateFunctionUsing::Jar( + "hdfs://somewhere.com:8020/very/far".to_string() + )) + ) + } + _ => unreachable!(), + } + + let generic = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + }; + + assert_eq!( + generic.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError( + "Expected an object type after CREATE, found: FUNCTION".to_string() + ) + ); + + let sql = "CREATE TEMPORARY FUNCTION mydb.myfunc AS 'org.random.class.Name' USING JAR"; + assert_eq!( + hive().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected literal string, found: EOF".to_string()), + ); +} + fn hive() -> TestedDialects { TestedDialects { dialects: vec![Box::new(HiveDialect {})], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e3566bca9..10aa803f6 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -314,6 +314,41 @@ fn parse_quote_identifiers_2() { distribute_by: vec![], sort_by: vec![], having: None, + qualify: None + })), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + lock: None, + })) + ); +} + +#[test] +fn parse_quote_identifiers_3() { + let sql = "SELECT ```quoted identifier```"; + assert_eq!( + mysql().verified_stmt(sql), + Statement::Query(Box::new(Query { + with: None, + body: SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { + value: "`quoted identifier`".into(), + quote_style: Some('`'), + }))], + into: None, + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None })), order_by: vec![], limit: None, @@ -625,7 +660,7 @@ fn parse_update_with_joins() { name: Ident::new("o"), columns: vec![] }), - args: vec![], + args: None, with_hints: vec![], }, joins: vec![Join { @@ -635,7 +670,7 @@ fn parse_update_with_joins() { name: Ident::new("c"), columns: vec![] }), - args: vec![], + args: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { @@ -742,7 +777,7 @@ fn parse_substring_in_select() { quote_style: None }]), alias: None, - args: vec![], + args: None, with_hints: vec![] }, joins: vec![] @@ -754,6 +789,7 @@ fn parse_substring_in_select() { distribute_by: vec![], sort_by: vec![], having: None, + qualify: None })), order_by: vec![], limit: None, @@ -768,6 +804,36 @@ fn parse_substring_in_select() { } } +#[test] +fn parse_kill() { + let stmt = mysql_and_generic().verified_stmt("KILL CONNECTION 5"); + assert_eq!( + stmt, + Statement::Kill { + modifier: Some(KillType::Connection), + id: 5, + } + ); + + let stmt = mysql_and_generic().verified_stmt("KILL QUERY 5"); + assert_eq!( + stmt, + Statement::Kill { + modifier: Some(KillType::Query), + id: 5, + } + ); + + let stmt = mysql_and_generic().verified_stmt("KILL 5"); + assert_eq!( + stmt, + Statement::Kill { + modifier: None, + id: 5, + } + ); +} + fn mysql() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 262f10f7e..1faa878f3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -18,6 +18,7 @@ mod test_utils; use test_utils::*; +use sqlparser::ast::Value::Boolean; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; @@ -412,7 +413,7 @@ fn parse_update_set_from() { relation: TableFactor::Table { name: ObjectName(vec![Ident::new("t1")]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, joins: vec![], @@ -438,7 +439,7 @@ fn parse_update_set_from() { relation: TableFactor::Table { name: ObjectName(vec![Ident::new("t1")]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, joins: vec![], @@ -450,6 +451,7 @@ fn parse_update_set_from() { distribute_by: vec![], sort_by: vec![], having: None, + qualify: None })), order_by: vec![], limit: None, @@ -589,7 +591,7 @@ fn test_copy_to() { #[test] fn parse_copy_from() { - let sql = "COPY table (a, b) FROM 'file.csv' WITH + let sql = "COPY table (a, b) FROM 'file.csv' WITH ( FORMAT CSV, FREEZE, @@ -779,7 +781,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variable: "a".into(), + variable: ObjectName(vec![Ident::new("a")]), value: vec![SetVariableValue::Ident("b".into())], } ); @@ -790,7 +792,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variable: "a".into(), + variable: ObjectName(vec![Ident::new("a")]), value: vec![SetVariableValue::Literal(Value::SingleQuotedString( "b".into() ))], @@ -803,7 +805,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variable: "a".into(), + variable: ObjectName(vec![Ident::new("a")]), value: vec![SetVariableValue::Literal(number("0"))], } ); @@ -814,7 +816,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variable: "a".into(), + variable: ObjectName(vec![Ident::new("a")]), value: vec![SetVariableValue::Ident("DEFAULT".into())], } ); @@ -825,11 +827,42 @@ fn parse_set() { Statement::SetVariable { local: true, hivevar: false, - variable: "a".into(), + variable: ObjectName(vec![Ident::new("a")]), value: vec![SetVariableValue::Ident("b".into())], } ); + let stmt = pg_and_generic().verified_stmt("SET a.b.c = b"); + assert_eq!( + stmt, + Statement::SetVariable { + local: false, + hivevar: false, + variable: ObjectName(vec![Ident::new("a"), Ident::new("b"), Ident::new("c")]), + value: vec![SetVariableValue::Ident("b".into())], + } + ); + + let stmt = pg_and_generic().one_statement_parses_to( + "SET hive.tez.auto.reducer.parallelism=false", + "SET hive.tez.auto.reducer.parallelism = false", + ); + assert_eq!( + stmt, + Statement::SetVariable { + local: false, + hivevar: false, + variable: ObjectName(vec![ + Ident::new("hive"), + Ident::new("tez"), + Ident::new("auto"), + Ident::new("reducer"), + Ident::new("parallelism") + ]), + value: vec![SetVariableValue::Literal(Boolean(false))], + } + ); + pg_and_generic().one_statement_parses_to("SET a TO b", "SET a = b"); pg_and_generic().one_statement_parses_to("SET SESSION a = b", "SET a = b"); @@ -1136,31 +1169,31 @@ fn parse_array_index_expr() { .collect(); let sql = "SELECT foo[0] FROM foos"; - let select = pg().verified_only_select(sql); + let select = pg_and_generic().verified_only_select(sql); assert_eq!( &Expr::ArrayIndex { obj: Box::new(Expr::Identifier(Ident::new("foo"))), - indexs: vec![num[0].clone()], + indexes: vec![num[0].clone()], }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT foo[0][0] FROM foos"; - let select = pg().verified_only_select(sql); + let select = pg_and_generic().verified_only_select(sql); assert_eq!( &Expr::ArrayIndex { obj: Box::new(Expr::Identifier(Ident::new("foo"))), - indexs: vec![num[0].clone(), num[0].clone()], + indexes: vec![num[0].clone(), num[0].clone()], }, expr_from_projection(only(&select.projection)), ); let sql = r#"SELECT bar[0]["baz"]["fooz"] FROM foos"#; - let select = pg().verified_only_select(sql); + let select = pg_and_generic().verified_only_select(sql); assert_eq!( &Expr::ArrayIndex { obj: Box::new(Expr::Identifier(Ident::new("bar"))), - indexs: vec![ + indexes: vec![ num[0].clone(), Expr::Identifier(Ident { value: "baz".to_string(), @@ -1176,7 +1209,7 @@ fn parse_array_index_expr() { ); let sql = "SELECT (CAST(ARRAY[ARRAY[2, 3]] AS INT[][]))[1][2]"; - let select = pg().verified_only_select(sql); + let select = pg_and_generic().verified_only_select(sql); assert_eq!( &Expr::ArrayIndex { obj: Box::new(Expr::Nested(Box::new(Expr::Cast { @@ -1192,7 +1225,7 @@ fn parse_array_index_expr() { ))))), collation: None, }))), - indexs: vec![num[1].clone(), num[2].clone()], + indexes: vec![num[1].clone(), num[2].clone()], }, expr_from_projection(only(&select.projection)), ); @@ -1302,6 +1335,67 @@ fn test_json() { ); } +#[test] +fn test_composite_value() { + let sql = "SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9"; + let select = pg().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::CompositeAccess { + key: Ident::new("name"), + expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("on_hand"), + Ident::new("item") + ])))) + }), + select.projection[0] + ); + + #[cfg(feature = "bigdecimal")] + let num: Expr = Expr::Value(Value::Number(bigdecimal::BigDecimal::from(9), false)); + #[cfg(not(feature = "bigdecimal"))] + let num: Expr = Expr::Value(Value::Number("9".to_string(), false)); + assert_eq!( + select.selection, + Some(Expr::BinaryOp { + left: Box::new(Expr::CompositeAccess { + key: Ident::new("price"), + expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("on_hand"), + Ident::new("item") + ])))) + }), + op: BinaryOperator::Gt, + right: Box::new(num) + }) + ); + + let sql = "SELECT (information_schema._pg_expandarray(ARRAY['i', 'i'])).n"; + let select = pg().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::CompositeAccess { + key: Ident::new("n"), + expr: Box::new(Expr::Nested(Box::new(Expr::Function(Function { + name: ObjectName(vec![ + Ident::new("information_schema"), + Ident::new("_pg_expandarray") + ]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Array( + Array { + elem: vec![ + Expr::Value(Value::SingleQuotedString("i".to_string())), + Expr::Value(Value::SingleQuotedString("i".to_string())), + ], + named: true + } + )))], + over: None, + distinct: false, + })))) + }), + select.projection[0] + ); +} + #[test] fn parse_comments() { match pg().verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'") { @@ -1348,6 +1442,11 @@ fn parse_quoted_identifier() { pg_and_generic().verified_stmt(r#"SELECT "quoted "" ident""#); } +#[test] +fn parse_quoted_identifier_2() { + pg_and_generic().verified_stmt(r#"SELECT """quoted ident""""#); +} + #[test] fn parse_local_and_global() { pg_and_generic().verified_stmt("CREATE LOCAL TEMPORARY TABLE table (COL INT)"); @@ -1374,3 +1473,68 @@ fn pg_and_generic() -> TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(GenericDialect {})], } } + +#[test] +fn parse_escaped_literal_string() { + let sql = + r#"SELECT E's1 \n s1', E's2 \\n s2', E's3 \\\n s3', E's4 \\\\n s4', E'\'', E'foo \\'"#; + let select = pg_and_generic().verified_only_select(sql); + assert_eq!(6, select.projection.len()); + assert_eq!( + &Expr::Value(Value::EscapedStringLiteral("s1 \n s1".to_string())), + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Value(Value::EscapedStringLiteral("s2 \\n s2".to_string())), + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Value(Value::EscapedStringLiteral("s3 \\\n s3".to_string())), + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Value(Value::EscapedStringLiteral("s4 \\\\n s4".to_string())), + expr_from_projection(&select.projection[3]) + ); + assert_eq!( + &Expr::Value(Value::EscapedStringLiteral("'".to_string())), + expr_from_projection(&select.projection[4]) + ); + assert_eq!( + &Expr::Value(Value::EscapedStringLiteral("foo \\".to_string())), + expr_from_projection(&select.projection[5]) + ); + + let sql = r#"SELECT E'\'"#; + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + "sql parser error: Unterminated encoded string literal at Line: 1, Column 8" + ); +} + +#[test] +fn parse_fetch() { + pg_and_generic().verified_stmt("FETCH 2048 IN \"SQL_CUR0x7fa44801bc00\""); + pg_and_generic().verified_stmt("FETCH 2048 IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic().verified_stmt("FETCH NEXT IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic().verified_stmt("FETCH PRIOR IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic().verified_stmt("FETCH FIRST IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic().verified_stmt("FETCH LAST IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic() + .verified_stmt("FETCH ABSOLUTE 2048 IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic() + .verified_stmt("FETCH RELATIVE 2048 IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic().verified_stmt("FETCH ALL IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic().verified_stmt("FETCH ALL IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic() + .verified_stmt("FETCH FORWARD 2048 IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic() + .verified_stmt("FETCH FORWARD ALL IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic() + .verified_stmt("FETCH BACKWARD 2048 IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); + pg_and_generic() + .verified_stmt("FETCH BACKWARD ALL IN \"SQL_CUR0x7fa44801bc00\" INTO \"new_table\""); +} diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs new file mode 100644 index 000000000..6f77cf335 --- /dev/null +++ b/tests/sqlparser_redshift.rs @@ -0,0 +1,112 @@ +// 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. + +#[macro_use] +mod test_utils; + +use test_utils::*; + +use sqlparser::ast::*; +use sqlparser::dialect::RedshiftSqlDialect; + +#[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('[') + })), + ); + assert_eq!( + select.from[0], + TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![ + Ident { + value: "test_schema".to_string(), + quote_style: Some('[') + }, + Ident { + value: "test_table".to_string(), + quote_style: Some('[') + } + ]), + alias: None, + args: None, + with_hints: vec![], + }, + 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('"') + })), + ); + assert_eq!( + select.from[0], + TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![ + Ident { + value: "test_schema".to_string(), + quote_style: Some('"') + }, + Ident { + value: "test_table".to_string(), + quote_style: Some('"') + } + ]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + } + ); +} + +fn redshift() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(RedshiftSqlDialect {})], + } +} + +#[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] + ); +} diff --git a/tests/sqpparser_clickhouse.rs b/tests/sqpparser_clickhouse.rs index dfd555200..72a1a0556 100644 --- a/tests/sqpparser_clickhouse.rs +++ b/tests/sqpparser_clickhouse.rs @@ -58,7 +58,7 @@ fn parse_map_access_expr() { relation: Table { name: ObjectName(vec![Ident::new("foos")]), alias: None, - args: vec![], + args: None, with_hints: vec![], }, joins: vec![] @@ -96,7 +96,8 @@ fn parse_map_access_expr() { cluster_by: vec![], distribute_by: vec![], sort_by: vec![], - having: None + having: None, + qualify: None }, select ); @@ -118,6 +119,18 @@ fn parse_array_expr() { ) } +#[test] +fn parse_kill() { + let stmt = clickhouse().verified_stmt("KILL MUTATION 5"); + assert_eq!( + stmt, + Statement::Kill { + modifier: Some(KillType::Mutation), + id: 5, + } + ); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})],