diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c9b22db..ecd57703c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,37 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.45.0] 2024-04-12 + +### Added +* Support `DateTimeField` variants: `CUSTOM` and `WEEK(MONDAY)` (#1191) - Thanks @iffyio +* Support for arbitrary expr in `MapAccessSyntax` (#1179) - Thanks @iffyio +* Support unquoted hyphen in table/view declaration for BigQuery (#1178) - Thanks @iffyio +* Support `CREATE/DROP SECRET` for duckdb dialect (#1208) - Thanks @JichaoS +* Support MySQL `UNIQUE` table constraint (#1164) - Thanks @Nikita-str +* Support tailing commas on Snowflake. (#1205) - Thanks @yassun7010 +* Support `[FIRST | AFTER column_name]` in `ALTER TABLE` for MySQL (#1180) - Thanks @xring +* Support inline comment with hash syntax for BigQuery (#1192) - Thanks @iffyio +* Support named windows in OVER (window_definition) clause (#1166) - Thanks @Nikita-str +* Support PARALLEL ... and for ..ON NULL INPUT ... to CREATE FUNCTION` (#1202) - Thanks @dimfeld +* Support DuckDB functions named arguments with assignment operator (#1195) - Thanks @alamb +* Support DuckDB struct literal syntax (#1194) - Thanks @gstvg +* Support `$$` in generic dialect ... (#1185)- Thanks @milenkovicm +* Support row_alias and col_aliases in `INSERT` statement for MySQL and Generic dialects (#1136) - Thanks @emin100 + +### Fixed +* Fix dollar quoted string tokenizer (#1193) - Thanks @ZacJW +* Do not allocate in `impl Display` for `DateTimeField` (#1209) - Thanks @alamb +* Fix parse `COPY INTO` stage names without parens for SnowFlake (#1187) - Thanks @mobuchowski +* Solve stack overflow on RecursionLimitExceeded on debug builds (#1171) - Thanks @Nikita-str +* Fix parsing of equality binary operator in function argument (#1182) - Thanks @jmhain +* Fix some comments (#1184) - Thanks @sunxunle + +### Changed +* Cleanup `CREATE FUNCTION` tests (#1203) - Thanks @alamb +* Parse `SUBSTRING FROM` syntax in all dialects, reflect change in the AST (#1173) - Thanks @lovasoa +* Add identifier quote style to Dialect trait (#1170) - Thanks @backkem + ## [0.44.0] 2024-03-02 ### Added diff --git a/Cargo.toml b/Cargo.toml index ed3d88b9f..3c5d4651c 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.44.0" +version = "0.45.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/README.md b/README.md index 8a4b5d986..512f5f6c0 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ maintain this crate is limited. Please read the following sections carefully. ### New Syntax The most commonly accepted PRs add support for or fix a bug in a feature in the -SQL standard, or a a popular RDBMS, such as Microsoft SQL +SQL standard, or a popular RDBMS, such as Microsoft SQL Server or PostgreSQL, will likely be accepted after a brief review. Any SQL feature that is dialect specific should be parsed by *both* the relevant [`Dialect`] as well as [`GenericDialect`]. diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 6b082a1fb..d71900bff 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -40,7 +40,7 @@ pub enum DataType { /// Variable-length character type e.g. VARCHAR(10) Varchar(Option), /// Variable-length character type e.g. NVARCHAR(10) - Nvarchar(Option), + Nvarchar(Option), /// Uuid type Uuid, /// Large character object with optional length e.g. CHARACTER LARGE OBJECT, CHARACTER LARGE OBJECT(1000), [standard] @@ -238,9 +238,7 @@ impl fmt::Display for DataType { DataType::CharVarying(size) => format_character_string_type(f, "CHAR VARYING", size), DataType::Varchar(size) => format_character_string_type(f, "VARCHAR", size), - DataType::Nvarchar(size) => { - format_type_with_optional_length(f, "NVARCHAR", size, false) - } + DataType::Nvarchar(size) => format_character_string_type(f, "NVARCHAR", size), DataType::Uuid => write!(f, "UUID"), DataType::CharacterLargeObject(size) => { format_type_with_optional_length(f, "CHARACTER LARGE OBJECT", size, false) @@ -349,7 +347,8 @@ impl fmt::Display for DataType { DataType::Bytea => write!(f, "BYTEA"), DataType::Array(ty) => match ty { ArrayElemTypeDef::None => write!(f, "ARRAY"), - ArrayElemTypeDef::SquareBracket(t) => write!(f, "{t}[]"), + ArrayElemTypeDef::SquareBracket(t, None) => write!(f, "{t}[]"), + ArrayElemTypeDef::SquareBracket(t, Some(size)) => write!(f, "{t}[{size}]"), ArrayElemTypeDef::AngleBracket(t) => write!(f, "ARRAY<{t}>"), }, DataType::Custom(ty, modifiers) => { @@ -592,6 +591,6 @@ pub enum ArrayElemTypeDef { None, /// `ARRAY` AngleBracket(Box), - /// `[]INT` - SquareBracket(Box), + /// `INT[]` or `INT[2]` + SquareBracket(Box, Option), } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9e3137d94..de514550b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -15,7 +15,7 @@ #[cfg(not(feature = "std"))] use alloc::{boxed::Box, string::String, vec::Vec}; -use core::fmt; +use core::fmt::{self, Write}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -25,8 +25,8 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName, SequenceOptions, - SqlOption, + display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, + ObjectName, SequenceOptions, SqlOption, }; use crate::tokenizer::Token; @@ -45,6 +45,8 @@ pub enum AlterTableOperation { if_not_exists: bool, /// . column_def: ColumnDef, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, }, /// `DISABLE ROW LEVEL SECURITY` /// @@ -129,6 +131,16 @@ pub enum AlterTableOperation { new_name: Ident, data_type: DataType, options: Vec, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, + }, + // CHANGE [ COLUMN ] [ ] + ModifyColumn { + col_name: Ident, + data_type: DataType, + options: Vec, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, }, /// `RENAME CONSTRAINT TO ` /// @@ -171,6 +183,7 @@ impl fmt::Display for AlterTableOperation { column_keyword, if_not_exists, column_def, + column_position, } => { write!(f, "ADD")?; if *column_keyword { @@ -181,6 +194,10 @@ impl fmt::Display for AlterTableOperation { } write!(f, " {column_def}")?; + if let Some(position) = column_position { + write!(f, " {position}")?; + } + Ok(()) } AlterTableOperation::AlterColumn { column_name, op } => { @@ -271,13 +288,33 @@ impl fmt::Display for AlterTableOperation { new_name, data_type, options, + column_position, } => { write!(f, "CHANGE COLUMN {old_name} {new_name} {data_type}")?; - if options.is_empty() { - Ok(()) - } else { - write!(f, " {}", display_separated(options, " ")) + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; + } + if let Some(position) = column_position { + write!(f, " {position}")?; } + + Ok(()) + } + AlterTableOperation::ModifyColumn { + col_name, + data_type, + options, + column_position, + } => { + write!(f, "MODIFY COLUMN {col_name} {data_type}")?; + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; + } + if let Some(position) = column_position { + write!(f, " {position}")?; + } + + Ok(()) } AlterTableOperation::RenameConstraint { old_name, new_name } => { write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") @@ -384,12 +421,68 @@ impl fmt::Display for AlterColumnOperation { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TableConstraint { - /// `[ CONSTRAINT ] { PRIMARY KEY | UNIQUE } ()` + /// MySQL [definition][1] for `UNIQUE` constraints statements:\ + /// * `[CONSTRAINT []] UNIQUE [] [index_type] () ` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// * [index_type_display][4] is `[INDEX | KEY]` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + /// [4]: KeyOrIndexDisplay Unique { + /// Constraint name. + /// + /// Can be not the same as `index_name` + name: Option, + /// Index name + index_name: Option, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + index_type_display: KeyOrIndexDisplay, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + index_type: Option, + /// Identifiers of the columns that are unique. + columns: Vec, + index_options: Vec, + characteristics: Option, + }, + /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ + /// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` + /// + /// Actually the specification have no `[index_name]` but the next query will complete successfully: + /// ```sql + /// CREATE TABLE unspec_table ( + /// xid INT NOT NULL, + /// CONSTRAINT p_name PRIMARY KEY index_name USING BTREE (xid) + /// ); + /// ``` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + PrimaryKey { + /// Constraint name. + /// + /// Can be not the same as `index_name` name: Option, + /// Index name + index_name: Option, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + index_type: Option, + /// Identifiers of the columns that form the primary key. columns: Vec, - /// Whether this is a `PRIMARY KEY` or just a `UNIQUE` constraint - is_primary: bool, + index_options: Vec, characteristics: Option, }, /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () @@ -459,22 +552,51 @@ impl fmt::Display for TableConstraint { match self { TableConstraint::Unique { name, + index_name, + index_type_display, + index_type, columns, - is_primary, + index_options, characteristics, } => { write!( f, - "{}{} ({})", + "{}UNIQUE{index_type_display:>}{}{} ({})", display_constraint_name(name), - if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }, - display_comma_separated(columns) + display_option_spaced(index_name), + display_option(" USING ", "", index_type), + display_comma_separated(columns), )?; - if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + if !index_options.is_empty() { + write!(f, " {}", display_separated(index_options, " "))?; } + write!(f, "{}", display_option_spaced(characteristics))?; + Ok(()) + } + TableConstraint::PrimaryKey { + name, + index_name, + index_type, + columns, + index_options, + characteristics, + } => { + write!( + f, + "{}PRIMARY KEY{}{} ({})", + display_constraint_name(name), + display_option_spaced(index_name), + display_option(" USING ", "", index_type), + display_comma_separated(columns), + )?; + + if !index_options.is_empty() { + write!(f, " {}", display_separated(index_options, " "))?; + } + + write!(f, "{}", display_option_spaced(characteristics))?; Ok(()) } TableConstraint::ForeignKey { @@ -537,9 +659,7 @@ impl fmt::Display for TableConstraint { write!(f, "SPATIAL")?; } - if !matches!(index_type_display, KeyOrIndexDisplay::None) { - write!(f, " {index_type_display}")?; - } + write!(f, "{index_type_display:>}")?; if let Some(name) = opt_index_name { write!(f, " {name}")?; @@ -572,8 +692,20 @@ pub enum KeyOrIndexDisplay { Index, } +impl KeyOrIndexDisplay { + pub fn is_none(self) -> bool { + matches!(self, Self::None) + } +} + impl fmt::Display for KeyOrIndexDisplay { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let left_space = matches!(f.align(), Some(fmt::Alignment::Right)); + + if left_space && !self.is_none() { + f.write_char(' ')? + } + match self { KeyOrIndexDisplay::None => { write!(f, "") @@ -613,6 +745,30 @@ impl fmt::Display for IndexType { } } } + +/// MySQLs index option. +/// +/// This structure used here [`MySQL` CREATE TABLE][1], [`MySQL` CREATE INDEX][2]. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html +/// [2]: https://dev.mysql.com/doc/refman/8.3/en/create-index.html +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum IndexOption { + Using(IndexType), + Comment(String), +} + +impl fmt::Display for IndexOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Using(index_type) => write!(f, "USING {index_type}"), + Self::Comment(s) => write!(f, "COMMENT '{s}'"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -896,6 +1052,7 @@ pub enum GeneratedExpressionMode { Stored, } +#[must_use] fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl<'a> fmt::Display for ConstraintName<'a> { @@ -909,6 +1066,36 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { ConstraintName(name) } +/// If `option` is +/// * `Some(inner)` => create display struct for `"{prefix}{inner}{postfix}"` +/// * `_` => do nothing +#[must_use] +fn display_option<'a, T: fmt::Display>( + prefix: &'a str, + postfix: &'a str, + option: &'a Option, +) -> impl fmt::Display + 'a { + struct OptionDisplay<'a, T>(&'a str, &'a str, &'a Option); + impl<'a, T: fmt::Display> fmt::Display for OptionDisplay<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(inner) = self.2 { + let (prefix, postfix) = (self.0, self.1); + write!(f, "{prefix}{inner}{postfix}")?; + } + Ok(()) + } + } + OptionDisplay(prefix, postfix, option) +} + +/// If `option` is +/// * `Some(inner)` => create display struct for `" {inner}"` +/// * `_` => do nothing +#[must_use] +fn display_option_spaced(option: &Option) -> impl fmt::Display + '_ { + display_option(" ", "", option) +} + /// ` = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]` /// /// Used in UNIQUE and foreign key constraints. The individual settings may occur in any order. diff --git a/src/ast/dml.rs b/src/ast/dml.rs new file mode 100644 index 000000000..badc58a7d --- /dev/null +++ b/src/ast/dml.rs @@ -0,0 +1,84 @@ +// 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. + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, vec::Vec}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +use super::{ + Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, + Query, SelectItem, SqliteOnConflict, TableWithJoins, +}; + +/// INSERT statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Insert { + /// Only for Sqlite + pub or: Option, + /// Only for mysql + pub ignore: bool, + /// INTO - optional keyword + pub into: bool, + /// TABLE + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// table_name as foo (for PostgreSQL) + pub table_alias: Option, + /// COLUMNS + pub columns: Vec, + /// Overwrite (Hive) + pub overwrite: bool, + /// A SQL query that specifies what to insert + pub source: Option>, + /// partitioned insert (Hive) + pub partitioned: Option>, + /// Columns defined after PARTITION + pub after_columns: Vec, + /// whether the insert has the table keyword (Hive) + pub table: bool, + pub on: Option, + /// RETURNING + pub returning: Option>, + /// Only for mysql + pub replace_into: bool, + /// Only for mysql + pub priority: Option, + /// Only for mysql + pub insert_alias: Option, +} + +/// DELETE statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Delete { + /// Multi tables delete are supported in mysql + pub tables: Vec, + /// FROM + pub from: FromTable, + /// USING (Snowflake, Postgres, MySQL) + pub using: Option>, + /// WHERE + pub selection: Option, + /// RETURNING + pub returning: Option>, + /// ORDER BY (MySQL) + pub order_by: Vec, + /// LIMIT (MySQL) + pub limit: Option, +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2a4e60892..3abb0e53d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -33,19 +33,22 @@ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue} pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, DeferrableInitial, GeneratedAs, - GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, + GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; +pub use self::dml::{Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - ConnectBy, Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, - ForClause, ForJson, ForXml, GroupByExpr, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, - JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, - NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, - ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, - SetQuantifier, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, - ValueTableMode, Values, WildcardAdditionalOptions, With, + AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, + ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, ForJson, ForXml, GroupByExpr, + IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, JoinOperator, JsonTableColumn, + JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, + MatchRecognizeSymbol, Measure, NamedWindowDefinition, NonBlock, Offset, OffsetRows, + OrderByExpr, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, + ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, + SetQuantifier, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, + Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, @@ -60,6 +63,7 @@ pub use visitor::*; mod data_type; mod dcl; mod ddl; +mod dml; pub mod helpers; mod operator; mod query; @@ -229,7 +233,7 @@ impl fmt::Display for Interval { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let value = self.value.as_ref(); match ( - self.leading_field, + &self.leading_field, self.leading_precision, self.fractional_seconds_precision, ) { @@ -248,13 +252,13 @@ impl fmt::Display for Interval { } _ => { write!(f, "INTERVAL {value}")?; - if let Some(leading_field) = self.leading_field { + if let Some(leading_field) = &self.leading_field { write!(f, " {leading_field}")?; } if let Some(leading_precision) = self.leading_precision { write!(f, " ({leading_precision})")?; } - if let Some(last_field) = self.last_field { + if let Some(last_field) = &self.last_field { write!(f, " TO {last_field}")?; } if let Some(fractional_seconds_precision) = self.fractional_seconds_precision { @@ -347,6 +351,23 @@ impl fmt::Display for StructField { } } +/// A dictionary field within a dictionary. +/// +/// [duckdb]: https://duckdb.org/docs/sql/data_types/struct#creating-structs +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DictionaryField { + pub key: Ident, + pub value: Box, +} + +impl fmt::Display for DictionaryField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.key, self.value) + } +} + /// Options for `CAST` / `TRY_CAST` /// BigQuery: #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -357,6 +378,60 @@ pub enum CastFormat { ValueAtTimeZone(Value, Value), } +/// Represents the syntax/style used in a map access. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MapAccessSyntax { + /// Access using bracket notation. `mymap[mykey]` + Bracket, + /// Access using period notation. `mymap.mykey` + Period, +} + +/// Expression used to access a value in a nested structure. +/// +/// Example: `SAFE_OFFSET(0)` in +/// ```sql +/// SELECT mymap[SAFE_OFFSET(0)]; +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MapAccessKey { + pub key: Expr, + pub syntax: MapAccessSyntax, +} + +impl fmt::Display for MapAccessKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.syntax { + MapAccessSyntax::Bracket => write!(f, "[{}]", self.key), + MapAccessSyntax::Period => write!(f, ".{}", self.key), + } + } +} + +/// The syntax used for in a cast expression. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CastKind { + /// The standard SQL cast syntax, e.g. `CAST( as )` + Cast, + /// A cast that returns `NULL` on failure, e.g. `TRY_CAST( as )`. + /// + /// See . + /// See . + TryCast, + /// A cast that returns `NULL` on failure, bigQuery-specific , e.g. `SAFE_CAST( as )`. + /// + /// See . + SafeCast, + /// ` :: ` + DoubleColon, +} + /// An SQL expression of any type. /// /// The parser does not distinguish between expressions of different types @@ -450,21 +525,21 @@ pub enum Expr { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// `ILIKE` (case-insensitive `LIKE`) ILike { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// SIMILAR TO regex SimilarTo { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// MySQL: RLIKE regex or REGEXP regex RLike { @@ -504,25 +579,7 @@ pub enum Expr { }, /// `CAST` an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))` Cast { - expr: Box, - data_type: DataType, - // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery - // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax - format: Option, - }, - /// `TRY_CAST` an expression to a different data type e.g. `TRY_CAST(foo AS VARCHAR(123))` - // this differs from CAST in the choice of how to implement invalid conversions - TryCast { - expr: Box, - data_type: DataType, - // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery - // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax - format: Option, - }, - /// `SAFE_CAST` an expression to a different data type e.g. `SAFE_CAST(foo AS FLOAT64)` - // only available for BigQuery: https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-and-operators#safe_casting - // this works the same as `TRY_CAST` - SafeCast { + kind: CastKind, expr: Box, data_type: DataType, // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery @@ -568,13 +625,18 @@ pub enum Expr { /// ```sql /// SUBSTRING( [FROM ] [FOR ]) /// ``` + /// or + /// ```sql + /// SUBSTRING(, , ) + /// ``` Substring { expr: Box, substring_from: Option>, substring_for: Option>, - // Some dialects use `SUBSTRING(expr [FROM start] [FOR len])` syntax while others omit FROM, - // FOR keywords (e.g. Microsoft SQL Server). This flags is used for formatting. + /// false if the expression is represented using the `SUBSTRING(expr [FROM start] [FOR len])` syntax + /// true if the expression is represented using the `SUBSTRING(expr, start, len)` syntax + /// This flag is used for formatting. special: bool, }, /// ```sql @@ -625,7 +687,7 @@ pub enum Expr { /// MapAccess { column: Box, - keys: Vec, + keys: Vec, }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), @@ -691,6 +753,14 @@ pub enum Expr { expr: Box, name: Ident, }, + /// `DuckDB` specific `Struct` literal expression [1] + /// + /// Syntax: + /// ```sql + /// syntax: {'field_name': expr1[, ... ]} + /// ``` + /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + Dictionary(Vec), /// An array index expression e.g. `(ARRAY[1, 2])[1]` or `(current_schemas(FALSE))[1]` ArrayIndex { obj: Box, @@ -756,15 +826,7 @@ impl fmt::Display for Expr { Expr::Identifier(s) => write!(f, "{s}"), Expr::SigmaParameter(s) => write!(f, "@sigma.{s}"), Expr::MapAccess { column, keys } => { - write!(f, "{column}")?; - for k in keys { - match k { - k @ Expr::Value(Value::Number(_, _)) => write!(f, "[{k}]")?, - Expr::Value(Value::SingleQuotedString(s)) => write!(f, "[\"{s}\"]")?, - _ => write!(f, "[{k}]")?, - } - } - Ok(()) + write!(f, "{column}{}", display_separated(keys, "")) } Expr::Wildcard => f.write_str("*"), Expr::QualifiedWildcard(prefix) => write!(f, "{}.*", prefix), @@ -956,38 +1018,36 @@ impl fmt::Display for Expr { write!(f, ")") } Expr::Cast { + kind, expr, data_type, format, - } => { - if let Some(format) = format { - write!(f, "CAST({expr} AS {data_type} FORMAT {format})") - } else { - write!(f, "CAST({expr} AS {data_type})") + } => match kind { + CastKind::Cast => { + if let Some(format) = format { + write!(f, "CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "CAST({expr} AS {data_type})") + } } - } - Expr::TryCast { - expr, - data_type, - format, - } => { - if let Some(format) = format { - write!(f, "TRY_CAST({expr} AS {data_type} FORMAT {format})") - } else { - write!(f, "TRY_CAST({expr} AS {data_type})") + CastKind::TryCast => { + if let Some(format) = format { + write!(f, "TRY_CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "TRY_CAST({expr} AS {data_type})") + } } - } - Expr::SafeCast { - expr, - data_type, - format, - } => { - if let Some(format) = format { - write!(f, "SAFE_CAST({expr} AS {data_type} FORMAT {format})") - } else { - write!(f, "SAFE_CAST({expr} AS {data_type})") + CastKind::SafeCast => { + if let Some(format) = format { + write!(f, "SAFE_CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "SAFE_CAST({expr} AS {data_type})") + } } - } + CastKind::DoubleColon => { + write!(f, "{expr}::{data_type}") + } + }, Expr::Extract { field, expr } => write!(f, "EXTRACT({field} FROM {expr})"), Expr::Ceil { expr, field } => { if field == &DateTimeField::NoDateTime { @@ -1164,6 +1224,9 @@ impl fmt::Display for Expr { Expr::Named { expr, name } => { write!(f, "{} AS {}", expr, name) } + Expr::Dictionary(fields) => { + write!(f, "{{{}}}", display_comma_separated(fields)) + } Expr::ArrayIndex { obj, indexes } => { write!(f, "{obj}")?; for i in indexes { @@ -1237,11 +1300,19 @@ impl Display for WindowType { } } -/// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) +/// A window specification (i.e. `OVER ([window_name] PARTITION BY .. ORDER BY .. etc.)`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WindowSpec { + /// Optional window name. + /// + /// You can find it at least in [MySQL][1], [BigQuery][2], [PostgreSQL][3] + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/window-functions-named-windows.html + /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/window-function-calls + /// [3]: https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS + pub window_name: Option, /// `OVER (PARTITION BY ...)` pub partition_by: Vec, /// `OVER (ORDER BY ...)` @@ -1253,7 +1324,12 @@ pub struct WindowSpec { impl fmt::Display for WindowSpec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut delim = ""; + if let Some(window_name) = &self.window_name { + delim = " "; + write!(f, "{window_name}")?; + } if !self.partition_by.is_empty() { + f.write_str(delim)?; delim = " "; write!( f, @@ -1752,38 +1828,7 @@ pub enum Statement { /// ```sql /// INSERT /// ``` - Insert { - /// Only for Sqlite - or: Option, - /// Only for mysql - ignore: bool, - /// INTO - optional keyword - into: bool, - /// TABLE - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - /// table_name as foo (for PostgreSQL) - table_alias: Option, - /// COLUMNS - columns: Vec, - /// Overwrite (Hive) - overwrite: bool, - /// A SQL query that specifies what to insert - source: Option>, - /// partitioned insert (Hive) - partitioned: Option>, - /// Columns defined after PARTITION - after_columns: Vec, - /// whether the insert has the table keyword (Hive) - table: bool, - on: Option, - /// RETURNING - returning: Option>, - /// Only for mysql - replace_into: bool, - /// Only for mysql - priority: Option, - }, + Insert(Insert), /// ```sql /// INSTALL /// ``` @@ -1873,22 +1918,7 @@ pub enum Statement { /// ```sql /// DELETE /// ``` - Delete { - /// Multi tables delete are supported in mysql - tables: Vec, - /// FROM - from: FromTable, - /// USING (Snowflake, Postgres, MySQL) - using: Option>, - /// WHERE - selection: Option, - /// RETURNING - returning: Option>, - /// ORDER BY (MySQL) - order_by: Vec, - /// LIMIT (MySQL) - limit: Option, - }, + Delete(Delete), /// ```sql /// CREATE VIEW /// ``` @@ -2016,6 +2046,19 @@ pub enum Statement { authorization_owner: Option, }, /// ```sql + /// CREATE SECRET + /// ``` + /// See [duckdb](https://duckdb.org/docs/sql/statements/create_secret.html) + CreateSecret { + or_replace: bool, + temporary: Option, + if_not_exists: bool, + name: Option, + storage_specifier: Option, + secret_type: Ident, + options: Vec, + }, + /// ```sql /// ALTER TABLE /// ``` AlterTable { @@ -2064,6 +2107,31 @@ pub enum Statement { /// true if the syntax is 'ATTACH DATABASE', false if it's just 'ATTACH' database: bool, }, + /// (DuckDB-specific) + /// ```sql + /// ATTACH 'sqlite_file.db' AS sqlite_db (READ_ONLY, TYPE SQLITE); + /// ``` + /// See + AttachDuckDBDatabase { + if_not_exists: bool, + /// true if the syntax is 'ATTACH DATABASE', false if it's just 'ATTACH' + database: bool, + /// An expression that indicates the path to the database file + database_path: Ident, + database_alias: Option, + attach_options: Vec, + }, + /// (DuckDB-specific) + /// ```sql + /// DETACH db_alias; + /// ``` + /// See + DetachDuckDBDatabase { + if_exists: bool, + /// true if the syntax is 'DETACH DATABASE', false if it's just 'DETACH' + database: bool, + database_alias: Ident, + }, /// ```sql /// DROP [TABLE, VIEW, ...] /// ``` @@ -2097,6 +2165,15 @@ pub enum Statement { option: Option, }, /// ```sql + /// DROP SECRET + /// ``` + DropSecret { + if_exists: bool, + temporary: Option, + name: Ident, + storage_specifier: Option, + }, + /// ```sql /// DECLARE /// ``` /// Declare Cursor Variables @@ -2496,20 +2573,23 @@ pub enum Statement { /// RELEASE [ SAVEPOINT ] savepoint_name /// ``` ReleaseSavepoint { name: Ident }, + /// A `MERGE` statement. + /// /// ```sql - /// MERGE INTO + /// MERGE INTO USING ON { matchedClause | notMatchedClause } [ ... ] /// ``` - /// Based on Snowflake. See + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) Merge { - // optional INTO keyword + /// optional INTO keyword into: bool, - // Specifies the table to merge + /// Specifies the table to merge table: TableFactor, - // Specifies the table or subquery to join with the target table + /// Specifies the table or subquery to join with the target table source: TableFactor, - // Specifies the expression on which to join the target table and source + /// 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. + /// Specifies the actions to perform when values match or do not match. clauses: Vec, }, /// ```sql @@ -2748,6 +2828,40 @@ impl fmt::Display for Statement { let keyword = if *database { "DATABASE " } else { "" }; write!(f, "ATTACH {keyword}{database_file_name} AS {schema_name}") } + Statement::AttachDuckDBDatabase { + if_not_exists, + database, + database_path, + database_alias, + attach_options, + } => { + write!( + f, + "ATTACH{database}{if_not_exists} {database_path}", + database = if *database { " DATABASE" } else { "" }, + if_not_exists = if *if_not_exists { " IF NOT EXISTS" } else { "" }, + )?; + if let Some(alias) = database_alias { + write!(f, " AS {alias}")?; + } + if !attach_options.is_empty() { + write!(f, " ({})", display_comma_separated(attach_options))?; + } + Ok(()) + } + Statement::DetachDuckDBDatabase { + if_exists, + database, + database_alias, + } => { + write!( + f, + "DETACH{database}{if_exists} {database_alias}", + database = if *database { " DATABASE" } else { "" }, + if_exists = if *if_exists { " IF EXISTS" } else { "" }, + )?; + Ok(()) + } Statement::Analyze { table_name, partitions, @@ -2781,23 +2895,25 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Insert { - or, - ignore, - into, - table_name, - table_alias, - overwrite, - partitioned, - columns, - after_columns, - source, - table, - on, - returning, - replace_into, - priority, - } => { + Statement::Insert(insert) => { + let Insert { + or, + ignore, + into, + table_name, + table_alias, + overwrite, + partitioned, + columns, + after_columns, + source, + table, + on, + returning, + replace_into, + priority, + insert_alias, + } = insert; let table_name = if let Some(alias) = table_alias { format!("{table_name} AS {alias}") } else { @@ -2846,6 +2962,16 @@ impl fmt::Display for Statement { write!(f, "DEFAULT VALUES")?; } + if let Some(insert_alias) = insert_alias { + write!(f, " AS {0}", insert_alias.row_alias)?; + + if let Some(col_aliases) = &insert_alias.col_aliases { + if !col_aliases.is_empty() { + write!(f, " ({})", display_comma_separated(col_aliases))?; + } + } + } + if let Some(on) = on { write!(f, "{on}")?; } @@ -2932,15 +3058,16 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Delete { - tables, - from, - using, - selection, - returning, - order_by, - limit, - } => { + Statement::Delete(delete) => { + let Delete { + tables, + from, + using, + selection, + returning, + order_by, + limit, + } = delete; write!(f, "DELETE ")?; if !tables.is_empty() { write!(f, "{} ", display_comma_separated(tables))?; @@ -3521,6 +3648,41 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateSecret { + or_replace, + temporary, + if_not_exists, + name, + storage_specifier, + secret_type, + options, + } => { + write!( + f, + "CREATE {or_replace}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(t) = temporary { + write!(f, "{}", if *t { "TEMPORARY " } else { "PERSISTENT " })?; + } + write!( + f, + "SECRET {if_not_exists}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if let Some(n) = name { + write!(f, "{n} ")?; + }; + if let Some(s) = storage_specifier { + write!(f, "IN {s} ")?; + } + write!(f, "( TYPE {secret_type}",)?; + if !options.is_empty() { + write!(f, ", {o}", o = display_comma_separated(options))?; + } + write!(f, " )")?; + Ok(()) + } Statement::AlterTable { name, if_exists, @@ -3601,6 +3763,26 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DropSecret { + if_exists, + temporary, + name, + storage_specifier, + } => { + write!(f, "DROP ")?; + if let Some(t) = temporary { + write!(f, "{}", if *t { "TEMPORARY " } else { "PERSISTENT " })?; + } + write!( + f, + "SECRET {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + )?; + if let Some(s) = storage_specifier { + write!(f, " FROM {s}")?; + } + Ok(()) + } Statement::Discard { object_type } => { write!(f, "DISCARD {object_type}")?; Ok(()) @@ -4218,6 +4400,14 @@ pub enum OnInsert { OnConflict(OnConflict), } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct InsertAliases { + pub row_alias: ObjectName, + pub col_aliases: Option>, +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -4554,6 +4744,8 @@ pub enum FunctionArgOperator { Equals, /// function(arg1 => value1) RightArrow, + /// function(arg1 := value1) + Assignment, } impl fmt::Display for FunctionArgOperator { @@ -4561,6 +4753,7 @@ impl fmt::Display for FunctionArgOperator { match self { FunctionArgOperator::Equals => f.write_str("="), FunctionArgOperator::RightArrow => f.write_str("=>"), + FunctionArgOperator::Assignment => f.write_str(":="), } } } @@ -4616,7 +4809,7 @@ pub struct Function { pub args: Vec, /// e.g. `x > 5` in `COUNT(x) FILTER (WHERE x > 5)` pub filter: Option>, - // Snowflake/MSSQL supports diffrent options for null treatment in rank functions + // Snowflake/MSSQL supports different options for null treatment in rank functions pub null_treatment: Option, pub within_group: Option>, pub over: Option, @@ -5029,6 +5222,39 @@ impl fmt::Display for SqlOption { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SecretOption { + pub key: Ident, + pub value: Ident, +} + +impl fmt::Display for SecretOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.key, self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AttachDuckDBDatabaseOption { + ReadOnly(Option), + Type(Ident), +} + +impl fmt::Display for AttachDuckDBDatabaseOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AttachDuckDBDatabaseOption::ReadOnly(Some(true)) => write!(f, "READ_ONLY true"), + AttachDuckDBDatabaseOption::ReadOnly(Some(false)) => write!(f, "READ_ONLY false"), + AttachDuckDBDatabaseOption::ReadOnly(None) => write!(f, "READ_ONLY"), + AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {}", t), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5355,75 +5581,196 @@ impl fmt::Display for CopyLegacyCsvOption { } } -/// `MERGE` Statement +/// Variant of `WHEN` clause used within a `MERGE` Statement. /// +/// Example: /// ```sql -/// MERGE INTO USING ON { matchedClause | notMatchedClause } [ ... ] +/// MERGE INTO T USING U ON FALSE WHEN MATCHED THEN DELETE /// ``` +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MergeClauseKind { + /// `WHEN MATCHED` + Matched, + /// `WHEN NOT MATCHED` + NotMatched, + /// `WHEN MATCHED BY TARGET` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + NotMatchedByTarget, + /// `WHEN MATCHED BY SOURCE` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + NotMatchedBySource, +} + +impl Display for MergeClauseKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MergeClauseKind::Matched => write!(f, "MATCHED"), + MergeClauseKind::NotMatched => write!(f, "NOT MATCHED"), + MergeClauseKind::NotMatchedByTarget => write!(f, "NOT MATCHED BY TARGET"), + MergeClauseKind::NotMatchedBySource => write!(f, "NOT MATCHED BY SOURCE"), + } + } +} + +/// The type of expression used to insert rows within a `MERGE` statement. /// -/// See [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum MergeClause { - MatchedUpdate { - predicate: Option, - assignments: Vec, - }, - MatchedDelete(Option), - NotMatched { - predicate: Option, - columns: Vec, - values: Values, - }, +pub enum MergeInsertKind { + /// The insert expression is defined from an explicit `VALUES` clause + /// + /// Example: + /// ```sql + /// INSERT VALUES(product, quantity) + /// ``` + Values(Values), + /// The insert expression is defined using only the `ROW` keyword. + /// + /// Example: + /// ```sql + /// INSERT ROW + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) + Row, } -impl fmt::Display for MergeClause { +impl Display for MergeInsertKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use MergeClause::*; - write!(f, "WHEN")?; match self { - MatchedUpdate { - predicate, - assignments, - } => { - write!(f, " MATCHED")?; - if let Some(pred) = predicate { - write!(f, " AND {pred}")?; - } - write!( - f, - " THEN UPDATE SET {}", - display_comma_separated(assignments) - ) + MergeInsertKind::Values(values) => { + write!(f, "{values}") } - MatchedDelete(predicate) => { - write!(f, " MATCHED")?; - if let Some(pred) = predicate { - write!(f, " AND {pred}")?; - } - write!(f, " THEN DELETE") + MergeInsertKind::Row => { + write!(f, "ROW") } - NotMatched { - predicate, - columns, - values, - } => { - write!(f, " NOT MATCHED")?; - if let Some(pred) = predicate { - write!(f, " AND {pred}")?; - } - write!( - f, - " THEN INSERT ({}) {}", - display_comma_separated(columns), - values - ) + } + } +} + +/// The expression used to insert rows within a `MERGE` statement. +/// +/// Examples +/// ```sql +/// INSERT (product, quantity) VALUES(product, quantity) +/// INSERT ROW +/// ``` +/// +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MergeInsertExpr { + /// Columns (if any) specified by the insert. + /// + /// Example: + /// ```sql + /// INSERT (product, quantity) VALUES(product, quantity) + /// INSERT (product, quantity) ROW + /// ``` + pub columns: Vec, + /// The insert type used by the statement. + pub kind: MergeInsertKind, +} + +impl Display for MergeInsertExpr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if !self.columns.is_empty() { + write!(f, "({}) ", display_comma_separated(self.columns.as_slice()))?; + } + write!(f, "{}", self.kind) + } +} + +/// Underlying statement of a when clause within a `MERGE` Statement +/// +/// Example +/// ```sql +/// INSERT (product, quantity) VALUES(product, quantity) +/// ``` +/// +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MergeAction { + /// An `INSERT` clause + /// + /// Example: + /// ```sql + /// INSERT (product, quantity) VALUES(product, quantity) + /// ``` + Insert(MergeInsertExpr), + /// An `UPDATE` clause + /// + /// Example: + /// ```sql + /// UPDATE SET quantity = T.quantity + S.quantity + /// ``` + Update { assignments: Vec }, + /// A plain `DELETE` clause + Delete, +} + +impl Display for MergeAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MergeAction::Insert(insert) => { + write!(f, "INSERT {insert}") + } + MergeAction::Update { assignments } => { + write!(f, "UPDATE SET {}", display_comma_separated(assignments)) + } + MergeAction::Delete => { + write!(f, "DELETE") } } } } +/// A when clause within a `MERGE` Statement +/// +/// Example: +/// ```sql +/// WHEN NOT MATCHED BY SOURCE AND product LIKE '%washer%' THEN DELETE +/// ``` +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/merge) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MergeClause { + pub clause_kind: MergeClauseKind, + pub predicate: Option, + pub action: MergeAction, +} + +impl Display for MergeClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let MergeClause { + clause_kind, + predicate, + action, + } = self; + + write!(f, "WHEN {clause_kind}")?; + if let Some(pred) = predicate { + write!(f, " AND {pred}")?; + } + write!(f, " THEN {action}") + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5655,6 +6002,46 @@ impl fmt::Display for FunctionBehavior { } } +/// These attributes describe the behavior of the function when called with a null argument. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FunctionCalledOnNull { + CalledOnNullInput, + ReturnsNullOnNullInput, + Strict, +} + +impl fmt::Display for FunctionCalledOnNull { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionCalledOnNull::CalledOnNullInput => write!(f, "CALLED ON NULL INPUT"), + FunctionCalledOnNull::ReturnsNullOnNullInput => write!(f, "RETURNS NULL ON NULL INPUT"), + FunctionCalledOnNull::Strict => write!(f, "STRICT"), + } + } +} + +/// If it is safe for PostgreSQL to call the function from multiple threads at once +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FunctionParallel { + Unsafe, + Restricted, + Safe, +} + +impl fmt::Display for FunctionParallel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionParallel::Unsafe => write!(f, "PARALLEL UNSAFE"), + FunctionParallel::Restricted => write!(f, "PARALLEL RESTRICTED"), + FunctionParallel::Safe => write!(f, "PARALLEL SAFE"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5675,7 +6062,7 @@ impl fmt::Display for FunctionDefinition { /// Postgres specific feature. /// -/// See [Postgresdocs](https://www.postgresql.org/docs/15/sql-createfunction.html) +/// See [Postgres docs](https://www.postgresql.org/docs/15/sql-createfunction.html) /// for more details #[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -5685,6 +6072,10 @@ pub struct CreateFunctionBody { pub language: Option, /// IMMUTABLE | STABLE | VOLATILE pub behavior: Option, + /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT + pub called_on_null: Option, + /// PARALLEL { UNSAFE | RESTRICTED | SAFE } + pub parallel: Option, /// AS 'definition' /// /// Note that Hive's `AS class_name` is also parsed here. @@ -5703,6 +6094,12 @@ impl fmt::Display for CreateFunctionBody { if let Some(behavior) = &self.behavior { write!(f, " {behavior}")?; } + if let Some(called_on_null) = &self.called_on_null { + write!(f, " {called_on_null}")?; + } + if let Some(parallel) = &self.parallel { + write!(f, " {parallel}")?; + } if let Some(definition) = &self.as_ { write!(f, " AS {definition}")?; } @@ -5927,6 +6324,28 @@ impl fmt::Display for HiveSetLocation { } } +/// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MySQLColumnPosition { + First, + After(Ident), +} + +impl Display for MySQLColumnPosition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MySQLColumnPosition::First => Ok(write!(f, "FIRST")?), + MySQLColumnPosition::After(ident) => { + let column_name = &ident.value; + Ok(write!(f, "AFTER {column_name}")?) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/query.rs b/src/ast/query.rs index a2bfea102..0a02a7342 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -92,8 +92,6 @@ impl fmt::Display for Query { pub enum SetExpr { /// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations) Select(Box