diff --git a/.asf.yaml b/.asf.yaml index 0534705bc..08b31804d 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -50,3 +50,7 @@ github: - "test (beta)" - "test (nightly)" - "Release Audit Tool (RAT)" + pull_requests: + # enable updating head branches of pull requests + allow_update_branch: true + allow_auto_merge: true diff --git a/Cargo.toml b/Cargo.toml index 48db33b3f..ed94bbbdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "allo # of dev-dependencies because of # https://github.com/rust-lang/cargo/issues/1596 serde_json = { version = "1.0", optional = true } -sqlparser_derive = { version = "0.3.0", path = "derive", optional = true } +sqlparser_derive = { version = "0.4.0", path = "derive", optional = true } [dev-dependencies] simple_logger = "5.0" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 7b6477300..549477041 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -18,7 +18,7 @@ [package] name = "sqlparser_derive" description = "Procedural (proc) macros for sqlparser" -version = "0.3.0" +version = "0.4.0" authors = ["sqlparser-rs authors"] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser_derive/" diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index f8523e844..6da6a90d0 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -375,7 +375,7 @@ pub enum DataType { /// Databricks timestamp without time zone. See [1]. /// /// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type - TimestampNtz, + TimestampNtz(Option), /// Interval type. Interval { /// [PostgreSQL] fields specification like `INTERVAL YEAR TO MONTH`. @@ -676,7 +676,9 @@ impl fmt::Display for DataType { DataType::Timestamp(precision, timezone_info) => { format_datetime_precision_and_tz(f, "TIMESTAMP", precision, timezone_info) } - DataType::TimestampNtz => write!(f, "TIMESTAMP_NTZ"), + DataType::TimestampNtz(precision) => { + format_type_with_optional_length(f, "TIMESTAMP_NTZ", precision, false) + } DataType::Datetime64(precision, timezone) => { format_clickhouse_datetime_precision_and_timezone( f, diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 079894075..d04875a73 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -28,8 +28,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use super::{display_comma_separated, Expr, Ident, Password}; +use super::{display_comma_separated, Expr, Ident, Password, Spanned}; use crate::ast::{display_separated, ObjectName}; +use crate::tokenizer::Span; /// An option in `ROLE` statement. /// @@ -252,3 +253,113 @@ impl fmt::Display for SecondaryRoles { } } } + +/// CREATE ROLE statement +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateRole { + pub names: Vec, + pub if_not_exists: bool, + // Postgres + pub login: Option, + pub inherit: Option, + pub bypassrls: Option, + pub password: Option, + pub superuser: Option, + pub create_db: Option, + pub create_role: Option, + pub replication: Option, + pub connection_limit: Option, + pub valid_until: Option, + pub in_role: Vec, + pub in_group: Vec, + pub role: Vec, + pub user: Vec, + pub admin: Vec, + // MSSQL + pub authorization_owner: Option, +} + +impl fmt::Display for CreateRole { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + names = display_separated(&self.names, ", "), + superuser = match self.superuser { + Some(true) => " SUPERUSER", + Some(false) => " NOSUPERUSER", + None => "" + }, + create_db = match self.create_db { + Some(true) => " CREATEDB", + Some(false) => " NOCREATEDB", + None => "" + }, + create_role = match self.create_role { + Some(true) => " CREATEROLE", + Some(false) => " NOCREATEROLE", + None => "" + }, + inherit = match self.inherit { + Some(true) => " INHERIT", + Some(false) => " NOINHERIT", + None => "" + }, + login = match self.login { + Some(true) => " LOGIN", + Some(false) => " NOLOGIN", + None => "" + }, + replication = match self.replication { + Some(true) => " REPLICATION", + Some(false) => " NOREPLICATION", + None => "" + }, + bypassrls = match self.bypassrls { + Some(true) => " BYPASSRLS", + Some(false) => " NOBYPASSRLS", + None => "" + } + )?; + if let Some(limit) = &self.connection_limit { + write!(f, " CONNECTION LIMIT {limit}")?; + } + match &self.password { + Some(Password::Password(pass)) => write!(f, " PASSWORD {pass}")?, + Some(Password::NullPassword) => write!(f, " PASSWORD NULL")?, + None => {} + }; + if let Some(until) = &self.valid_until { + write!(f, " VALID UNTIL {until}")?; + } + if !self.in_role.is_empty() { + write!(f, " IN ROLE {}", display_comma_separated(&self.in_role))?; + } + if !self.in_group.is_empty() { + write!(f, " IN GROUP {}", display_comma_separated(&self.in_group))?; + } + if !self.role.is_empty() { + write!(f, " ROLE {}", display_comma_separated(&self.role))?; + } + if !self.user.is_empty() { + write!(f, " USER {}", display_comma_separated(&self.user))?; + } + if !self.admin.is_empty() { + write!(f, " ADMIN {}", display_comma_separated(&self.admin))?; + } + if let Some(owner) = &self.authorization_owner { + write!(f, " AUTHORIZATION {owner}")?; + } + Ok(()) + } +} + +impl Spanned for CreateRole { + fn span(&self) -> Span { + Span::empty() + } +} diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c4f769675..fd481213f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -19,7 +19,7 @@ //! (commonly referred to as Data Definition Language, or DDL) #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, vec::Vec}; +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; use core::fmt::{self, Display, Write}; #[cfg(feature = "serde")] @@ -30,14 +30,19 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, ArgMode, CommentDef, ConditionalStatements, - CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, DataType, - Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, - FunctionParallel, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, - InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, - OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, - SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, - TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, + display_comma_separated, display_separated, + table_constraints::{ + CheckConstraint, ForeignKeyConstraint, PrimaryKeyConstraint, TableConstraint, + UniqueConstraint, + }, + ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, + CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, CreateViewParams, DataType, Expr, + FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDesc, FunctionDeterminismSpecifier, + FunctionParallel, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, + HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, + OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, + RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, + Tag, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; @@ -53,6 +58,22 @@ pub struct IndexColumn { pub operator_class: Option, } +impl From for IndexColumn { + fn from(c: Ident) -> Self { + Self { + column: OrderByExpr::from(c), + operator_class: None, + } + } +} + +impl<'a> From<&'a str> for IndexColumn { + fn from(c: &'a str) -> Self { + let ident = Ident::new(c); + ident.into() + } +} + impl fmt::Display for IndexColumn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.column)?; @@ -1029,291 +1050,6 @@ impl fmt::Display for AlterColumnOperation { } } -/// A table-level constraint, specified in a `CREATE TABLE` or an -/// `ALTER TABLE ADD ` 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 TableConstraint { - /// 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, - /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` - nulls_distinct: NullsDistinctOption, - }, - /// 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, - index_options: Vec, - characteristics: Option, - }, - /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () - /// REFERENCES () - /// { [ON DELETE ] [ON UPDATE ] | - /// [ON UPDATE ] [ON DELETE ] - /// }`). - ForeignKey { - name: Option, - /// MySQL-specific field - /// - index_name: Option, - columns: Vec, - foreign_table: ObjectName, - referred_columns: Vec, - on_delete: Option, - on_update: Option, - characteristics: Option, - }, - /// `[ CONSTRAINT ] CHECK () [[NOT] ENFORCED]` - Check { - name: Option, - expr: Box, - /// MySQL-specific syntax - /// - enforced: Option, - }, - /// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage - /// is restricted to MySQL, as no other dialects that support this syntax were found. - /// - /// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` - /// - /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html - Index { - /// Whether this index starts with KEY (true) or INDEX (false), to maintain the same syntax. - display_as_key: bool, - /// Index name. - name: Option, - /// Optional [index type][1]. - /// - /// [1]: IndexType - index_type: Option, - /// Referred column identifier list. - columns: Vec, - /// Optional index options such as `USING`; see [`IndexOption`]. - index_options: Vec, - }, - /// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition is exactly the same, - /// and MySQL displays both the same way, it is part of this definition as well. - /// - /// Supported syntax: - /// - /// ```markdown - /// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) - /// - /// key_part: col_name - /// ``` - /// - /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html - /// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html - FulltextOrSpatial { - /// Whether this is a `FULLTEXT` (true) or `SPATIAL` (false) definition. - fulltext: bool, - /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. - index_type_display: KeyOrIndexDisplay, - /// Optional index name. - opt_index_name: Option, - /// Referred column identifier list. - columns: Vec, - }, -} - -impl fmt::Display for TableConstraint { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - TableConstraint::Unique { - name, - index_name, - index_type_display, - index_type, - columns, - index_options, - characteristics, - nulls_distinct, - } => { - write!( - f, - "{}UNIQUE{nulls_distinct}{index_type_display:>}{}{} ({})", - 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::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 { - name, - index_name, - columns, - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => { - write!( - f, - "{}FOREIGN KEY{} ({}) REFERENCES {}", - display_constraint_name(name), - display_option_spaced(index_name), - display_comma_separated(columns), - foreign_table, - )?; - if !referred_columns.is_empty() { - write!(f, "({})", display_comma_separated(referred_columns))?; - } - if let Some(action) = on_delete { - write!(f, " ON DELETE {action}")?; - } - if let Some(action) = on_update { - write!(f, " ON UPDATE {action}")?; - } - if let Some(characteristics) = characteristics { - write!(f, " {characteristics}")?; - } - Ok(()) - } - TableConstraint::Check { - name, - expr, - enforced, - } => { - write!(f, "{}CHECK ({})", display_constraint_name(name), expr)?; - if let Some(b) = enforced { - write!(f, " {}", if *b { "ENFORCED" } else { "NOT ENFORCED" }) - } else { - Ok(()) - } - } - TableConstraint::Index { - display_as_key, - name, - index_type, - columns, - index_options, - } => { - write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; - if let Some(name) = name { - write!(f, " {name}")?; - } - if let Some(index_type) = index_type { - write!(f, " USING {index_type}")?; - } - write!(f, " ({})", display_comma_separated(columns))?; - if !index_options.is_empty() { - write!(f, " {}", display_comma_separated(index_options))?; - } - Ok(()) - } - Self::FulltextOrSpatial { - fulltext, - index_type_display, - opt_index_name, - columns, - } => { - if *fulltext { - write!(f, "FULLTEXT")?; - } else { - write!(f, "SPATIAL")?; - } - - write!(f, "{index_type_display:>}")?; - - if let Some(name) = opt_index_name { - write!(f, " {name}")?; - } - - write!(f, " ({})", display_comma_separated(columns))?; - - Ok(()) - } - } - } -} - /// Representation whether a definition can can contains the KEY or INDEX keywords with the same /// meaning. /// @@ -1458,12 +1194,19 @@ pub struct ProcedureParam { pub name: Ident, pub data_type: DataType, pub mode: Option, + pub default: Option, } impl fmt::Display for ProcedureParam { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(mode) = &self.mode { - write!(f, "{mode} {} {}", self.name, self.data_type) + if let Some(default) = &self.default { + write!(f, "{mode} {} {} = {}", self.name, self.data_type, default) + } else { + write!(f, "{mode} {} {}", self.name, self.data_type) + } + } else if let Some(default) = &self.default { + write!(f, "{} {} = {}", self.name, self.data_type, default) } else { write!(f, "{} {}", self.name, self.data_type) } @@ -1831,27 +1574,20 @@ pub enum ColumnOption { /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values) Alias(Expr), - /// `{ PRIMARY KEY | UNIQUE } []` - Unique { - is_primary: bool, - characteristics: Option, - }, - /// A referential integrity constraint (`[FOREIGN KEY REFERENCES - /// () + /// `PRIMARY KEY []` + PrimaryKey(PrimaryKeyConstraint), + /// `UNIQUE []` + Unique(UniqueConstraint), + /// A referential integrity constraint (`REFERENCES () + /// [ MATCH { FULL | PARTIAL | SIMPLE } ] /// { [ON DELETE ] [ON UPDATE ] | /// [ON UPDATE ] [ON DELETE ] - /// } + /// } /// [] /// `). - ForeignKey { - foreign_table: ObjectName, - referred_columns: Vec, - on_delete: Option, - on_update: Option, - characteristics: Option, - }, + ForeignKey(ForeignKeyConstraint), /// `CHECK ()` - Check(Expr), + Check(CheckConstraint), /// Dialect-specific options, such as: /// - MySQL's `AUTO_INCREMENT` or SQLite's `AUTOINCREMENT` /// - ... @@ -1920,6 +1656,29 @@ pub enum ColumnOption { Invisible, } +impl From for ColumnOption { + fn from(c: UniqueConstraint) -> Self { + ColumnOption::Unique(c) + } +} + +impl From for ColumnOption { + fn from(c: PrimaryKeyConstraint) -> Self { + ColumnOption::PrimaryKey(c) + } +} + +impl From for ColumnOption { + fn from(c: CheckConstraint) -> Self { + ColumnOption::Check(c) + } +} +impl From for ColumnOption { + fn from(fk: ForeignKeyConstraint) -> Self { + ColumnOption::ForeignKey(fk) + } +} + impl fmt::Display for ColumnOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use ColumnOption::*; @@ -1936,39 +1695,44 @@ impl fmt::Display for ColumnOption { } } Alias(expr) => write!(f, "ALIAS {expr}"), - Unique { - is_primary, - characteristics, - } => { - write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" })?; - if let Some(characteristics) = characteristics { + PrimaryKey(constraint) => { + write!(f, "PRIMARY KEY")?; + if let Some(characteristics) = &constraint.characteristics { write!(f, " {characteristics}")?; } Ok(()) } - ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => { - write!(f, "REFERENCES {foreign_table}")?; - if !referred_columns.is_empty() { - write!(f, " ({})", display_comma_separated(referred_columns))?; + Unique(constraint) => { + write!(f, "UNIQUE")?; + if let Some(characteristics) = &constraint.characteristics { + write!(f, " {characteristics}")?; + } + Ok(()) + } + ForeignKey(constraint) => { + write!(f, "REFERENCES {}", constraint.foreign_table)?; + if !constraint.referred_columns.is_empty() { + write!( + f, + " ({})", + display_comma_separated(&constraint.referred_columns) + )?; + } + if let Some(match_kind) = &constraint.match_kind { + write!(f, " {match_kind}")?; } - if let Some(action) = on_delete { + if let Some(action) = &constraint.on_delete { write!(f, " ON DELETE {action}")?; } - if let Some(action) = on_update { + if let Some(action) = &constraint.on_update { write!(f, " ON UPDATE {action}")?; } - if let Some(characteristics) = characteristics { + if let Some(characteristics) = &constraint.characteristics { write!(f, " {characteristics}")?; } Ok(()) } - Check(expr) => write!(f, "CHECK ({expr})"), + Check(constraint) => write!(f, "{constraint}"), DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")), CharacterSet(n) => write!(f, "CHARACTER SET {n}"), Collation(n) => write!(f, "COLLATE {n}"), @@ -2065,7 +1829,7 @@ pub enum GeneratedExpressionMode { } #[must_use] -fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { +pub(crate) fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl fmt::Display for ConstraintName<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -2082,7 +1846,7 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { /// * `Some(inner)` => create display struct for `"{prefix}{inner}{postfix}"` /// * `_` => do nothing #[must_use] -fn display_option<'a, T: fmt::Display>( +pub(crate) fn display_option<'a, T: fmt::Display>( prefix: &'a str, postfix: &'a str, option: &'a Option, @@ -2104,7 +1868,7 @@ fn display_option<'a, T: fmt::Display>( /// * `Some(inner)` => create display struct for `" {inner}"` /// * `_` => do nothing #[must_use] -fn display_option_spaced(option: &Option) -> impl fmt::Display + '_ { +pub(crate) fn display_option_spaced(option: &Option) -> impl fmt::Display + '_ { display_option(" ", "", option) } @@ -3199,6 +2963,26 @@ impl Spanned for RenameTableNameKind { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// Whether the syntax used for the trigger object (ROW or STATEMENT) is `FOR` or `FOR EACH`. +pub enum TriggerObjectKind { + /// The `FOR` syntax is used. + For(TriggerObject), + /// The `FOR EACH` syntax is used. + ForEach(TriggerObject), +} + +impl Display for TriggerObjectKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TriggerObjectKind::For(obj) => write!(f, "FOR {obj}"), + TriggerObjectKind::ForEach(obj) => write!(f, "FOR EACH {obj}"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -3220,6 +3004,23 @@ pub struct CreateTrigger { /// /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver16#arguments) pub or_alter: bool, + /// True if this is a temporary trigger. + /// + /// Examples: + /// + /// ```sql + /// CREATE TEMP TRIGGER trigger_name + /// ``` + /// + /// or + /// + /// ```sql + /// CREATE TEMPORARY TRIGGER trigger_name; + /// CREATE TEMP TRIGGER trigger_name; + /// ``` + /// + /// [SQLite](https://sqlite.org/lang_createtrigger.html#temp_triggers_on_non_temp_tables) + pub temporary: bool, /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. /// /// Example: @@ -3262,8 +3063,10 @@ pub struct CreateTrigger { /// FOR EACH ROW /// EXECUTE FUNCTION trigger_function(); /// ``` - pub period: TriggerPeriod, + pub period: Option, /// Whether the trigger period was specified before the target table name. + /// This does not refer to whether the period is BEFORE, AFTER, or INSTEAD OF, + /// but rather the position of the period clause in relation to the table name. /// /// ```sql /// -- period_before_table == true: Postgres, MySQL, and standard SQL @@ -3283,9 +3086,9 @@ pub struct CreateTrigger { pub referencing: Vec, /// This specifies whether the trigger function should be fired once for /// every row affected by the trigger event, or just once per SQL statement. - pub trigger_object: TriggerObject, - /// Whether to include the `EACH` term of the `FOR EACH`, as it is optional syntax. - pub include_each: bool, + /// This is optional in some SQL dialects, such as SQLite, and if not specified, in + /// those cases, the implied default is `FOR EACH ROW`. + pub trigger_object: Option, /// Triggering conditions pub condition: Option, /// Execute logic block @@ -3302,6 +3105,7 @@ impl Display for CreateTrigger { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let CreateTrigger { or_alter, + temporary, or_replace, is_constraint, name, @@ -3313,7 +3117,6 @@ impl Display for CreateTrigger { referencing, trigger_object, condition, - include_each, exec_body, statements_as, statements, @@ -3321,21 +3124,26 @@ impl Display for CreateTrigger { } = self; write!( f, - "CREATE {or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + "CREATE {temporary}{or_alter}{or_replace}{is_constraint}TRIGGER {name} ", + temporary = if *temporary { "TEMPORARY " } else { "" }, or_alter = if *or_alter { "OR ALTER " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, is_constraint = if *is_constraint { "CONSTRAINT " } else { "" }, )?; if *period_before_table { - write!(f, "{period}")?; + if let Some(p) = period { + write!(f, "{p} ")?; + } if !events.is_empty() { - write!(f, " {}", display_separated(events, " OR "))?; + write!(f, "{} ", display_separated(events, " OR "))?; } - write!(f, " ON {table_name}")?; - } else { write!(f, "ON {table_name}")?; - write!(f, " {period}")?; + } else { + write!(f, "ON {table_name} ")?; + if let Some(p) = period { + write!(f, "{p}")?; + } if !events.is_empty() { write!(f, " {}", display_separated(events, ", "))?; } @@ -3353,10 +3161,8 @@ impl Display for CreateTrigger { write!(f, " REFERENCING {}", display_separated(referencing, " "))?; } - if *include_each { - write!(f, " FOR EACH {trigger_object}")?; - } else if exec_body.is_some() { - write!(f, " FOR {trigger_object}")?; + if let Some(trigger_object) = trigger_object { + write!(f, " {trigger_object}")?; } if let Some(condition) = condition { write!(f, " WHEN {condition}")?; @@ -3416,3 +3222,394 @@ impl fmt::Display for DropTrigger { Ok(()) } } + +/// A `TRUNCATE` statement. +/// +/// ```sql +/// TRUNCATE TABLE table_names [PARTITION (partitions)] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE | RESTRICT] [ON CLUSTER cluster_name] +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Truncate { + /// Table names to truncate + pub table_names: Vec, + /// Optional partition specification + pub partitions: Option>, + /// TABLE - optional keyword + pub table: bool, + /// Postgres-specific option: [ RESTART IDENTITY | CONTINUE IDENTITY ] + pub identity: Option, + /// Postgres-specific option: [ CASCADE | RESTRICT ] + pub cascade: Option, + /// ClickHouse-specific option: [ ON CLUSTER cluster_name ] + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) + pub on_cluster: Option, +} + +impl fmt::Display for Truncate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let table = if self.table { "TABLE " } else { "" }; + + write!( + f, + "TRUNCATE {table}{table_names}", + table_names = display_comma_separated(&self.table_names) + )?; + + if let Some(identity) = &self.identity { + match identity { + super::TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?, + super::TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?, + } + } + if let Some(cascade) = &self.cascade { + match cascade { + super::CascadeOption::Cascade => write!(f, " CASCADE")?, + super::CascadeOption::Restrict => write!(f, " RESTRICT")?, + } + } + + if let Some(ref parts) = &self.partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + if let Some(on_cluster) = &self.on_cluster { + write!(f, " ON CLUSTER {on_cluster}")?; + } + Ok(()) + } +} + +impl Spanned for Truncate { + fn span(&self) -> Span { + Span::union_iter( + self.table_names.iter().map(|i| i.name.span()).chain( + self.partitions + .iter() + .flat_map(|i| i.iter().map(|k| k.span())), + ), + ) + } +} + +/// An `MSCK` statement. +/// +/// ```sql +/// MSCK [REPAIR] TABLE table_name [ADD|DROP|SYNC PARTITIONS] +/// ``` +/// MSCK (Hive) - MetaStore Check command +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Msck { + /// Table name to check + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// Whether to repair the table + pub repair: bool, + /// Partition action (ADD, DROP, or SYNC) + pub partition_action: Option, +} + +impl fmt::Display for Msck { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "MSCK {repair}TABLE {table}", + repair = if self.repair { "REPAIR " } else { "" }, + table = self.table_name + )?; + if let Some(pa) = &self.partition_action { + write!(f, " {pa}")?; + } + Ok(()) + } +} + +impl Spanned for Msck { + fn span(&self) -> Span { + self.table_name.span() + } +} + +/// CREATE VIEW 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 CreateView { + /// True if this is a `CREATE OR ALTER VIEW` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql) + pub or_alter: bool, + pub or_replace: bool, + pub materialized: bool, + /// Snowflake: SECURE view modifier + /// + pub secure: bool, + /// View name + pub name: ObjectName, + /// If `if_not_exists` is true, this flag is set to true if the view name comes before the `IF NOT EXISTS` clause. + /// Example: + /// ```sql + /// CREATE VIEW myview IF NOT EXISTS AS SELECT 1` + /// ``` + /// Otherwise, the flag is set to false if the view name comes after the clause + /// Example: + /// ```sql + /// CREATE VIEW IF NOT EXISTS myview AS SELECT 1` + /// ``` + pub name_before_not_exists: bool, + pub columns: Vec, + pub query: Box, + pub options: CreateTableOptions, + pub cluster_by: Vec, + /// Snowflake: Views can have comments in Snowflake. + /// + pub comment: Option, + /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause + pub with_no_schema_binding: bool, + /// if true, has SQLite `IF NOT EXISTS` clause + pub if_not_exists: bool, + /// if true, has SQLite `TEMP` or `TEMPORARY` clause + pub temporary: bool, + /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results + /// + pub to: Option, + /// MySQL: Optional parameters for the view algorithm, definer, and security context + pub params: Option, +} + +impl fmt::Display for CreateView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {or_alter}{or_replace}", + or_alter = if self.or_alter { "OR ALTER " } else { "" }, + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(ref params) = self.params { + params.fmt(f)?; + } + write!( + f, + "{secure}{materialized}{temporary}VIEW {if_not_and_name}{to}", + if_not_and_name = if self.if_not_exists { + if self.name_before_not_exists { + format!("{} IF NOT EXISTS", self.name) + } else { + format!("IF NOT EXISTS {}", self.name) + } + } else { + format!("{}", self.name) + }, + secure = if self.secure { "SECURE " } else { "" }, + materialized = if self.materialized { + "MATERIALIZED " + } else { + "" + }, + temporary = if self.temporary { "TEMPORARY " } else { "" }, + to = self + .to + .as_ref() + .map(|to| format!(" TO {to}")) + .unwrap_or_default() + )?; + if !self.columns.is_empty() { + write!(f, " ({})", display_comma_separated(&self.columns))?; + } + if matches!(self.options, CreateTableOptions::With(_)) { + write!(f, " {}", self.options)?; + } + if let Some(ref comment) = self.comment { + write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?; + } + if !self.cluster_by.is_empty() { + write!( + f, + " CLUSTER BY ({})", + display_comma_separated(&self.cluster_by) + )?; + } + if matches!(self.options, CreateTableOptions::Options(_)) { + write!(f, " {}", self.options)?; + } + f.write_str(" AS")?; + SpaceOrNewline.fmt(f)?; + self.query.fmt(f)?; + if self.with_no_schema_binding { + write!(f, " WITH NO SCHEMA BINDING")?; + } + Ok(()) + } +} + +/// CREATE EXTENSION statement +/// Note: this is a PostgreSQL-specific 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 CreateExtension { + pub name: Ident, + pub if_not_exists: bool, + pub cascade: bool, + pub schema: Option, + pub version: Option, +} + +impl fmt::Display for CreateExtension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE EXTENSION {if_not_exists}{name}", + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + name = self.name + )?; + if self.cascade || self.schema.is_some() || self.version.is_some() { + write!(f, " WITH")?; + + if let Some(name) = &self.schema { + write!(f, " SCHEMA {name}")?; + } + if let Some(version) = &self.version { + write!(f, " VERSION {version}")?; + } + if self.cascade { + write!(f, " CASCADE")?; + } + } + + Ok(()) + } +} + +impl Spanned for CreateExtension { + fn span(&self) -> Span { + Span::empty() + } +} + +/// DROP EXTENSION statement +/// Note: this is a PostgreSQL-specific statement +/// +/// # References +/// +/// PostgreSQL Documentation: +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DropExtension { + pub names: Vec, + pub if_exists: bool, + /// `CASCADE` or `RESTRICT` + pub cascade_or_restrict: Option, +} + +impl fmt::Display for DropExtension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DROP EXTENSION")?; + if self.if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", display_comma_separated(&self.names))?; + if let Some(cascade_or_restrict) = &self.cascade_or_restrict { + write!(f, " {cascade_or_restrict}")?; + } + Ok(()) + } +} + +impl Spanned for DropExtension { + fn span(&self) -> Span { + Span::empty() + } +} + +/// ALTER TABLE 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 AlterTable { + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + pub if_exists: bool, + pub only: bool, + pub operations: Vec, + pub location: Option, + /// ClickHouse dialect supports `ON CLUSTER` clause for ALTER TABLE + /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) + pub on_cluster: Option, + /// Snowflake "ICEBERG" clause for Iceberg tables + /// + pub iceberg: bool, + /// Token that represents the end of the statement (semicolon or EOF) + pub end_token: AttachedToken, +} + +impl fmt::Display for AlterTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.iceberg { + write!(f, "ALTER ICEBERG TABLE ")?; + } else { + write!(f, "ALTER TABLE ")?; + } + + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + if self.only { + write!(f, "ONLY ")?; + } + write!(f, "{} ", &self.name)?; + if let Some(cluster) = &self.on_cluster { + write!(f, "ON CLUSTER {cluster} ")?; + } + write!(f, "{}", display_comma_separated(&self.operations))?; + if let Some(loc) = &self.location { + write!(f, " {loc}")? + } + Ok(()) + } +} + +/// DROP FUNCTION 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 DropFunction { + pub if_exists: bool, + /// One or more functions to drop + pub func_desc: Vec, + /// `CASCADE` or `RESTRICT` + pub drop_behavior: Option, +} + +impl fmt::Display for DropFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "DROP FUNCTION{} {}", + if self.if_exists { " IF EXISTS" } else { "" }, + display_comma_separated(&self.func_desc), + )?; + if let Some(op) = &self.drop_behavior { + write!(f, " {op}")?; + } + Ok(()) + } +} + +impl Spanned for DropFunction { + fn span(&self) -> Span { + Span::empty() + } +} diff --git a/src/ast/dml.rs b/src/ast/dml.rs index e4d99bcfc..c0bfcb19f 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -29,7 +29,7 @@ use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; use super::{ display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, Query, SelectItem, - Setting, SqliteOnConflict, TableObject, TableWithJoins, + Setting, SqliteOnConflict, TableObject, TableWithJoins, UpdateTableFromKind, }; /// INSERT statement. @@ -240,3 +240,66 @@ impl Display for Delete { Ok(()) } } + +/// UPDATE 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 Update { + /// TABLE + pub table: TableWithJoins, + /// Column assignments + pub assignments: Vec, + /// Table which provide value to be set + pub from: Option, + /// WHERE + pub selection: Option, + /// RETURNING + pub returning: Option>, + /// SQLite-specific conflict resolution clause + pub or: Option, + /// LIMIT + pub limit: Option, +} + +impl Display for Update { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("UPDATE ")?; + if let Some(or) = &self.or { + or.fmt(f)?; + f.write_str(" ")?; + } + self.table.fmt(f)?; + if let Some(UpdateTableFromKind::BeforeSet(from)) = &self.from { + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; + } + if !self.assignments.is_empty() { + SpaceOrNewline.fmt(f)?; + f.write_str("SET")?; + indented_list(f, &self.assignments)?; + } + if let Some(UpdateTableFromKind::AfterSet(from)) = &self.from { + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; + } + if let Some(selection) = &self.selection { + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; + } + if let Some(returning) = &self.returning { + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; + } + if let Some(limit) = &self.limit { + SpaceOrNewline.fmt(f)?; + write!(f, "LIMIT {limit}")?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4c1743feb..176d36545 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -43,7 +43,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::{ - display_utils::{indented_list, SpaceOrNewline}, + display_utils::SpaceOrNewline, tokenizer::{Span, Token}, }; use crate::{ @@ -56,23 +56,24 @@ pub use self::data_type::{ ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo, }; pub use self::dcl::{ - AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, + AlterRoleOperation, CreateRole, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, - AlterSchema, AlterSchemaOperation, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, - AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, - AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, - ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, - CreateFunction, CreateIndex, CreateTable, CreateTrigger, Deduplicate, DeferrableInitial, - DropBehavior, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, - ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, - TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, - ViewColumnDef, + AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, + AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, + ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, + ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, + CreateIndex, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, + DropBehavior, DropExtension, DropFunction, DropTrigger, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, + NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, + ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; -pub use self::dml::{Delete, Insert}; +pub use self::dml::{Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, @@ -118,6 +119,11 @@ mod dcl; mod ddl; mod dml; pub mod helpers; +pub mod table_constraints; +pub use table_constraints::{ + CheckConstraint, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint, + PrimaryKeyConstraint, TableConstraint, UniqueConstraint, +}; mod operator; mod query; mod spans; @@ -152,14 +158,14 @@ where } } -pub fn display_separated<'a, T>(slice: &'a [T], sep: &'static str) -> DisplaySeparated<'a, T> +pub(crate) fn display_separated<'a, T>(slice: &'a [T], sep: &'static str) -> DisplaySeparated<'a, T> where T: fmt::Display, { DisplaySeparated { slice, sep } } -pub fn display_comma_separated(slice: &[T]) -> DisplaySeparated<'_, T> +pub(crate) fn display_comma_separated(slice: &[T]) -> DisplaySeparated<'_, T> where T: fmt::Display, { @@ -651,6 +657,31 @@ pub enum CastKind { DoubleColon, } +/// `MATCH` type for constraint references +/// +/// See: +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ConstraintReferenceMatchKind { + /// `MATCH FULL` + Full, + /// `MATCH PARTIAL` + Partial, + /// `MATCH SIMPLE` + Simple, +} + +impl fmt::Display for ConstraintReferenceMatchKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Full => write!(f, "MATCH FULL"), + Self::Partial => write!(f, "MATCH PARTIAL"), + Self::Simple => write!(f, "MATCH SIMPLE"), + } + } +} + /// `EXTRACT` syntax variants. /// /// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax @@ -3060,6 +3091,59 @@ impl Display for ExceptionWhen { } } +/// ANALYZE TABLE statement (Hive-specific) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Analyze { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + pub partitions: Option>, + pub for_columns: bool, + pub columns: Vec, + pub cache_metadata: bool, + pub noscan: bool, + pub compute_statistics: bool, + pub has_table_keyword: bool, +} + +impl fmt::Display for Analyze { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ANALYZE{}{table_name}", + if self.has_table_keyword { + " TABLE " + } else { + " " + }, + table_name = self.table_name + )?; + if let Some(ref parts) = self.partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + + if self.compute_statistics { + write!(f, " COMPUTE STATISTICS")?; + } + if self.noscan { + write!(f, " NOSCAN")?; + } + if self.cache_metadata { + write!(f, " CACHE METADATA")?; + } + if self.for_columns { + write!(f, " FOR COLUMNS")?; + if !self.columns.is_empty() { + write!(f, " {}", display_comma_separated(&self.columns))?; + } + } + Ok(()) + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3074,49 +3158,18 @@ pub enum Statement { /// ANALYZE /// ``` /// Analyze (Hive) - Analyze { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - partitions: Option>, - for_columns: bool, - columns: Vec, - cache_metadata: bool, - noscan: bool, - compute_statistics: bool, - has_table_keyword: bool, - }, + Analyze(Analyze), Set(Set), /// ```sql /// TRUNCATE /// ``` /// Truncate (Hive) - Truncate { - table_names: Vec, - partitions: Option>, - /// TABLE - optional keyword; - table: bool, - /// Postgres-specific option - /// [ RESTART IDENTITY | CONTINUE IDENTITY ] - identity: Option, - /// Postgres-specific option - /// [ CASCADE | RESTRICT ] - cascade: Option, - /// ClickHouse-specific option - /// [ ON CLUSTER cluster_name ] - /// - /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) - on_cluster: Option, - }, + Truncate(Truncate), /// ```sql /// MSCK /// ``` /// Msck (Hive) - Msck { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - repair: bool, - partition_action: Option, - }, + Msck(Msck), /// ```sql /// SELECT /// ``` @@ -3219,22 +3272,7 @@ pub enum Statement { /// ```sql /// UPDATE /// ``` - Update { - /// TABLE - table: TableWithJoins, - /// Column assignments - assignments: Vec, - /// Table which provide value to be set - from: Option, - /// WHERE - selection: Option, - /// RETURNING - returning: Option>, - /// SQLite-specific conflict resolution clause - or: Option, - /// LIMIT - limit: Option, - }, + Update(Update), /// ```sql /// DELETE /// ``` @@ -3242,48 +3280,7 @@ pub enum Statement { /// ```sql /// CREATE VIEW /// ``` - CreateView { - /// True if this is a `CREATE OR ALTER VIEW` statement - /// - /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql) - or_alter: bool, - or_replace: bool, - materialized: bool, - /// Snowflake: SECURE view modifier - /// - secure: bool, - /// View name - name: ObjectName, - /// If `if_not_exists` is true, this flag is set to true if the view name comes before the `IF NOT EXISTS` clause. - /// Example: - /// ```sql - /// CREATE VIEW myview IF NOT EXISTS AS SELECT 1` - /// ``` - /// Otherwise, the flag is set to false if the view name comes after the clause - /// Example: - /// ```sql - /// CREATE VIEW IF NOT EXISTS myview AS SELECT 1` - /// ``` - name_before_not_exists: bool, - columns: Vec, - query: Box, - options: CreateTableOptions, - cluster_by: Vec, - /// Snowflake: Views can have comments in Snowflake. - /// - comment: Option, - /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause - with_no_schema_binding: bool, - /// if true, has SQLite `IF NOT EXISTS` clause - if_not_exists: bool, - /// if true, has SQLite `TEMP` or `TEMPORARY` clause - temporary: bool, - /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results - /// - to: Option, - /// MySQL: Optional parameters for the view algorithm, definer, and security context - params: Option, - }, + CreateView(CreateView), /// ```sql /// CREATE TABLE /// ``` @@ -3307,28 +3304,7 @@ pub enum Statement { /// CREATE ROLE /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.html) - CreateRole { - names: Vec, - if_not_exists: bool, - // Postgres - login: Option, - inherit: Option, - bypassrls: Option, - password: Option, - superuser: Option, - create_db: Option, - create_role: Option, - replication: Option, - connection_limit: Option, - valid_until: Option, - in_role: Vec, - in_group: Vec, - role: Vec, - user: Vec, - admin: Vec, - // MSSQL - authorization_owner: Option, - }, + CreateRole(CreateRole), /// ```sql /// CREATE SECRET /// ``` @@ -3366,24 +3342,7 @@ pub enum Statement { /// ```sql /// ALTER TABLE /// ``` - AlterTable { - /// Table name - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - name: ObjectName, - if_exists: bool, - only: bool, - operations: Vec, - location: Option, - /// ClickHouse dialect supports `ON CLUSTER` clause for ALTER TABLE - /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` - /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) - on_cluster: Option, - /// Snowflake "ICEBERG" clause for Iceberg tables - /// - iceberg: bool, - /// Token that represents the end of the statement (semicolon or EOF) - end_token: AttachedToken, - }, + AlterTable(AlterTable), /// ```sql /// ALTER SCHEMA /// ``` @@ -3519,13 +3478,7 @@ pub enum Statement { /// ```sql /// DROP FUNCTION /// ``` - DropFunction { - if_exists: bool, - /// One or more function to drop - func_desc: Vec, - /// `CASCADE` or `RESTRICT` - drop_behavior: Option, - }, + DropFunction(DropFunction), /// ```sql /// DROP DOMAIN /// ``` @@ -3589,25 +3542,13 @@ pub enum Statement { /// ``` /// /// Note: this is a PostgreSQL-specific statement, - CreateExtension { - name: Ident, - if_not_exists: bool, - cascade: bool, - schema: Option, - version: Option, - }, + CreateExtension(CreateExtension), /// ```sql /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] - /// - /// Note: this is a PostgreSQL-specific statement. - /// https://www.postgresql.org/docs/current/sql-dropextension.html /// ``` - DropExtension { - names: Vec, - if_exists: bool, - /// `CASCADE` or `RESTRICT` - cascade_or_restrict: Option, - }, + /// Note: this is a PostgreSQL-specific statement. + /// + DropExtension(DropExtension), /// ```sql /// FETCH /// ``` @@ -4324,6 +4265,24 @@ pub enum Statement { Vacuum(VacuumStatement), } +impl From for Statement { + fn from(analyze: Analyze) -> Self { + Statement::Analyze(analyze) + } +} + +impl From for Statement { + fn from(truncate: ddl::Truncate) -> Self { + Statement::Truncate(truncate) + } +} + +impl From for Statement { + fn from(msck: ddl::Msck) -> Self { + Statement::Msck(msck) + } +} + /// ```sql /// {COPY | REVOKE} CURRENT GRANTS /// ``` @@ -4524,61 +4483,8 @@ impl fmt::Display for Statement { } write!(f, " {source}") } - Statement::Msck { - table_name, - repair, - partition_action, - } => { - write!( - f, - "MSCK {repair}TABLE {table}", - repair = if *repair { "REPAIR " } else { "" }, - table = table_name - )?; - if let Some(pa) = partition_action { - write!(f, " {pa}")?; - } - Ok(()) - } - Statement::Truncate { - table_names, - partitions, - table, - identity, - cascade, - on_cluster, - } => { - let table = if *table { "TABLE " } else { "" }; - - write!( - f, - "TRUNCATE {table}{table_names}", - table_names = display_comma_separated(table_names) - )?; - - if let Some(identity) = identity { - match identity { - TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?, - TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?, - } - } - if let Some(cascade) = cascade { - match cascade { - CascadeOption::Cascade => write!(f, " CASCADE")?, - CascadeOption::Restrict => write!(f, " RESTRICT")?, - } - } - - if let Some(ref parts) = partitions { - if !parts.is_empty() { - write!(f, " PARTITION ({})", display_comma_separated(parts))?; - } - } - if let Some(on_cluster) = on_cluster { - write!(f, " ON CLUSTER {on_cluster}")?; - } - Ok(()) - } + Statement::Msck(msck) => msck.fmt(f), + Statement::Truncate(truncate) => truncate.fmt(f), Statement::Case(stmt) => { write!(f, "{stmt}") } @@ -4633,44 +4539,7 @@ impl fmt::Display for Statement { )?; Ok(()) } - Statement::Analyze { - table_name, - partitions, - for_columns, - columns, - cache_metadata, - noscan, - compute_statistics, - has_table_keyword, - } => { - write!( - f, - "ANALYZE{}{table_name}", - if *has_table_keyword { " TABLE " } else { " " } - )?; - if let Some(ref parts) = partitions { - if !parts.is_empty() { - write!(f, " PARTITION ({})", display_comma_separated(parts))?; - } - } - - if *compute_statistics { - write!(f, " COMPUTE STATISTICS")?; - } - if *noscan { - write!(f, " NOSCAN")?; - } - if *cache_metadata { - write!(f, " CACHE METADATA")?; - } - if *for_columns { - write!(f, " FOR COLUMNS")?; - if !columns.is_empty() { - write!(f, " {}", display_comma_separated(columns))?; - } - } - Ok(()) - } + Statement::Analyze(analyze) => analyze.fmt(f), Statement::Insert(insert) => insert.fmt(f), Statement::Install { extension_name: name, @@ -4726,53 +4595,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Update { - table, - assignments, - from, - selection, - returning, - or, - limit, - } => { - f.write_str("UPDATE ")?; - if let Some(or) = or { - or.fmt(f)?; - f.write_str(" ")?; - } - table.fmt(f)?; - if let Some(UpdateTableFromKind::BeforeSet(from)) = from { - SpaceOrNewline.fmt(f)?; - f.write_str("FROM")?; - indented_list(f, from)?; - } - if !assignments.is_empty() { - SpaceOrNewline.fmt(f)?; - f.write_str("SET")?; - indented_list(f, assignments)?; - } - if let Some(UpdateTableFromKind::AfterSet(from)) = from { - SpaceOrNewline.fmt(f)?; - f.write_str("FROM")?; - indented_list(f, from)?; - } - if let Some(selection) = selection { - SpaceOrNewline.fmt(f)?; - f.write_str("WHERE")?; - SpaceOrNewline.fmt(f)?; - Indent(selection).fmt(f)?; - } - if let Some(returning) = returning { - SpaceOrNewline.fmt(f)?; - f.write_str("RETURNING")?; - indented_list(f, returning)?; - } - if let Some(limit) = limit { - SpaceOrNewline.fmt(f)?; - write!(f, "LIMIT {limit}")?; - } - Ok(()) - } + Statement::Update(update) => update.fmt(f), Statement::Delete(delete) => delete.fmt(f), Statement::Open(open) => open.fmt(f), Statement::Close { cursor } => { @@ -4928,80 +4751,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateView { - or_alter, - name, - or_replace, - columns, - query, - materialized, - secure, - options, - cluster_by, - comment, - with_no_schema_binding, - if_not_exists, - temporary, - to, - params, - name_before_not_exists, - } => { - write!( - f, - "CREATE {or_alter}{or_replace}", - or_alter = if *or_alter { "OR ALTER " } else { "" }, - or_replace = if *or_replace { "OR REPLACE " } else { "" }, - )?; - if let Some(params) = params { - params.fmt(f)?; - } - write!( - f, - "{secure}{materialized}{temporary}VIEW {if_not_and_name}{to}", - if_not_and_name = if *if_not_exists { - if *name_before_not_exists { - format!("{name} IF NOT EXISTS") - } else { - format!("IF NOT EXISTS {name}") - } - } else { - format!("{name}") - }, - secure = if *secure { "SECURE " } else { "" }, - materialized = if *materialized { "MATERIALIZED " } else { "" }, - temporary = if *temporary { "TEMPORARY " } else { "" }, - to = to - .as_ref() - .map(|to| format!(" TO {to}")) - .unwrap_or_default() - )?; - if !columns.is_empty() { - write!(f, " ({})", display_comma_separated(columns))?; - } - if matches!(options, CreateTableOptions::With(_)) { - write!(f, " {options}")?; - } - if let Some(comment) = comment { - write!( - f, - " COMMENT = '{}'", - value::escape_single_quote_string(comment) - )?; - } - if !cluster_by.is_empty() { - write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; - } - if matches!(options, CreateTableOptions::Options(_)) { - write!(f, " {options}")?; - } - f.write_str(" AS")?; - SpaceOrNewline.fmt(f)?; - query.fmt(f)?; - if *with_no_schema_binding { - write!(f, " WITH NO SCHEMA BINDING")?; - } - Ok(()) - } + Statement::CreateView(create_view) => create_view.fmt(f), Statement::CreateTable(create_table) => create_table.fmt(f), Statement::LoadData { local, @@ -5052,141 +4802,9 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateIndex(create_index) => create_index.fmt(f), - Statement::CreateExtension { - name, - if_not_exists, - cascade, - schema, - version, - } => { - write!( - f, - "CREATE EXTENSION {if_not_exists}{name}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } - )?; - if *cascade || schema.is_some() || version.is_some() { - write!(f, " WITH")?; - - if let Some(name) = schema { - write!(f, " SCHEMA {name}")?; - } - if let Some(version) = version { - write!(f, " VERSION {version}")?; - } - if *cascade { - write!(f, " CASCADE")?; - } - } - - Ok(()) - } - Statement::DropExtension { - names, - if_exists, - cascade_or_restrict, - } => { - write!(f, "DROP EXTENSION")?; - if *if_exists { - write!(f, " IF EXISTS")?; - } - write!(f, " {}", display_comma_separated(names))?; - if let Some(cascade_or_restrict) = cascade_or_restrict { - write!(f, " {cascade_or_restrict}")?; - } - Ok(()) - } - Statement::CreateRole { - names, - if_not_exists, - inherit, - login, - bypassrls, - password, - create_db, - create_role, - superuser, - replication, - connection_limit, - valid_until, - in_role, - in_group, - role, - user, - admin, - authorization_owner, - } => { - write!( - f, - "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - names = display_separated(names, ", "), - superuser = match *superuser { - Some(true) => " SUPERUSER", - Some(false) => " NOSUPERUSER", - None => "" - }, - create_db = match *create_db { - Some(true) => " CREATEDB", - Some(false) => " NOCREATEDB", - None => "" - }, - create_role = match *create_role { - Some(true) => " CREATEROLE", - Some(false) => " NOCREATEROLE", - None => "" - }, - inherit = match *inherit { - Some(true) => " INHERIT", - Some(false) => " NOINHERIT", - None => "" - }, - login = match *login { - Some(true) => " LOGIN", - Some(false) => " NOLOGIN", - None => "" - }, - replication = match *replication { - Some(true) => " REPLICATION", - Some(false) => " NOREPLICATION", - None => "" - }, - bypassrls = match *bypassrls { - Some(true) => " BYPASSRLS", - Some(false) => " NOBYPASSRLS", - None => "" - } - )?; - if let Some(limit) = connection_limit { - write!(f, " CONNECTION LIMIT {limit}")?; - } - match password { - Some(Password::Password(pass)) => write!(f, " PASSWORD {pass}"), - Some(Password::NullPassword) => write!(f, " PASSWORD NULL"), - None => Ok(()), - }?; - if let Some(until) = valid_until { - write!(f, " VALID UNTIL {until}")?; - } - if !in_role.is_empty() { - write!(f, " IN ROLE {}", display_comma_separated(in_role))?; - } - if !in_group.is_empty() { - write!(f, " IN GROUP {}", display_comma_separated(in_group))?; - } - if !role.is_empty() { - write!(f, " ROLE {}", display_comma_separated(role))?; - } - if !user.is_empty() { - write!(f, " USER {}", display_comma_separated(user))?; - } - if !admin.is_empty() { - write!(f, " ADMIN {}", display_comma_separated(admin))?; - } - if let Some(owner) = authorization_owner { - write!(f, " AUTHORIZATION {owner}")?; - } - Ok(()) - } + Statement::CreateExtension(create_extension) => write!(f, "{create_extension}"), + Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"), + Statement::CreateRole(create_role) => write!(f, "{create_role}"), Statement::CreateSecret { or_replace, temporary, @@ -5268,42 +4886,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateConnector(create_connector) => create_connector.fmt(f), - Statement::AlterTable { - name, - if_exists, - only, - operations, - location, - on_cluster, - iceberg, - end_token: _, - } => { - if *iceberg { - write!(f, "ALTER ICEBERG TABLE ")?; - } else { - write!(f, "ALTER TABLE ")?; - } - - if *if_exists { - write!(f, "IF EXISTS ")?; - } - if *only { - write!(f, "ONLY ")?; - } - write!(f, "{name} ")?; - if let Some(cluster) = on_cluster { - write!(f, "ON CLUSTER {cluster} ")?; - } - write!( - f, - "{operations}", - operations = display_comma_separated(operations) - )?; - if let Some(loc) = location { - write!(f, " {loc}")? - } - Ok(()) - } + Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") } @@ -5406,22 +4989,7 @@ impl fmt::Display for Statement { }; Ok(()) } - Statement::DropFunction { - if_exists, - func_desc, - drop_behavior, - } => { - write!( - f, - "DROP FUNCTION{} {}", - if *if_exists { " IF EXISTS" } else { "" }, - display_comma_separated(func_desc), - )?; - if let Some(op) = drop_behavior { - write!(f, " {op}")?; - } - Ok(()) - } + Statement::DropFunction(drop_function) => write!(f, "{drop_function}"), Statement::DropDomain(DropDomain { if_exists, name, @@ -8642,6 +8210,8 @@ pub enum CopyLegacyOption { Bzip2, /// CLEANPATH CleanPath, + /// COMPUPDATE [ PRESET | { ON | TRUE } | { OFF | FALSE } ] + CompUpdate { preset: bool, enabled: Option }, /// CSV ... Csv(Vec), /// DATEFORMAT \[ AS \] {'dateformat_string' | 'auto' } @@ -8682,8 +8252,12 @@ pub enum CopyLegacyOption { PartitionBy(UnloadPartitionBy), /// REGION \[ AS \] 'aws-region' } Region(String), + /// REMOVEQUOTES + RemoveQuotes, /// ROWGROUPSIZE \[ AS \] size \[ MB | GB \] RowGroupSize(FileSize), + /// STATUPDATE [ { ON | TRUE } | { OFF | FALSE } ] + StatUpdate(Option), /// TIMEFORMAT \[ AS \] {'timeformat_string' | 'auto' | 'epochsecs' | 'epochmillisecs' } TimeFormat(Option), /// TRUNCATECOLUMNS @@ -8710,6 +8284,22 @@ impl fmt::Display for CopyLegacyOption { BlankAsNull => write!(f, "BLANKSASNULL"), Bzip2 => write!(f, "BZIP2"), CleanPath => write!(f, "CLEANPATH"), + CompUpdate { preset, enabled } => { + write!(f, "COMPUPDATE")?; + if *preset { + write!(f, " PRESET")?; + } else if let Some(enabled) = enabled { + write!( + f, + "{}", + match enabled { + true => " TRUE", + false => " FALSE", + } + )?; + } + Ok(()) + } Csv(opts) => { write!(f, "CSV")?; if !opts.is_empty() { @@ -8756,7 +8346,19 @@ impl fmt::Display for CopyLegacyOption { Parquet => write!(f, "PARQUET"), PartitionBy(p) => write!(f, "{p}"), Region(region) => write!(f, "REGION '{}'", value::escape_single_quote_string(region)), + RemoveQuotes => write!(f, "REMOVEQUOTES"), RowGroupSize(file_size) => write!(f, "ROWGROUPSIZE {file_size}"), + StatUpdate(enabled) => { + write!( + f, + "STATUPDATE{}", + match enabled { + Some(true) => " TRUE", + Some(false) => " FALSE", + _ => "", + } + ) + } TimeFormat(fmt) => { write!(f, "TIMEFORMAT")?; if let Some(fmt) = fmt { @@ -10941,6 +10543,48 @@ impl From for Statement { } } +impl From for Statement { + fn from(u: Update) -> Self { + Self::Update(u) + } +} + +impl From for Statement { + fn from(cv: CreateView) -> Self { + Self::CreateView(cv) + } +} + +impl From for Statement { + fn from(cr: CreateRole) -> Self { + Self::CreateRole(cr) + } +} + +impl From for Statement { + fn from(at: AlterTable) -> Self { + Self::AlterTable(at) + } +} + +impl From for Statement { + fn from(df: DropFunction) -> Self { + Self::DropFunction(df) + } +} + +impl From for Statement { + fn from(ce: CreateExtension) -> Self { + Self::CreateExtension(ce) + } +} + +impl From for Statement { + fn from(de: DropExtension) -> Self { + Self::DropExtension(de) + } +} + impl From for Statement { fn from(c: CaseStatement) -> Self { Self::Case(c) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index d0bb05e3c..58c401f7d 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -33,35 +33,35 @@ use super::display_separated; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum UnaryOperator { + /// `@-@` Length or circumference (PostgreSQL/Redshift geometric operator) + /// see + AtDashAt, + /// Unary logical not operator: e.g. `! false` (Hive-specific) + BangNot, + /// Bitwise Not, e.g. `~9` + BitwiseNot, + /// `@@` Center (PostgreSQL/Redshift geometric operator) + /// see + DoubleAt, + /// `#` Number of points in path or polygon (PostgreSQL/Redshift geometric operator) + /// see + Hash, /// Plus, e.g. `+9` Plus, /// Minus, e.g. `-9` Minus, /// Not, e.g. `NOT(true)` Not, - /// Bitwise Not, e.g. `~9` (PostgreSQL-specific) - PGBitwiseNot, - /// Square root, e.g. `|/9` (PostgreSQL-specific) - PGSquareRoot, + /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) + PGAbs, /// Cube root, e.g. `||/27` (PostgreSQL-specific) PGCubeRoot, /// Factorial, e.g. `9!` (PostgreSQL-specific) PGPostfixFactorial, /// Factorial, e.g. `!!9` (PostgreSQL-specific) PGPrefixFactorial, - /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) - PGAbs, - /// Unary logical not operator: e.g. `! false` (Hive-specific) - BangNot, - /// `#` Number of points in path or polygon (PostgreSQL/Redshift geometric operator) - /// see - Hash, - /// `@-@` Length or circumference (PostgreSQL/Redshift geometric operator) - /// see - AtDashAt, - /// `@@` Center (PostgreSQL/Redshift geometric operator) - /// see - DoubleAt, + /// Square root, e.g. `|/9` (PostgreSQL-specific) + PGSquareRoot, /// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator) /// see QuestionDash, @@ -73,19 +73,19 @@ pub enum UnaryOperator { impl fmt::Display for UnaryOperator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match self { - UnaryOperator::Plus => "+", + UnaryOperator::AtDashAt => "@-@", + UnaryOperator::BangNot => "!", + UnaryOperator::BitwiseNot => "~", + UnaryOperator::DoubleAt => "@@", + UnaryOperator::Hash => "#", UnaryOperator::Minus => "-", UnaryOperator::Not => "NOT", - UnaryOperator::PGBitwiseNot => "~", - UnaryOperator::PGSquareRoot => "|/", + UnaryOperator::PGAbs => "@", UnaryOperator::PGCubeRoot => "||/", UnaryOperator::PGPostfixFactorial => "!", UnaryOperator::PGPrefixFactorial => "!!", - UnaryOperator::PGAbs => "@", - UnaryOperator::BangNot => "!", - UnaryOperator::Hash => "#", - UnaryOperator::AtDashAt => "@-@", - UnaryOperator::DoubleAt => "@@", + UnaryOperator::PGSquareRoot => "|/", + UnaryOperator::Plus => "+", UnaryOperator::QuestionDash => "?-", UnaryOperator::QuestionPipe => "?|", }) diff --git a/src/ast/query.rs b/src/ast/query.rs index 3b414cc6e..599b013ab 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2506,6 +2506,16 @@ pub struct OrderByExpr { pub with_fill: Option, } +impl From for OrderByExpr { + fn from(ident: Ident) -> Self { + OrderByExpr { + expr: Expr::Identifier(ident), + options: OrderByOptions::default(), + with_fill: None, + } + } +} + impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", self.expr, self.options)?; @@ -2574,7 +2584,7 @@ impl fmt::Display for InterpolateExpr { } } -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct OrderByOptions { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 4c53e55ce..7d2a00095 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -16,8 +16,8 @@ // under the License. use crate::ast::{ - ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, ColumnOptions, - ExportData, Owner, TypedString, + ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, AlterTable, + ColumnOptions, CreateView, ExportData, Owner, TypedString, }; use core::iter; @@ -25,23 +25,23 @@ use crate::tokenizer::Span; use super::{ dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, AttachedToken, - BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, - ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectBy, - ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, - Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, - Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, - FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, - Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, - LateralView, LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, - ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, - RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, - SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, - TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, - UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, - WildcardAdditionalOptions, With, WithFill, + AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget, + AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, + ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, + ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, + CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, + ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, + FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, + IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, + JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, + MatchRecognizePattern, Measure, NamedParenthesizedList, NamedWindowDefinition, ObjectName, + ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, + OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, RaiseStatement, + RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, + SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, + TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Value, Values, + ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -298,38 +298,9 @@ impl Spanned for Values { impl Spanned for Statement { fn span(&self) -> Span { match self { - Statement::Analyze { - table_name, - partitions, - for_columns: _, - columns, - cache_metadata: _, - noscan: _, - compute_statistics: _, - has_table_keyword: _, - } => union_spans( - core::iter::once(table_name.span()) - .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))) - .chain(columns.iter().map(|i| i.span)), - ), - Statement::Truncate { - table_names, - partitions, - table: _, - identity: _, - cascade: _, - on_cluster: _, - } => union_spans( - table_names - .iter() - .map(|i| i.name.span()) - .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ), - Statement::Msck { - table_name, - repair: _, - partition_action: _, - } => table_name.span(), + Statement::Analyze(analyze) => analyze.span(), + Statement::Truncate(truncate) => truncate.span(), + Statement::Msck(msck) => msck.span(), Statement::Query(query) => query.span(), Statement::Insert(insert) => insert.span(), Statement::Install { extension_name } => extension_name.span, @@ -375,47 +346,9 @@ impl Spanned for Statement { CloseCursor::All => Span::empty(), CloseCursor::Specific { name } => name.span, }, - Statement::Update { - table, - assignments, - from, - selection, - returning, - or: _, - limit: _, - } => union_spans( - core::iter::once(table.span()) - .chain(assignments.iter().map(|i| i.span())) - .chain(from.iter().map(|i| i.span())) - .chain(selection.iter().map(|i| i.span())) - .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ), + Statement::Update(update) => update.span(), Statement::Delete(delete) => delete.span(), - Statement::CreateView { - or_alter: _, - or_replace: _, - materialized: _, - secure: _, - name, - columns, - query, - options, - cluster_by, - comment: _, - with_no_schema_binding: _, - if_not_exists: _, - temporary: _, - to, - name_before_not_exists: _, - params: _, - } => union_spans( - core::iter::once(name.span()) - .chain(columns.iter().map(|i| i.span())) - .chain(core::iter::once(query.span())) - .chain(core::iter::once(options.span())) - .chain(cluster_by.iter().map(|i| i.span)) - .chain(to.iter().map(|i| i.span())), - ), + Statement::CreateView(create_view) => create_view.span(), Statement::CreateTable(create_table) => create_table.span(), Statement::CreateVirtualTable { name, @@ -428,25 +361,13 @@ impl Spanned for Statement { .chain(module_args.iter().map(|i| i.span)), ), Statement::CreateIndex(create_index) => create_index.span(), - Statement::CreateRole { .. } => Span::empty(), + Statement::CreateRole(create_role) => create_role.span(), + Statement::CreateExtension(create_extension) => create_extension.span(), + Statement::DropExtension(drop_extension) => drop_extension.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), - Statement::AlterTable { - name, - if_exists: _, - only: _, - operations, - location: _, - on_cluster, - iceberg: _, - end_token, - } => union_spans( - core::iter::once(name.span()) - .chain(operations.iter().map(|i| i.span())) - .chain(on_cluster.iter().map(|i| i.span)) - .chain(core::iter::once(end_token.0.span)), - ), + Statement::AlterTable(alter_table) => alter_table.span(), Statement::AlterIndex { name, operation } => name.span().union(&operation.span()), Statement::AlterView { name, @@ -467,13 +388,11 @@ impl Spanned for Statement { Statement::AttachDuckDBDatabase { .. } => Span::empty(), Statement::DetachDuckDBDatabase { .. } => Span::empty(), Statement::Drop { .. } => Span::empty(), - Statement::DropFunction { .. } => Span::empty(), + Statement::DropFunction(drop_function) => drop_function.span(), Statement::DropDomain { .. } => Span::empty(), Statement::DropProcedure { .. } => Span::empty(), Statement::DropSecret { .. } => Span::empty(), Statement::Declare { .. } => Span::empty(), - Statement::CreateExtension { .. } => Span::empty(), - Statement::DropExtension { .. } => Span::empty(), Statement::Fetch { .. } => Span::empty(), Statement::Flush { .. } => Span::empty(), Statement::Discard { .. } => Span::empty(), @@ -670,83 +589,12 @@ impl Spanned for ColumnOptionDef { impl Spanned for TableConstraint { fn span(&self) -> Span { match self { - TableConstraint::Unique { - name, - index_name, - index_type_display: _, - index_type: _, - columns, - index_options: _, - characteristics, - nulls_distinct: _, - } => union_spans( - name.iter() - .map(|i| i.span) - .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span())) - .chain(characteristics.iter().map(|i| i.span())), - ), - TableConstraint::PrimaryKey { - name, - index_name, - index_type: _, - columns, - index_options: _, - characteristics, - } => union_spans( - name.iter() - .map(|i| i.span) - .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span())) - .chain(characteristics.iter().map(|i| i.span())), - ), - TableConstraint::ForeignKey { - name, - columns, - index_name, - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => union_spans( - name.iter() - .map(|i| i.span) - .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) - .chain(core::iter::once(foreign_table.span())) - .chain(referred_columns.iter().map(|i| i.span)) - .chain(on_delete.iter().map(|i| i.span())) - .chain(on_update.iter().map(|i| i.span())) - .chain(characteristics.iter().map(|i| i.span())), - ), - TableConstraint::Check { - name, - expr, - enforced: _, - } => expr.span().union_opt(&name.as_ref().map(|i| i.span)), - TableConstraint::Index { - display_as_key: _, - name, - index_type: _, - columns, - index_options: _, - } => union_spans( - name.iter() - .map(|i| i.span) - .chain(columns.iter().map(|i| i.span())), - ), - TableConstraint::FulltextOrSpatial { - fulltext: _, - index_type_display: _, - opt_index_name, - columns, - } => union_spans( - opt_index_name - .iter() - .map(|i| i.span) - .chain(columns.iter().map(|i| i.span())), - ), + TableConstraint::Unique(constraint) => constraint.span(), + TableConstraint::PrimaryKey(constraint) => constraint.span(), + TableConstraint::ForeignKey(constraint) => constraint.span(), + TableConstraint::Check(constraint) => constraint.span(), + TableConstraint::Index(constraint) => constraint.span(), + TableConstraint::FulltextOrSpatial(constraint) => constraint.span(), } } } @@ -880,7 +728,8 @@ impl Spanned for RaiseStatementValue { /// - [ColumnOption::Null] /// - [ColumnOption::NotNull] /// - [ColumnOption::Comment] -/// - [ColumnOption::Unique]¨ +/// - [ColumnOption::PrimaryKey] +/// - [ColumnOption::Unique] /// - [ColumnOption::DialectSpecific] /// - [ColumnOption::Generated] impl Spanned for ColumnOption { @@ -892,21 +741,10 @@ impl Spanned for ColumnOption { ColumnOption::Materialized(expr) => expr.span(), ColumnOption::Ephemeral(expr) => expr.as_ref().map_or(Span::empty(), |e| e.span()), ColumnOption::Alias(expr) => expr.span(), - ColumnOption::Unique { .. } => Span::empty(), - ColumnOption::ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => union_spans( - core::iter::once(foreign_table.span()) - .chain(referred_columns.iter().map(|i| i.span)) - .chain(on_delete.iter().map(|i| i.span())) - .chain(on_update.iter().map(|i| i.span())) - .chain(characteristics.iter().map(|i| i.span())), - ), - ColumnOption::Check(expr) => expr.span(), + ColumnOption::PrimaryKey(constraint) => constraint.span(), + ColumnOption::Unique(constraint) => constraint.span(), + ColumnOption::Check(constraint) => constraint.span(), + ColumnOption::ForeignKey(constraint) => constraint.span(), ColumnOption::DialectSpecific(_) => Span::empty(), ColumnOption::CharacterSet(object_name) => object_name.span(), ColumnOption::Collation(object_name) => object_name.span(), @@ -944,6 +782,20 @@ impl Spanned for ConstraintCharacteristics { } } +impl Spanned for Analyze { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.table_name.span()) + .chain( + self.partitions + .iter() + .flat_map(|i| i.iter().map(|k| k.span())), + ) + .chain(self.columns.iter().map(|i| i.span)), + ) + } +} + /// # partial span /// /// Missing spans: @@ -1012,6 +864,29 @@ impl Spanned for Delete { } } +impl Spanned for Update { + fn span(&self) -> Span { + let Update { + table, + assignments, + from, + selection, + returning, + or: _, + limit, + } = self; + + union_spans( + core::iter::once(table.span()) + .chain(assignments.iter().map(|i| i.span())) + .chain(from.iter().map(|i| i.span())) + .chain(selection.iter().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(limit.iter().map(|i| i.span())), + ) + } +} + impl Spanned for FromTable { fn span(&self) -> Span { match self { @@ -2446,6 +2321,30 @@ impl Spanned for AlterSchema { } } +impl Spanned for CreateView { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(self.columns.iter().map(|i| i.span())) + .chain(core::iter::once(self.query.span())) + .chain(core::iter::once(self.options.span())) + .chain(self.cluster_by.iter().map(|i| i.span)) + .chain(self.to.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for AlterTable { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(self.operations.iter().map(|i| i.span())) + .chain(self.on_cluster.iter().map(|i| i.span)) + .chain(core::iter::once(self.end_token.0.span)), + ) + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs new file mode 100644 index 000000000..ddf0c1253 --- /dev/null +++ b/src/ast/table_constraints.rs @@ -0,0 +1,520 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! SQL Abstract Syntax Tree (AST) types for table constraints + +use crate::ast::{ + display_comma_separated, display_separated, ConstraintCharacteristics, + ConstraintReferenceMatchKind, Expr, Ident, IndexColumn, IndexOption, IndexType, + KeyOrIndexDisplay, NullsDistinctOption, ObjectName, ReferentialAction, +}; +use crate::tokenizer::Span; +use core::fmt; + +#[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}; + +/// A table-level constraint, specified in a `CREATE TABLE` or an +/// `ALTER TABLE ADD ` 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 TableConstraint { + /// 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(UniqueConstraint), + /// 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(PrimaryKeyConstraint), + /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () + /// REFERENCES () + /// { [ON DELETE ] [ON UPDATE ] | + /// [ON UPDATE ] [ON DELETE ] + /// }`). + ForeignKey(ForeignKeyConstraint), + /// `[ CONSTRAINT ] CHECK () [[NOT] ENFORCED]` + Check(CheckConstraint), + /// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage + /// is restricted to MySQL, as no other dialects that support this syntax were found. + /// + /// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html + Index(IndexConstraint), + /// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition is exactly the same, + /// and MySQL displays both the same way, it is part of this definition as well. + /// + /// Supported syntax: + /// + /// ```markdown + /// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) + /// + /// key_part: col_name + /// ``` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html + /// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html + FulltextOrSpatial(FullTextOrSpatialConstraint), +} + +impl From for TableConstraint { + fn from(constraint: UniqueConstraint) -> Self { + TableConstraint::Unique(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: PrimaryKeyConstraint) -> Self { + TableConstraint::PrimaryKey(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: ForeignKeyConstraint) -> Self { + TableConstraint::ForeignKey(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: CheckConstraint) -> Self { + TableConstraint::Check(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: IndexConstraint) -> Self { + TableConstraint::Index(constraint) + } +} + +impl From for TableConstraint { + fn from(constraint: FullTextOrSpatialConstraint) -> Self { + TableConstraint::FulltextOrSpatial(constraint) + } +} + +impl fmt::Display for TableConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TableConstraint::Unique(constraint) => constraint.fmt(f), + TableConstraint::PrimaryKey(constraint) => constraint.fmt(f), + TableConstraint::ForeignKey(constraint) => constraint.fmt(f), + TableConstraint::Check(constraint) => constraint.fmt(f), + TableConstraint::Index(constraint) => constraint.fmt(f), + TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CheckConstraint { + pub name: Option, + pub expr: Box, + /// MySQL-specific syntax + /// + pub enforced: Option, +} + +impl fmt::Display for CheckConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::display_constraint_name; + write!( + f, + "{}CHECK ({})", + display_constraint_name(&self.name), + self.expr + )?; + if let Some(b) = self.enforced { + write!(f, " {}", if b { "ENFORCED" } else { "NOT ENFORCED" }) + } else { + Ok(()) + } + } +} + +impl crate::ast::Spanned for CheckConstraint { + fn span(&self) -> Span { + self.expr + .span() + .union_opt(&self.name.as_ref().map(|i| i.span)) + } +} + +/// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () +/// REFERENCES () [ MATCH { FULL | PARTIAL | SIMPLE } ] +/// { [ON DELETE ] [ON UPDATE ] | +/// [ON UPDATE ] [ON DELETE ] +/// }`). +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ForeignKeyConstraint { + pub name: Option, + /// MySQL-specific field + /// + pub index_name: Option, + pub columns: Vec, + pub foreign_table: ObjectName, + pub referred_columns: Vec, + pub on_delete: Option, + pub on_update: Option, + pub match_kind: Option, + pub characteristics: Option, +} + +impl fmt::Display for ForeignKeyConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::{display_constraint_name, display_option_spaced}; + write!( + f, + "{}FOREIGN KEY{} ({}) REFERENCES {}", + display_constraint_name(&self.name), + display_option_spaced(&self.index_name), + display_comma_separated(&self.columns), + self.foreign_table, + )?; + if !self.referred_columns.is_empty() { + write!(f, "({})", display_comma_separated(&self.referred_columns))?; + } + if let Some(match_kind) = &self.match_kind { + write!(f, " {match_kind}")?; + } + if let Some(action) = &self.on_delete { + write!(f, " ON DELETE {action}")?; + } + if let Some(action) = &self.on_update { + write!(f, " ON UPDATE {action}")?; + } + if let Some(characteristics) = &self.characteristics { + write!(f, " {characteristics}")?; + } + Ok(()) + } +} + +impl crate::ast::Spanned for ForeignKeyConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.index_name.iter().map(|i| i.span)) + .chain(self.columns.iter().map(|i| i.span)) + .chain(core::iter::once(self.foreign_table.span())) + .chain(self.referred_columns.iter().map(|i| i.span)) + .chain(self.on_delete.iter().map(|i| i.span())) + .chain(self.on_update.iter().map(|i| i.span())) + .chain(self.characteristics.iter().map(|i| i.span())), + ) + } +} + +/// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition is exactly the same, +/// and MySQL displays both the same way, it is part of this definition as well. +/// +/// Supported syntax: +/// +/// ```markdown +/// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) +/// +/// key_part: col_name +/// ``` +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html +/// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct FullTextOrSpatialConstraint { + /// Whether this is a `FULLTEXT` (true) or `SPATIAL` (false) definition. + pub fulltext: bool, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + pub index_type_display: KeyOrIndexDisplay, + /// Optional index name. + pub opt_index_name: Option, + /// Referred column identifier list. + pub columns: Vec, +} + +impl fmt::Display for FullTextOrSpatialConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.fulltext { + write!(f, "FULLTEXT")?; + } else { + write!(f, "SPATIAL")?; + } + + write!(f, "{:>}", self.index_type_display)?; + + if let Some(name) = &self.opt_index_name { + write!(f, " {name}")?; + } + + write!(f, " ({})", display_comma_separated(&self.columns))?; + + Ok(()) + } +} + +impl crate::ast::Spanned for FullTextOrSpatialConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.opt_index_name + .iter() + .map(|i| i.span) + .chain(self.columns.iter().map(|i| i.span())), + ) + } +} + +/// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage +/// is restricted to MySQL, as no other dialects that support this syntax were found. +/// +/// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IndexConstraint { + /// Whether this index starts with KEY (true) or INDEX (false), to maintain the same syntax. + pub display_as_key: bool, + /// Index name. + pub name: Option, + /// Optional [index type][1]. + /// + /// [1]: IndexType + pub index_type: Option, + /// Referred column identifier list. + pub columns: Vec, + /// Optional index options such as `USING`; see [`IndexOption`]. + pub index_options: Vec, +} + +impl fmt::Display for IndexConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", if self.display_as_key { "KEY" } else { "INDEX" })?; + if let Some(name) = &self.name { + write!(f, " {name}")?; + } + if let Some(index_type) = &self.index_type { + write!(f, " USING {index_type}")?; + } + write!(f, " ({})", display_comma_separated(&self.columns))?; + if !self.index_options.is_empty() { + write!(f, " {}", display_comma_separated(&self.index_options))?; + } + Ok(()) + } +} + +impl crate::ast::Spanned for IndexConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.columns.iter().map(|i| i.span())), + ) + } +} + +/// 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 +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct PrimaryKeyConstraint { + /// Constraint name. + /// + /// Can be not the same as `index_name` + pub name: Option, + /// Index name + pub index_name: Option, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + pub index_type: Option, + /// Identifiers of the columns that form the primary key. + pub columns: Vec, + pub index_options: Vec, + pub characteristics: Option, +} + +impl fmt::Display for PrimaryKeyConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::{display_constraint_name, display_option, display_option_spaced}; + write!( + f, + "{}PRIMARY KEY{}{} ({})", + display_constraint_name(&self.name), + display_option_spaced(&self.index_name), + display_option(" USING ", "", &self.index_type), + display_comma_separated(&self.columns), + )?; + + if !self.index_options.is_empty() { + write!(f, " {}", display_separated(&self.index_options, " "))?; + } + + write!(f, "{}", display_option_spaced(&self.characteristics))?; + Ok(()) + } +} + +impl crate::ast::Spanned for PrimaryKeyConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.index_name.iter().map(|i| i.span)) + .chain(self.columns.iter().map(|i| i.span())) + .chain(self.characteristics.iter().map(|i| i.span())), + ) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct UniqueConstraint { + /// Constraint name. + /// + /// Can be not the same as `index_name` + pub name: Option, + /// Index name + pub index_name: Option, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + pub index_type_display: KeyOrIndexDisplay, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + pub index_type: Option, + /// Identifiers of the columns that are unique. + pub columns: Vec, + pub index_options: Vec, + pub characteristics: Option, + /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` + pub nulls_distinct: NullsDistinctOption, +} + +impl fmt::Display for UniqueConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::{display_constraint_name, display_option, display_option_spaced}; + write!( + f, + "{}UNIQUE{}{:>}{}{} ({})", + display_constraint_name(&self.name), + self.nulls_distinct, + self.index_type_display, + display_option_spaced(&self.index_name), + display_option(" USING ", "", &self.index_type), + display_comma_separated(&self.columns), + )?; + + if !self.index_options.is_empty() { + write!(f, " {}", display_separated(&self.index_options, " "))?; + } + + write!(f, "{}", display_option_spaced(&self.characteristics))?; + Ok(()) + } +} + +impl crate::ast::Spanned for UniqueConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.index_name.iter().map(|i| i.span)) + .chain(self.columns.iter().map(|i| i.span())) + .chain(self.characteristics.iter().map(|i| i.span())), + ) + } +} diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 7840f0e14..328f925f7 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -182,6 +182,10 @@ visit_noop!(bigdecimal::BigDecimal); /// ``` pub trait Visitor { /// Type returned when the recursion returns early. + /// + /// Important note: The `Break` type should be kept as small as possible to prevent + /// stack overflow during recursion. If you need to return an error, consider + /// boxing it with `Box` to minimize stack usage. type Break; /// Invoked for any queries that appear in the AST before visiting children @@ -290,6 +294,10 @@ pub trait Visitor { /// ``` pub trait VisitorMut { /// Type returned when the recursion returns early. + /// + /// Important note: The `Break` type should be kept as small as possible to prevent + /// stack overflow during recursion. If you need to return an error, consider + /// boxing it with `Box` to minimize stack usage. type Break; /// Invoked for any queries that appear in the AST before visiting children diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 4bb8c8d51..c5d5f9740 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -69,4 +69,9 @@ impl Dialect for DatabricksDialect { fn supports_nested_comments(&self) -> bool { true } + + /// See + fn supports_group_by_with_modifier(&self) -> bool { + true + } } diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 4fcc0e4b6..e1902b389 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -18,7 +18,7 @@ use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::{ BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, CreateTrigger, - GranteesType, IfStatement, Statement, TriggerObject, + GranteesType, IfStatement, Statement, }; use crate::dialect::Dialect; use crate::keywords::{self, Keyword}; @@ -254,17 +254,17 @@ impl MsSqlDialect { Ok(CreateTrigger { or_alter, + temporary: false, or_replace: false, is_constraint: false, name, - period, + period: Some(period), period_before_table: false, events, table_name, referenced_table_name: None, referencing: Vec::new(), - trigger_object: TriggerObject::Statement, - include_each: false, + trigger_object: None, condition: None, exec_body: None, statements_as: true, diff --git a/src/keywords.rs b/src/keywords.rs index 3c9855222..319c57827 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -215,6 +215,7 @@ define_keywords!( COMMITTED, COMPATIBLE, COMPRESSION, + COMPUPDATE, COMPUTE, CONCURRENTLY, CONDITION, @@ -713,6 +714,7 @@ define_keywords!( PARAMETER, PARQUET, PART, + PARTIAL, PARTITION, PARTITIONED, PARTITIONS, @@ -748,6 +750,7 @@ define_keywords!( PRECISION, PREPARE, PRESERVE, + PRESET, PREWHERE, PRIMARY, PRINT, @@ -800,6 +803,7 @@ define_keywords!( RELEASES, REMOTE, REMOVE, + REMOVEQUOTES, RENAME, REORG, REPAIR, @@ -885,6 +889,7 @@ define_keywords!( SHOW, SIGNED, SIMILAR, + SIMPLE, SKIP, SLOW, SMALLINT, @@ -913,6 +918,7 @@ define_keywords!( STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, + STATUPDATE, STATUS, STDDEV_POP, STDDEV_SAMP, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d97fa1bd0..9a01e510b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -982,11 +982,12 @@ impl<'a> Parser<'a> { Ok(pa) })? .unwrap_or_default(); - Ok(Statement::Msck { + Ok(Msck { repair, table_name, partition_action, - }) + } + .into()) } pub fn parse_truncate(&mut self) -> Result { @@ -1024,14 +1025,15 @@ impl<'a> Parser<'a> { let on_cluster = self.parse_optional_on_cluster()?; - Ok(Statement::Truncate { + Ok(Truncate { table_names, partitions, table, identity, cascade, on_cluster, - }) + } + .into()) } fn parse_cascade_option(&mut self) -> Option { @@ -1167,7 +1169,7 @@ impl<'a> Parser<'a> { } } - Ok(Statement::Analyze { + Ok(Analyze { has_table_keyword, table_name, for_columns, @@ -1176,7 +1178,8 @@ impl<'a> Parser<'a> { cache_metadata, noscan, compute_statistics, - }) + } + .into()) } /// Parse a new expression including wildcard & qualified wildcard. @@ -1630,7 +1633,6 @@ impl<'a> Parser<'a> { | tok @ Token::PGSquareRoot | tok @ Token::PGCubeRoot | tok @ Token::AtSign - | tok @ Token::Tilde if dialect_is!(dialect is PostgreSqlDialect) => { let op = match tok { @@ -1638,7 +1640,6 @@ impl<'a> Parser<'a> { Token::PGSquareRoot => UnaryOperator::PGSquareRoot, Token::PGCubeRoot => UnaryOperator::PGCubeRoot, Token::AtSign => UnaryOperator::PGAbs, - Token::Tilde => UnaryOperator::PGBitwiseNot, _ => unreachable!(), }; Ok(Expr::UnaryOp { @@ -1648,6 +1649,10 @@ impl<'a> Parser<'a> { ), }) } + Token::Tilde => Ok(Expr::UnaryOp { + op: UnaryOperator::BitwiseNot, + expr: Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?), + }), tok @ Token::Sharp | tok @ Token::AtDashAt | tok @ Token::AtAt @@ -4750,9 +4755,9 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::DOMAIN) { self.parse_create_domain() } else if self.parse_keyword(Keyword::TRIGGER) { - self.parse_create_trigger(or_alter, or_replace, false) + self.parse_create_trigger(temporary, or_alter, or_replace, false) } else if self.parse_keywords(&[Keyword::CONSTRAINT, Keyword::TRIGGER]) { - self.parse_create_trigger(or_alter, or_replace, true) + self.parse_create_trigger(temporary, or_alter, or_replace, true) } else if self.parse_keyword(Keyword::MACRO) { self.parse_create_macro(or_replace, temporary) } else if self.parse_keyword(Keyword::SECRET) { @@ -5548,7 +5553,8 @@ impl<'a> Parser<'a> { /// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] /// ``` pub fn parse_drop_trigger(&mut self) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) + { self.prev_token(); return self.expected("an object type after DROP", self.peek_token()); } @@ -5576,17 +5582,19 @@ impl<'a> Parser<'a> { pub fn parse_create_trigger( &mut self, + temporary: bool, or_alter: bool, or_replace: bool, is_constraint: bool, ) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect | MsSqlDialect) { + if !dialect_of!(self is PostgreSqlDialect | SQLiteDialect | GenericDialect | MySqlDialect | MsSqlDialect) + { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); } let name = self.parse_object_name(false)?; - let period = self.parse_trigger_period()?; + let period = self.maybe_parse(|parser| parser.parse_trigger_period())?; let events = self.parse_keyword_separated(Keyword::OR, Parser::parse_trigger_event)?; self.expect_keyword_is(Keyword::ON)?; @@ -5607,14 +5615,25 @@ impl<'a> Parser<'a> { } } - self.expect_keyword_is(Keyword::FOR)?; - let include_each = self.parse_keyword(Keyword::EACH); - let trigger_object = - match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { - Keyword::ROW => TriggerObject::Row, - Keyword::STATEMENT => TriggerObject::Statement, - _ => unreachable!(), - }; + let trigger_object = if self.parse_keyword(Keyword::FOR) { + let include_each = self.parse_keyword(Keyword::EACH); + let trigger_object = + match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { + Keyword::ROW => TriggerObject::Row, + Keyword::STATEMENT => TriggerObject::Statement, + _ => unreachable!(), + }; + + Some(if include_each { + TriggerObjectKind::ForEach(trigger_object) + } else { + TriggerObjectKind::For(trigger_object) + }) + } else { + let _ = self.parse_keyword(Keyword::FOR); + + None + }; let condition = self .parse_keyword(Keyword::WHEN) @@ -5629,8 +5648,9 @@ impl<'a> Parser<'a> { statements = Some(self.parse_conditional_statements(&[Keyword::END])?); } - Ok(Statement::CreateTrigger(CreateTrigger { + Ok(CreateTrigger { or_alter, + temporary, or_replace, is_constraint, name, @@ -5641,13 +5661,13 @@ impl<'a> Parser<'a> { referenced_table_name, referencing, trigger_object, - include_each, condition, exec_body, statements_as: false, statements, characteristics, - })) + } + .into()) } pub fn parse_trigger_period(&mut self) -> Result { @@ -5926,7 +5946,7 @@ impl<'a> Parser<'a> { Keyword::BINDING, ]); - Ok(Statement::CreateView { + Ok(CreateView { or_alter, name, columns, @@ -5943,7 +5963,8 @@ impl<'a> Parser<'a> { to, params: create_view_params, name_before_not_exists, - }) + } + .into()) } /// Parse optional parameters for the `CREATE VIEW` statement supported by [MySQL]. @@ -6206,7 +6227,7 @@ impl<'a> Parser<'a> { }? } - Ok(Statement::CreateRole { + Ok(CreateRole { names, if_not_exists, login, @@ -6225,7 +6246,8 @@ impl<'a> Parser<'a> { user, admin, authorization_owner, - }) + } + .into()) } pub fn parse_owner(&mut self) -> Result { @@ -6503,11 +6525,11 @@ impl<'a> Parser<'a> { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let func_desc = self.parse_comma_separated(Parser::parse_function_desc)?; let drop_behavior = self.parse_optional_drop_behavior(); - Ok(Statement::DropFunction { + Ok(Statement::DropFunction(DropFunction { if_exists, func_desc, drop_behavior, - }) + })) } /// ```sql @@ -7175,13 +7197,14 @@ impl<'a> Parser<'a> { (None, None, false) }; - Ok(Statement::CreateExtension { + Ok(CreateExtension { name, if_not_exists, schema, version, cascade, - }) + } + .into()) } /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. @@ -7190,7 +7213,7 @@ impl<'a> Parser<'a> { let names = self.parse_comma_separated(|p| p.parse_identifier())?; let cascade_or_restrict = self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]); - Ok(Statement::DropExtension { + Ok(Statement::DropExtension(DropExtension { names, if_exists, cascade_or_restrict: cascade_or_restrict @@ -7200,7 +7223,7 @@ impl<'a> Parser<'a> { _ => self.expected("CASCADE or RESTRICT", self.peek_token()), }) .transpose()?, - }) + })) } //TODO: Implement parsing for Skewed @@ -7904,15 +7927,22 @@ impl<'a> Parser<'a> { }; let name = self.parse_identifier()?; let data_type = self.parse_data_type()?; + let default = if self.consume_token(&Token::Eq) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(ProcedureParam { name, data_type, mode, + default, }) } pub fn parse_column_def(&mut self) -> Result { - let name = self.parse_identifier()?; + let col_name = self.parse_identifier()?; let data_type = if self.is_column_type_sqlite_unspecified() { DataType::Unspecified } else { @@ -7937,7 +7967,7 @@ impl<'a> Parser<'a> { }; } Ok(ColumnDef { - name, + name: col_name, data_type, options, }) @@ -8022,25 +8052,46 @@ impl<'a> Parser<'a> { } } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) { let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(ColumnOption::Unique { - is_primary: true, - characteristics, - })) + Ok(Some( + PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics, + } + .into(), + )) } else if self.parse_keyword(Keyword::UNIQUE) { let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(ColumnOption::Unique { - is_primary: false, - characteristics, - })) + Ok(Some( + UniqueConstraint { + name: None, + index_name: None, + index_type_display: KeyOrIndexDisplay::None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics, + nulls_distinct: NullsDistinctOption::None, + } + .into(), + )) } else if self.parse_keyword(Keyword::REFERENCES) { let foreign_table = self.parse_object_name(false)?; // PostgreSQL allows omitting the column list and // uses the primary key column of the foreign table by default let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; + let mut match_kind = None; let mut on_delete = None; let mut on_update = None; loop { - if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) { + match_kind = Some(self.parse_match_kind()?); + } else if on_delete.is_none() + && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) + { on_delete = Some(self.parse_referential_action()?); } else if on_update.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) @@ -8052,19 +8103,33 @@ impl<'a> Parser<'a> { } let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(ColumnOption::ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - })) + Ok(Some( + ForeignKeyConstraint { + name: None, // Column-level constraints don't have names + index_name: None, // Not applicable for column-level constraints + columns: vec![], // Not applicable for column-level constraints + foreign_table, + referred_columns, + on_delete, + on_update, + match_kind, + characteristics, + } + .into(), + )) } else if self.parse_keyword(Keyword::CHECK) { self.expect_token(&Token::LParen)?; // since `CHECK` requires parentheses, we can parse the inner expression in ParserState::Normal let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?; self.expect_token(&Token::RParen)?; - Ok(Some(ColumnOption::Check(expr))) + Ok(Some( + CheckConstraint { + name: None, // Column-level check constraints don't have names + expr: Box::new(expr), + enforced: None, // Could be extended later to support MySQL ENFORCED/NOT ENFORCED + } + .into(), + )) } else if self.parse_keyword(Keyword::AUTO_INCREMENT) && dialect_of!(self is MySqlDialect | GenericDialect) { @@ -8332,6 +8397,18 @@ impl<'a> Parser<'a> { } } + pub fn parse_match_kind(&mut self) -> Result { + if self.parse_keyword(Keyword::FULL) { + Ok(ConstraintReferenceMatchKind::Full) + } else if self.parse_keyword(Keyword::PARTIAL) { + Ok(ConstraintReferenceMatchKind::Partial) + } else if self.parse_keyword(Keyword::SIMPLE) { + Ok(ConstraintReferenceMatchKind::Simple) + } else { + self.expected("one of FULL, PARTIAL or SIMPLE", self.peek_token()) + } + } + pub fn parse_constraint_characteristics( &mut self, ) -> Result, ParserError> { @@ -8398,16 +8475,19 @@ impl<'a> Parser<'a> { let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(TableConstraint::Unique { - name, - index_name, - index_type_display, - index_type, - columns, - index_options, - characteristics, - nulls_distinct, - })) + Ok(Some( + UniqueConstraint { + name, + index_name, + index_type_display, + index_type, + columns, + index_options, + characteristics, + nulls_distinct, + } + .into(), + )) } Token::Word(w) if w.keyword == Keyword::PRIMARY => { // after `PRIMARY` always stay `KEY` @@ -8420,14 +8500,17 @@ impl<'a> Parser<'a> { let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(TableConstraint::PrimaryKey { - name, - index_name, - index_type, - columns, - index_options, - characteristics, - })) + Ok(Some( + PrimaryKeyConstraint { + name, + index_name, + index_type, + columns, + index_options, + characteristics, + } + .into(), + )) } Token::Word(w) if w.keyword == Keyword::FOREIGN => { self.expect_keyword_is(Keyword::KEY)?; @@ -8436,10 +8519,15 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name(false)?; let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; + let mut match_kind = None; let mut on_delete = None; let mut on_update = None; loop { - if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) { + match_kind = Some(self.parse_match_kind()?); + } else if on_delete.is_none() + && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) + { on_delete = Some(self.parse_referential_action()?); } else if on_update.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) @@ -8452,16 +8540,20 @@ impl<'a> Parser<'a> { let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(TableConstraint::ForeignKey { - name, - index_name, - columns, - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - })) + Ok(Some( + ForeignKeyConstraint { + name, + index_name, + columns, + foreign_table, + referred_columns, + on_delete, + on_update, + match_kind, + characteristics, + } + .into(), + )) } Token::Word(w) if w.keyword == Keyword::CHECK => { self.expect_token(&Token::LParen)?; @@ -8476,11 +8568,14 @@ impl<'a> Parser<'a> { None }; - Ok(Some(TableConstraint::Check { - name, - expr, - enforced, - })) + Ok(Some( + CheckConstraint { + name, + expr, + enforced, + } + .into(), + )) } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) @@ -8498,13 +8593,16 @@ impl<'a> Parser<'a> { let columns = self.parse_parenthesized_index_column_list()?; let index_options = self.parse_index_options()?; - Ok(Some(TableConstraint::Index { - display_as_key, - name, - index_type, - columns, - index_options, - })) + Ok(Some( + IndexConstraint { + display_as_key, + name, + index_type, + columns, + index_options, + } + .into(), + )) } Token::Word(w) if (w.keyword == Keyword::FULLTEXT || w.keyword == Keyword::SPATIAL) @@ -8528,12 +8626,15 @@ impl<'a> Parser<'a> { let columns = self.parse_parenthesized_index_column_list()?; - Ok(Some(TableConstraint::FulltextOrSpatial { - fulltext, - index_type_display, - opt_index_name, - columns, - })) + Ok(Some( + FullTextOrSpatialConstraint { + fulltext, + index_type_display, + opt_index_name, + columns, + } + .into(), + )) } _ => { if name.is_some() { @@ -9353,7 +9454,7 @@ impl<'a> Parser<'a> { self.get_current_token().clone() }; - Ok(Statement::AlterTable { + Ok(AlterTable { name: table_name, if_exists, only, @@ -9362,7 +9463,8 @@ impl<'a> Parser<'a> { on_cluster, iceberg, end_token: AttachedToken(end_token), - }) + } + .into()) } pub fn parse_alter_view(&mut self) -> Result { @@ -9641,6 +9743,7 @@ impl<'a> Parser<'a> { Keyword::BLANKSASNULL, Keyword::BZIP2, Keyword::CLEANPATH, + Keyword::COMPUPDATE, Keyword::CSV, Keyword::DATEFORMAT, Keyword::DELIMITER, @@ -9661,7 +9764,9 @@ impl<'a> Parser<'a> { Keyword::PARQUET, Keyword::PARTITION, Keyword::REGION, + Keyword::REMOVEQUOTES, Keyword::ROWGROUPSIZE, + Keyword::STATUPDATE, Keyword::TIMEFORMAT, Keyword::TRUNCATECOLUMNS, Keyword::ZSTD, @@ -9682,6 +9787,20 @@ impl<'a> Parser<'a> { Some(Keyword::BLANKSASNULL) => CopyLegacyOption::BlankAsNull, Some(Keyword::BZIP2) => CopyLegacyOption::Bzip2, Some(Keyword::CLEANPATH) => CopyLegacyOption::CleanPath, + Some(Keyword::COMPUPDATE) => { + let preset = self.parse_keyword(Keyword::PRESET); + let enabled = match self.parse_one_of_keywords(&[ + Keyword::TRUE, + Keyword::FALSE, + Keyword::ON, + Keyword::OFF, + ]) { + Some(Keyword::TRUE) | Some(Keyword::ON) => Some(true), + Some(Keyword::FALSE) | Some(Keyword::OFF) => Some(false), + _ => None, + }; + CopyLegacyOption::CompUpdate { preset, enabled } + } Some(Keyword::CSV) => CopyLegacyOption::Csv({ let mut opts = vec![]; while let Some(opt) = @@ -9770,11 +9889,25 @@ impl<'a> Parser<'a> { let region = self.parse_literal_string()?; CopyLegacyOption::Region(region) } + Some(Keyword::REMOVEQUOTES) => CopyLegacyOption::RemoveQuotes, Some(Keyword::ROWGROUPSIZE) => { let _ = self.parse_keyword(Keyword::AS); let file_size = self.parse_file_size()?; CopyLegacyOption::RowGroupSize(file_size) } + Some(Keyword::STATUPDATE) => { + let enabled = match self.parse_one_of_keywords(&[ + Keyword::TRUE, + Keyword::FALSE, + Keyword::ON, + Keyword::OFF, + ]) { + Some(Keyword::TRUE) | Some(Keyword::ON) => Some(true), + Some(Keyword::FALSE) | Some(Keyword::OFF) => Some(false), + _ => None, + }; + CopyLegacyOption::StatUpdate(enabled) + } Some(Keyword::TIMEFORMAT) => { let _ = self.parse_keyword(Keyword::AS); let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) { @@ -10411,7 +10544,9 @@ impl<'a> Parser<'a> { self.parse_optional_precision()?, TimezoneInfo::Tz, )), - Keyword::TIMESTAMP_NTZ => Ok(DataType::TimestampNtz), + Keyword::TIMESTAMP_NTZ => { + Ok(DataType::TimestampNtz(self.parse_optional_precision()?)) + } Keyword::TIME => { let precision = self.parse_optional_precision()?; let tz = if self.parse_keyword(Keyword::WITH) { @@ -15637,7 +15772,7 @@ impl<'a> Parser<'a> { } else { None }; - Ok(Statement::Update { + Ok(Update { table, assignments, from, @@ -15645,7 +15780,8 @@ impl<'a> Parser<'a> { returning, or, limit, - }) + } + .into()) } /// Parse a `var = expr` assignment, used in an UPDATE statement @@ -18136,85 +18272,91 @@ mod tests { test_parse_table_constraint!( dialect, "INDEX (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: false, name: None, index_type: None, columns: vec![mk_expected_col("c1")], index_options: vec![], } + .into() ); test_parse_table_constraint!( dialect, "KEY (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: true, name: None, index_type: None, columns: vec![mk_expected_col("c1")], index_options: vec![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX 'index' (c1, c2)", - TableConstraint::Index { + TableConstraint::Index(IndexConstraint { display_as_key: false, name: Some(Ident::with_quote('\'', "index")), index_type: None, columns: vec![mk_expected_col("c1"), mk_expected_col("c2")], index_options: vec![], - } + }) ); test_parse_table_constraint!( dialect, "INDEX USING BTREE (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: false, name: None, index_type: Some(IndexType::BTree), columns: vec![mk_expected_col("c1")], index_options: vec![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX USING HASH (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: false, name: None, index_type: Some(IndexType::Hash), columns: vec![mk_expected_col("c1")], index_options: vec![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX idx_name USING BTREE (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::BTree), columns: vec![mk_expected_col("c1")], index_options: vec![], } + .into() ); test_parse_table_constraint!( dialect, "INDEX idx_name USING HASH (c1)", - TableConstraint::Index { + IndexConstraint { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::Hash), columns: vec![mk_expected_col("c1")], index_options: vec![], } + .into() ); } diff --git a/src/test_utils.rs b/src/test_utils.rs index ab2cf89b2..a8c8afd59 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -343,21 +343,12 @@ pub fn expr_from_projection(item: &SelectItem) -> &Expr { pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTableOperation { match stmt { - Statement::AlterTable { - name, - if_exists, - only: is_only, - operations, - on_cluster: _, - location: _, - iceberg, - end_token: _, - } => { - assert_eq!(name.to_string(), expected_name); - assert!(!if_exists); - assert!(!is_only); - assert!(!iceberg); - only(operations) + Statement::AlterTable(alter_table) => { + assert_eq!(alter_table.name.to_string(), expected_name); + assert!(!alter_table.if_exists); + assert!(!alter_table.only); + assert!(!alter_table.iceberg); + only(alter_table.operations) } _ => panic!("Expected ALTER TABLE statement"), } @@ -469,37 +460,36 @@ pub fn index_column(stmt: Statement) -> Expr { } Statement::CreateTable(CreateTable { constraints, .. }) => { match constraints.first().unwrap() { - TableConstraint::Index { columns, .. } => { - columns.first().unwrap().column.expr.clone() + TableConstraint::Index(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() } - TableConstraint::Unique { columns, .. } => { - columns.first().unwrap().column.expr.clone() + TableConstraint::Unique(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() } - TableConstraint::PrimaryKey { columns, .. } => { - columns.first().unwrap().column.expr.clone() + TableConstraint::PrimaryKey(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() } - TableConstraint::FulltextOrSpatial { columns, .. } => { - columns.first().unwrap().column.expr.clone() + TableConstraint::FulltextOrSpatial(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() } _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), } } - Statement::AlterTable { operations, .. } => match operations.first().unwrap() { + Statement::AlterTable(alter_table) => match alter_table.operations.first().unwrap() { AlterTableOperation::AddConstraint { constraint, .. } => { match constraint { - TableConstraint::Index { columns, .. } => { - columns.first().unwrap().column.expr.clone() + TableConstraint::Index(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() } - TableConstraint::Unique { columns, .. } => { - columns.first().unwrap().column.expr.clone() + TableConstraint::Unique(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() } - TableConstraint::PrimaryKey { columns, .. } => { - columns.first().unwrap().column.expr.clone() + TableConstraint::PrimaryKey(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() + } + TableConstraint::FulltextOrSpatial(constraint) => { + constraint.columns.first().unwrap().column.expr.clone() } - TableConstraint::FulltextOrSpatial { - columns, - .. - } => columns.first().unwrap().column.expr.clone(), _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 4f0cfa3e8..03a0ac813 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -332,13 +332,13 @@ fn parse_create_view_with_options() { "AS SELECT column_1, column_2, column_3 FROM myproject.mydataset.mytable", ); match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, query, options, columns, .. - } => { + }) => { assert_eq!( name, ObjectName::from(vec![ @@ -401,7 +401,7 @@ fn parse_create_view_with_options() { fn parse_create_view_if_not_exists() { let sql = "CREATE VIEW IF NOT EXISTS mydataset.newview AS SELECT foo FROM bar"; match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, query, @@ -414,7 +414,7 @@ fn parse_create_view_if_not_exists() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("mydataset.newview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -435,12 +435,12 @@ fn parse_create_view_if_not_exists() { fn parse_create_view_with_unquoted_hyphen() { let sql = "CREATE VIEW IF NOT EXISTS my-pro-ject.mydataset.myview AS SELECT 1"; match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, query, if_not_exists, .. - } => { + }) => { assert_eq!("my-pro-ject.mydataset.myview", name.to_string()); assert_eq!("SELECT 1", query.to_string()); assert!(if_not_exists); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index bc1431f9c..44bfcda42 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -243,12 +243,10 @@ fn parse_alter_table_attach_and_detach_partition() { match clickhouse_and_generic() .verified_stmt(format!("ALTER TABLE t0 {operation} PARTITION part").as_str()) { - Statement::AlterTable { - name, operations, .. - } => { - pretty_assertions::assert_eq!("t0", name.to_string()); + Statement::AlterTable(alter_table) => { + pretty_assertions::assert_eq!("t0", alter_table.name.to_string()); pretty_assertions::assert_eq!( - operations[0], + alter_table.operations[0], if operation == &"ATTACH" { AlterTableOperation::AttachPartition { partition: Partition::Expr(Identifier(Ident::new("part"))), @@ -266,9 +264,9 @@ fn parse_alter_table_attach_and_detach_partition() { match clickhouse_and_generic() .verified_stmt(format!("ALTER TABLE t1 {operation} PART part").as_str()) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { pretty_assertions::assert_eq!("t1", name.to_string()); pretty_assertions::assert_eq!( operations[0], @@ -308,9 +306,9 @@ fn parse_alter_table_add_projection() { "ALTER TABLE t0 ADD PROJECTION IF NOT EXISTS my_name", " (SELECT a, b GROUP BY a ORDER BY b)", )) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( @@ -380,9 +378,9 @@ fn parse_alter_table_add_projection() { fn parse_alter_table_drop_projection() { match clickhouse_and_generic().verified_stmt("ALTER TABLE t0 DROP PROJECTION IF EXISTS my_name") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( @@ -413,9 +411,9 @@ fn parse_alter_table_clear_and_materialize_projection() { format!("ALTER TABLE t0 {keyword} PROJECTION IF EXISTS my_name IN PARTITION p0",) .as_str(), ) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( @@ -904,7 +902,7 @@ fn parse_create_table_with_variant_default_expressions() { #[test] fn parse_create_view_with_fields_data_types() { match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { - Statement::CreateView { name, columns, .. } => { + Statement::CreateView(CreateView { name, columns, .. }) => { assert_eq!(name, ObjectName::from(vec!["v".into()])); assert_eq!( columns, @@ -1518,7 +1516,7 @@ fn parse_freeze_and_unfreeze_partition() { Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), )); match clickhouse_and_generic().verified_stmt(&sql) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!(operations.len(), 1); let expected_operation = if operation_name == &"FREEZE" { AlterTableOperation::FreezePartition { @@ -1542,7 +1540,7 @@ fn parse_freeze_and_unfreeze_partition() { let sql = format!("ALTER TABLE t {operation_name} PARTITION '2024-08-14' WITH NAME 'hello'"); match clickhouse_and_generic().verified_stmt(&sql) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!(operations.len(), 1); let expected_partition = Partition::Expr(Expr::Value( Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 365d5469e..f1ba5df04 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -377,12 +377,12 @@ fn parse_insert_sqlite() { fn parse_update() { let sql = "UPDATE t SET a = 1, b = 2, c = 3 WHERE d"; match verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, selection, .. - } => { + }) => { assert_eq!(table.to_string(), "t".to_string()); assert_eq!( assignments, @@ -439,7 +439,7 @@ fn parse_update_set_from() { let stmt = dialects.verified_stmt(sql); assert_eq!( stmt, - Statement::Update { + Statement::Update(Update { table: TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], @@ -516,7 +516,7 @@ fn parse_update_set_from() { returning: None, or: None, limit: None - } + }) ); let sql = "UPDATE T SET a = b FROM U, (SELECT foo FROM V) AS W WHERE 1 = 1"; @@ -527,7 +527,7 @@ fn parse_update_set_from() { fn parse_update_with_table_alias() { let sql = "UPDATE users AS u SET u.username = 'new_user' WHERE u.username = 'old_user'"; match verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, from: _from, @@ -535,7 +535,7 @@ fn parse_update_with_table_alias() { returning, or: None, limit: None, - } => { + }) => { assert_eq!( TableWithJoins { relation: TableFactor::Table { @@ -591,7 +591,7 @@ fn parse_update_with_table_alias() { #[test] fn parse_update_or() { let expect_or_clause = |sql: &str, expected_action: SqliteOnConflict| match verified_stmt(sql) { - Statement::Update { or, .. } => assert_eq!(or, Some(expected_action)), + Statement::Update(Update { or, .. }) => assert_eq!(or, Some(expected_action)), other => unreachable!("Expected update with or, got {:?}", other), }; expect_or_clause( @@ -3763,10 +3763,14 @@ fn parse_create_table() { }, ColumnOptionDef { name: Some("pkey".into()), - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }, ColumnOptionDef { name: None, @@ -3774,14 +3778,24 @@ fn parse_create_table() { }, ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: false, - characteristics: None - }, + option: ColumnOption::Unique(UniqueConstraint { + name: None, + index_name: None, + index_type_display: KeyOrIndexDisplay::None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + nulls_distinct: NullsDistinctOption::None, + }), }, ColumnOptionDef { name: None, - option: ColumnOption::Check(verified_expr("constrained > 0")), + option: ColumnOption::Check(CheckConstraint { + name: None, + expr: Box::new(verified_expr("constrained > 0")), + enforced: None, + }), }, ], }, @@ -3790,13 +3804,17 @@ fn parse_create_table() { data_type: DataType::Int(None), options: vec![ColumnOptionDef { name: None, - option: ColumnOption::ForeignKey { + option: ColumnOption::ForeignKey(ForeignKeyConstraint { + name: None, + index_name: None, + columns: vec![], foreign_table: ObjectName::from(vec!["othertable".into()]), referred_columns: vec!["a".into(), "b".into()], on_delete: None, on_update: None, + match_kind: None, characteristics: None, - }, + }), }], }, ColumnDef { @@ -3804,13 +3822,17 @@ fn parse_create_table() { data_type: DataType::Int(None), options: vec![ColumnOptionDef { name: None, - option: ColumnOption::ForeignKey { + option: ColumnOption::ForeignKey(ForeignKeyConstraint { + name: None, + index_name: None, + columns: vec![], foreign_table: ObjectName::from(vec!["othertable2".into()]), referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::NoAction), + match_kind: None, characteristics: None, - }, + }), },], }, ] @@ -3818,7 +3840,7 @@ fn parse_create_table() { assert_eq!( constraints, vec![ - TableConstraint::ForeignKey { + ForeignKeyConstraint { name: Some("fkey".into()), index_name: None, columns: vec!["lat".into()], @@ -3826,9 +3848,11 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, + match_kind: None, characteristics: None, - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: Some("fkey2".into()), index_name: None, columns: vec!["lat".into()], @@ -3836,9 +3860,11 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), + match_kind: None, characteristics: None, - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, index_name: None, columns: vec!["lat".into()], @@ -3846,9 +3872,11 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), + match_kind: None, characteristics: None, - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, index_name: None, columns: vec!["lng".into()], @@ -3856,8 +3884,10 @@ fn parse_create_table() { referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), + match_kind: None, characteristics: None, - }, + } + .into(), ] ); assert_eq!(table_options, CreateTableOptions::None); @@ -3945,7 +3975,7 @@ fn parse_create_table_with_constraint_characteristics() { assert_eq!( constraints, vec![ - TableConstraint::ForeignKey { + ForeignKeyConstraint { name: Some("fkey".into()), index_name: None, columns: vec!["lat".into()], @@ -3953,13 +3983,15 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), enforced: None }), - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: Some("fkey2".into()), index_name: None, columns: vec!["lat".into()], @@ -3967,13 +3999,15 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Immediate), enforced: None, }), - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, index_name: None, columns: vec!["lat".into()], @@ -3981,13 +4015,15 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(false), initially: Some(DeferrableInitial::Deferred), enforced: Some(false), }), - }, - TableConstraint::ForeignKey { + } + .into(), + ForeignKeyConstraint { name: None, index_name: None, columns: vec!["lng".into()], @@ -3995,12 +4031,14 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(false), initially: Some(DeferrableInitial::Immediate), enforced: Some(true), }), - }, + } + .into(), ] ); assert_eq!(table_options, CreateTableOptions::None); @@ -4078,10 +4116,16 @@ fn parse_create_table_column_constraint_characteristics() { data_type: DataType::Int(None), options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: false, - characteristics: expected_value - } + option: ColumnOption::Unique(UniqueConstraint { + name: None, + index_name: None, + index_type_display: KeyOrIndexDisplay::None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: expected_value, + nulls_distinct: NullsDistinctOption::None, + }) }] }], "{message}" @@ -4838,9 +4882,9 @@ fn test_alter_table_with_on_cluster() { match all_dialects() .verified_stmt("ALTER TABLE t ON CLUSTER 'cluster' ADD CONSTRAINT bar PRIMARY KEY (baz)") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, on_cluster, .. - } => { + }) => { assert_eq!(name.to_string(), "t"); assert_eq!(on_cluster, Some(Ident::with_quote('\'', "cluster"))); } @@ -4850,9 +4894,9 @@ fn test_alter_table_with_on_cluster() { match all_dialects() .verified_stmt("ALTER TABLE t ON CLUSTER cluster_name ADD CONSTRAINT bar PRIMARY KEY (baz)") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, on_cluster, .. - } => { + }) => { assert_eq!(name.to_string(), "t"); assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); } @@ -7607,7 +7651,7 @@ fn parse_ctes() { // CTE in a view let sql = &format!("CREATE VIEW v AS {with}"); match verified_stmt(sql) { - Statement::CreateView { query, .. } => assert_ctes_in_select(&cte_sqls, &query), + Statement::CreateView(create_view) => assert_ctes_in_select(&cte_sqls, &create_view.query), _ => panic!("Expected: CREATE VIEW"), } // CTE in a CTE... @@ -8095,7 +8139,7 @@ fn parse_drop_database_if_exists() { fn parse_create_view() { let sql = "CREATE VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8112,7 +8156,7 @@ fn parse_create_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -8138,7 +8182,7 @@ fn parse_create_view() { fn parse_create_view_with_options() { let sql = "CREATE VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { options, .. } => { + Statement::CreateView(create_view) => { assert_eq!( CreateTableOptions::With(vec![ SqlOption::KeyValue { @@ -8152,7 +8196,7 @@ fn parse_create_view_with_options() { value: Expr::value(number("123")), }, ]), - options + create_view.options ); } _ => unreachable!(), @@ -8165,24 +8209,21 @@ fn parse_create_view_with_columns() { // TODO: why does this fail for ClickHouseDialect? (#1449) // match all_dialects().verified_stmt(sql) { match all_dialects_except(|d| d.is::()).verified_stmt(sql) { - Statement::CreateView { - or_alter, - name, - columns, - or_replace, - options, - query, - materialized, - cluster_by, - comment, - with_no_schema_binding: late_binding, - if_not_exists, - temporary, - to, - params, - name_before_not_exists: _, - secure: _, - } => { + Statement::CreateView(create_view) => { + let or_alter = create_view.or_alter; + let name = create_view.name; + let columns = create_view.columns; + let or_replace = create_view.or_replace; + let options = create_view.options; + let query = create_view.query; + let materialized = create_view.materialized; + let cluster_by = create_view.cluster_by; + let comment = create_view.comment; + let late_binding = create_view.with_no_schema_binding; + let if_not_exists = create_view.if_not_exists; + let temporary = create_view.temporary; + let to = create_view.to; + let params = create_view.params; assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!( @@ -8216,7 +8257,7 @@ fn parse_create_view_with_columns() { fn parse_create_view_temporary() { let sql = "CREATE TEMPORARY VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8233,7 +8274,7 @@ fn parse_create_view_temporary() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -8257,7 +8298,7 @@ fn parse_create_view_temporary() { fn parse_create_or_replace_view() { let sql = "CREATE OR REPLACE VIEW v AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8274,7 +8315,7 @@ fn parse_create_or_replace_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -8302,7 +8343,7 @@ fn parse_create_or_replace_materialized_view() { // https://docs.snowflake.com/en/sql-reference/sql/create-materialized-view.html let sql = "CREATE OR REPLACE MATERIALIZED VIEW v AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8319,7 +8360,7 @@ fn parse_create_or_replace_materialized_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -8343,7 +8384,7 @@ fn parse_create_or_replace_materialized_view() { fn parse_create_materialized_view() { let sql = "CREATE MATERIALIZED VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, or_replace, @@ -8360,7 +8401,7 @@ fn parse_create_materialized_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -8384,7 +8425,7 @@ fn parse_create_materialized_view() { fn parse_create_materialized_view_with_cluster_by() { let sql = "CREATE MATERIALIZED VIEW myschema.myview CLUSTER BY (foo) AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, or_replace, @@ -8401,7 +8442,7 @@ fn parse_create_materialized_view_with_cluster_by() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -9409,21 +9450,17 @@ fn parse_drop_index() { fn parse_create_role() { let sql = "CREATE ROLE consultant"; match verified_stmt(sql) { - Statement::CreateRole { names, .. } => { - assert_eq_vec(&["consultant"], &names); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["consultant"], &create_role.names); } _ => unreachable!(), } let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; match verified_stmt(sql) { - Statement::CreateRole { - names, - if_not_exists, - .. - } => { - assert_eq_vec(&["mysql_a", "mysql_b"], &names); - assert!(if_not_exists); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); + assert!(create_role.if_not_exists); } _ => unreachable!(), } @@ -13343,8 +13380,8 @@ fn test_extract_seconds_single_quote_err() { fn test_truncate_table_with_on_cluster() { let sql = "TRUNCATE TABLE t ON CLUSTER cluster_name"; match all_dialects().verified_stmt(sql) { - Statement::Truncate { on_cluster, .. } => { - assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); + Statement::Truncate(truncate) => { + assert_eq!(truncate.on_cluster, Some(Ident::new("cluster_name"))); } _ => panic!("Expected: TRUNCATE TABLE statement"), } @@ -16399,14 +16436,14 @@ fn parse_truncate_only() { ]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, identity: None, cascade: None, on_cluster: None, - }, + }), truncate ); } @@ -16497,7 +16534,8 @@ fn parse_create_procedure_with_parameter_modes() { span: fake_span, }, data_type: DataType::Integer(None), - mode: Some(ArgMode::In) + mode: Some(ArgMode::In), + default: None, }, ProcedureParam { name: Ident { @@ -16506,7 +16544,8 @@ fn parse_create_procedure_with_parameter_modes() { span: fake_span, }, data_type: DataType::Text, - mode: Some(ArgMode::Out) + mode: Some(ArgMode::Out), + default: None, }, ProcedureParam { name: Ident { @@ -16515,7 +16554,8 @@ fn parse_create_procedure_with_parameter_modes() { span: fake_span, }, data_type: DataType::Timestamp(None, TimezoneInfo::None), - mode: Some(ArgMode::InOut) + mode: Some(ArgMode::InOut), + default: None, }, ProcedureParam { name: Ident { @@ -16524,13 +16564,60 @@ fn parse_create_procedure_with_parameter_modes() { span: fake_span, }, data_type: DataType::Bool, - mode: None + mode: None, + default: None, }, ]) ); } _ => unreachable!(), } + + // parameters with default values + let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER = 1, OUT b TEXT = '2', INOUT c TIMESTAMP = NULL, d BOOL = 0) AS BEGIN SELECT 1; END"#; + match verified_stmt(sql) { + Statement::CreateProcedure { + or_alter, + name, + params, + .. + } => { + assert_eq!(or_alter, false); + assert_eq!(name.to_string(), "test_proc"); + assert_eq!( + params, + Some(vec![ + ProcedureParam { + name: Ident::new("a"), + data_type: DataType::Integer(None), + mode: Some(ArgMode::In), + default: Some(Expr::Value((number("1")).with_empty_span())), + }, + ProcedureParam { + name: Ident::new("b"), + data_type: DataType::Text, + mode: Some(ArgMode::Out), + default: Some(Expr::Value( + Value::SingleQuotedString("2".into()).with_empty_span() + )), + }, + ProcedureParam { + name: Ident::new("c"), + data_type: DataType::Timestamp(None, TimezoneInfo::None), + mode: Some(ArgMode::InOut), + default: Some(Expr::Value(Value::Null.with_empty_span())), + }, + ProcedureParam { + name: Ident::new("d"), + data_type: DataType::Bool, + mode: None, + default: Some(Expr::Value((number("0")).with_empty_span())), + } + ]), + ); + } + _ => unreachable!(), + } } #[test] @@ -17039,7 +17126,19 @@ fn parse_copy_options() { "IAM_ROLE DEFAULT ", "IGNOREHEADER AS 1 ", "TIMEFORMAT AS 'auto' ", - "TRUNCATECOLUMNS", + "TRUNCATECOLUMNS ", + "REMOVEQUOTES ", + "COMPUPDATE ", + "COMPUPDATE PRESET ", + "COMPUPDATE ON ", + "COMPUPDATE OFF ", + "COMPUPDATE TRUE ", + "COMPUPDATE FALSE ", + "STATUPDATE ", + "STATUPDATE ON ", + "STATUPDATE OFF ", + "STATUPDATE TRUE ", + "STATUPDATE FALSE", ), concat!( "COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ", @@ -17052,7 +17151,19 @@ fn parse_copy_options() { "IAM_ROLE DEFAULT ", "IGNOREHEADER 1 ", "TIMEFORMAT 'auto' ", - "TRUNCATECOLUMNS", + "TRUNCATECOLUMNS ", + "REMOVEQUOTES ", + "COMPUPDATE ", + "COMPUPDATE PRESET ", + "COMPUPDATE TRUE ", + "COMPUPDATE FALSE ", + "COMPUPDATE TRUE ", + "COMPUPDATE FALSE ", + "STATUPDATE ", + "STATUPDATE TRUE ", + "STATUPDATE FALSE ", + "STATUPDATE TRUE ", + "STATUPDATE FALSE", ), ); one_statement_parses_to( @@ -17230,9 +17341,9 @@ fn parse_invisible_column() { let sql = r#"ALTER TABLE t ADD COLUMN bar INT INVISIBLE"#; let stmt = verified_stmt(sql); match stmt { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(alter_table) => { assert_eq!( - operations, + alter_table.operations, vec![AlterTableOperation::AddColumn { column_keyword: true, if_not_exists: false, @@ -17502,3 +17613,22 @@ fn test_parse_alter_user() { } verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('ALL'), PASSWORD='secret', WORKLOAD_IDENTITY=(TYPE=AWS, ARN='arn:aws:iam::123456789:r1/')"); } + +#[test] +fn parse_generic_unary_ops() { + let unary_ops = &[ + ("~", UnaryOperator::BitwiseNot), + ("-", UnaryOperator::Minus), + ("+", UnaryOperator::Plus), + ]; + for (str_op, op) in unary_ops { + let select = verified_only_select(&format!("SELECT {}expr", &str_op)); + assert_eq!( + UnnamedExpr(UnaryOp { + op: *op, + expr: Box::new(Identifier(Ident::new("expr"))), + }), + select.projection[0] + ); + } +} diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index e01611b6f..92b635339 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -328,7 +328,7 @@ fn data_type_timestamp_ntz() { assert_eq!( databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), Expr::TypedString(TypedString { - data_type: DataType::TimestampNtz, + data_type: DataType::TimestampNtz(None), value: ValueWithSpan { value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), span: Span::empty(), @@ -345,7 +345,7 @@ fn data_type_timestamp_ntz() { expr: Box::new(Expr::Nested(Box::new(Expr::Identifier( "created_at".into() )))), - data_type: DataType::TimestampNtz, + data_type: DataType::TimestampNtz(None), format: None } ); @@ -357,7 +357,7 @@ fn data_type_timestamp_ntz() { columns, vec![ColumnDef { name: "x".into(), - data_type: DataType::TimestampNtz, + data_type: DataType::TimestampNtz(None), options: vec![], }] ); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b1ad422ec..a947db49b 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -156,6 +156,7 @@ fn parse_create_procedure() { }, data_type: DataType::Int(None), mode: None, + default: None, }, ProcedureParam { name: Ident { @@ -168,6 +169,7 @@ fn parse_create_procedure() { unit: None })), mode: None, + default: None, } ]), name: ObjectName::from(vec![Ident { @@ -196,6 +198,10 @@ fn parse_mssql_create_procedure() { let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN SELECT [foo], CASE WHEN [foo] IS NULL THEN 'empty' ELSE 'notempty' END AS [foo]; END"); // Multiple statements let _ = ms().verified_stmt("CREATE PROCEDURE [foo] AS BEGIN UPDATE bar SET col = 'test'; SELECT [foo] FROM BAR WHERE [FOO] > 10; END"); + + // parameters with default values + let sql = r#"CREATE PROCEDURE foo (IN @a INTEGER = 1, OUT @b TEXT = '2', INOUT @c DATETIME = NULL, @d BOOL = 0) AS BEGIN SELECT 1; END"#; + let _ = ms().verified_stmt(sql); } #[test] @@ -772,14 +778,10 @@ fn parse_mssql_bin_literal() { fn parse_mssql_create_role() { let sql = "CREATE ROLE mssql AUTHORIZATION helena"; match ms().verified_stmt(sql) { - Statement::CreateRole { - names, - authorization_owner, - .. - } => { - assert_eq_vec(&["mssql"], &names); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mssql"], &create_role.names); assert_eq!( - authorization_owner, + create_role.authorization_owner, Some(ObjectName::from(vec![Ident { value: "helena".into(), quote_style: None, @@ -2386,17 +2388,17 @@ fn parse_create_trigger() { create_stmt, Statement::CreateTrigger(CreateTrigger { or_alter: true, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("reminder1")]), - period: TriggerPeriod::After, + period: Some(TriggerPeriod::After), period_before_table: false, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![]),], table_name: ObjectName::from(vec![Ident::new("Sales"), Ident::new("Customer")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Statement, - include_each: false, + trigger_object: None, condition: None, exec_body: None, statements_as: true, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5d75aa508..e43df87ab 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -638,10 +638,14 @@ fn parse_create_table_auto_increment() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }, ColumnOptionDef { name: None, @@ -684,7 +688,7 @@ fn table_constraint_unique_primary_ctor( }) .collect(); match unique_index_type_display { - Some(index_type_display) => TableConstraint::Unique { + Some(index_type_display) => UniqueConstraint { name, index_name, index_type_display, @@ -693,15 +697,17 @@ fn table_constraint_unique_primary_ctor( index_options, characteristics, nulls_distinct: NullsDistinctOption::None, - }, - None => TableConstraint::PrimaryKey { + } + .into(), + None => PrimaryKeyConstraint { name, index_name, index_type, columns, index_options, characteristics, - }, + } + .into(), } } @@ -741,10 +747,14 @@ fn parse_create_table_primary_and_unique_key() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }, ColumnOptionDef { name: None, @@ -1380,10 +1390,14 @@ fn parse_quote_identifiers() { data_type: DataType::Int(None), options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }], }], columns @@ -2601,7 +2615,7 @@ fn parse_insert_with_numeric_prefix_column_name() { fn parse_update_with_joins() { let sql = "UPDATE orders AS o JOIN customers AS c ON o.customer_id = c.id SET o.completed = true WHERE c.firstname = 'Peter'"; match mysql().verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, from: _from, @@ -2609,7 +2623,7 @@ fn parse_update_with_joins() { returning, or: None, limit: None, - } => { + }) => { assert_eq!( TableWithJoins { relation: TableFactor::Table { @@ -2727,7 +2741,7 @@ fn parse_delete_with_limit() { #[test] fn parse_alter_table_add_column() { match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT FIRST") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, @@ -2736,7 +2750,7 @@ fn parse_alter_table_add_column() { location: _, on_cluster: _, end_token: _, - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!iceberg); @@ -2759,13 +2773,13 @@ fn parse_alter_table_add_column() { } match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT AFTER foo") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!only); @@ -2796,13 +2810,13 @@ fn parse_alter_table_add_columns() { match mysql() .verified_stmt("ALTER TABLE tab ADD COLUMN a TEXT FIRST, ADD COLUMN b INT AFTER foo") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!only); @@ -3024,7 +3038,7 @@ fn parse_alter_table_with_algorithm() { "ALTER TABLE users DROP COLUMN password_digest, ALGORITHM = COPY, RENAME COLUMN name TO username"; let stmt = mysql_and_generic().verified_stmt(sql); match stmt { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![ @@ -3072,7 +3086,7 @@ fn parse_alter_table_with_lock() { "ALTER TABLE users DROP COLUMN password_digest, LOCK = EXCLUSIVE, RENAME COLUMN name TO username"; let stmt = mysql_and_generic().verified_stmt(sql); match stmt { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![ @@ -3855,7 +3869,7 @@ fn parse_revoke() { fn parse_create_view_algorithm_param() { let sql = "CREATE ALGORITHM = MERGE VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3863,7 +3877,7 @@ fn parse_create_view_algorithm_param() { security, }), .. - } = stmt + }) = stmt { assert_eq!(algorithm, Some(CreateViewAlgorithm::Merge)); assert!(definer.is_none()); @@ -3879,7 +3893,7 @@ fn parse_create_view_algorithm_param() { fn parse_create_view_definer_param() { let sql = "CREATE DEFINER = 'jeffrey'@'localhost' VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3887,7 +3901,7 @@ fn parse_create_view_definer_param() { security, }), .. - } = stmt + }) = stmt { assert!(algorithm.is_none()); if let Some(GranteeName::UserHost { user, host }) = definer { @@ -3908,7 +3922,7 @@ fn parse_create_view_definer_param() { fn parse_create_view_security_param() { let sql = "CREATE SQL SECURITY DEFINER VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3916,7 +3930,7 @@ fn parse_create_view_security_param() { security, }), .. - } = stmt + }) = stmt { assert!(algorithm.is_none()); assert!(definer.is_none()); @@ -3931,7 +3945,7 @@ fn parse_create_view_security_param() { fn parse_create_view_multiple_params() { let sql = "CREATE ALGORITHM = UNDEFINED DEFINER = `root`@`%` SQL SECURITY INVOKER VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3939,7 +3953,7 @@ fn parse_create_view_multiple_params() { security, }), .. - } = stmt + }) = stmt { assert_eq!(algorithm, Some(CreateViewAlgorithm::Undefined)); if let Some(GranteeName::UserHost { user, host }) = definer { @@ -4016,17 +4030,17 @@ fn parse_create_trigger() { create_stmt, Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), - period: TriggerPeriod::Before, + period: Some(TriggerPeriod::Before), period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -4177,7 +4191,7 @@ fn test_variable_assignment_using_colon_equal() { let stmt = mysql().verified_stmt(sql_update); match stmt { - Statement::Update { assignments, .. } => { + Statement::Update(Update { assignments, .. }) => { assert_eq!( assignments, vec![Assignment { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 196a82f54..9ba0fb978 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -605,11 +605,12 @@ fn parse_alter_table_constraints_unique_nulls_distinct() { match pg_and_generic() .verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)") { - Statement::AlterTable { operations, .. } => match &operations[0] { + Statement::AlterTable(alter_table) => match &alter_table.operations[0] { AlterTableOperation::AddConstraint { - constraint: TableConstraint::Unique { nulls_distinct, .. }, + constraint: TableConstraint::Unique(constraint), .. } => { + let nulls_distinct = &constraint.nulls_distinct; assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct) } _ => unreachable!(), @@ -673,93 +674,93 @@ fn parse_create_extension() { fn parse_drop_extension() { assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: None, - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into(), "extension_name2".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into(), "extension_name2".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: None, - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic() .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name1".into(), "extension_name2".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic() .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name1".into(), "extension_name2".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); } @@ -828,13 +829,13 @@ fn parse_alter_table_alter_column_add_generated() { #[test] fn parse_alter_table_add_columns() { match pg().verified_stmt("ALTER TABLE IF EXISTS ONLY tab ADD COLUMN a TEXT, ADD COLUMN b INT") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(if_exists); assert!(only); @@ -908,13 +909,13 @@ fn parse_alter_table_owner_to() { for case in test_cases { match pg_and_generic().verified_stmt(case.sql) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists: _, only: _, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert_eq!( operations, @@ -2008,7 +2009,7 @@ fn parse_pg_returning() { RETURNING temp_lo AS lo, temp_hi AS hi, prcp", ); match stmt { - Statement::Update { returning, .. } => { + Statement::Update(Update { returning, .. }) => { assert_eq!( Some(vec![ SelectItem::ExprWithAlias { @@ -2143,13 +2144,11 @@ fn parse_ampersand_arobase() { #[test] fn parse_pg_unary_ops() { let pg_unary_ops = &[ - ("~", UnaryOperator::PGBitwiseNot), ("|/", UnaryOperator::PGSquareRoot), ("||/", UnaryOperator::PGCubeRoot), ("!!", UnaryOperator::PGPrefixFactorial), ("@", UnaryOperator::PGAbs), ]; - for (str_op, op) in pg_unary_ops { let select = pg().verified_only_select(&format!("SELECT {}a", &str_op)); assert_eq!( @@ -3832,47 +3831,29 @@ fn parse_custom_operator() { fn parse_create_role() { let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; match pg().verified_stmt(sql) { - Statement::CreateRole { - names, - if_not_exists, - .. - } => { - assert_eq_vec(&["mysql_a", "mysql_b"], &names); - assert!(if_not_exists); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); + assert!(create_role.if_not_exists); } _ => unreachable!(), } let sql = "CREATE ROLE abc LOGIN PASSWORD NULL"; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - login, - password, - .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq!(*login, Some(true)); - assert_eq!(*password, Some(Password::NullPassword)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.password, Some(Password::NullPassword)); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH LOGIN PASSWORD NULL"; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - login, - password, - .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq!(*login, Some(true)); - assert_eq!(*password, Some(Password::NullPassword)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.password, Some(Password::NullPassword)); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } @@ -3880,69 +3861,44 @@ fn parse_create_role() { let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION"; // Roundtrip order of optional parameters is not preserved match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - if_not_exists, - bypassrls, - login, - inherit, - password, - superuser, - create_db, - create_role, - replication, - connection_limit, - valid_until, - in_role, - in_group, - role, - user: _, - admin, - authorization_owner, - }], - ) => { - assert_eq_vec(&["magician"], names); - assert!(!*if_not_exists); - assert_eq!(*login, Some(true)); - assert_eq!(*inherit, Some(true)); - assert_eq!(*bypassrls, Some(true)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["magician"], &create_role.names); + assert!(!create_role.if_not_exists); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.inherit, Some(true)); + assert_eq!(create_role.bypassrls, Some(true)); assert_eq!( - *password, + create_role.password, Some(Password::Password(Expr::Value( (Value::SingleQuotedString("abcdef".into())).with_empty_span() ))) ); - assert_eq!(*superuser, Some(true)); - assert_eq!(*create_db, Some(false)); - assert_eq!(*create_role, Some(true)); - assert_eq!(*replication, Some(true)); - assert_eq!(*connection_limit, None); + assert_eq!(create_role.superuser, Some(true)); + assert_eq!(create_role.create_db, Some(false)); + assert_eq!(create_role.create_role, Some(true)); + assert_eq!(create_role.replication, Some(true)); + assert_eq!(create_role.connection_limit, None); assert_eq!( - *valid_until, + create_role.valid_until, Some(Expr::Value( (Value::SingleQuotedString("2025-01-01".into())).with_empty_span() )) ); - assert_eq_vec(&["role1", "role2"], in_role); - assert!(in_group.is_empty()); - assert_eq_vec(&["role3"], role); - assert_eq_vec(&["role4", "role5"], admin); - assert_eq!(*authorization_owner, None); + assert_eq_vec(&["role1", "role2"], &create_role.in_role); + assert!(create_role.in_group.is_empty()); + assert_eq_vec(&["role3"], &create_role.role); + assert_eq_vec(&["role4", "role5"], &create_role.admin); + assert_eq!(create_role.authorization_owner, None); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH USER foo, bar ROLE baz "; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, user, role, .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq_vec(&["foo", "bar"], user); - assert_eq_vec(&["baz"], role); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq_vec(&["foo", "bar"], &create_role.user); + assert_eq_vec(&["baz"], &create_role.role); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } @@ -4531,7 +4487,7 @@ fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![FunctionDesc { name: ObjectName::from(vec![Ident { @@ -4542,13 +4498,13 @@ fn parse_drop_function() { args: None }], drop_behavior: None - } + }) ); let sql = "DROP FUNCTION IF EXISTS test_func(a INTEGER, IN b INTEGER = 1)"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![FunctionDesc { name: ObjectName::from(vec![Ident { @@ -4569,13 +4525,13 @@ fn parse_drop_function() { ]), }], drop_behavior: None - } + }) ); let sql = "DROP FUNCTION IF EXISTS test_func1(a INTEGER, IN b INTEGER = 1), test_func2(a VARCHAR, IN b INTEGER = 1)"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![ FunctionDesc { @@ -4616,7 +4572,7 @@ fn parse_drop_function() { } ], drop_behavior: None - } + }) ); } @@ -4956,14 +4912,14 @@ fn parse_truncate() { only: false, }]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: false, identity: None, cascade: None, on_cluster: None, - }, + }), truncate ); } @@ -4980,14 +4936,14 @@ fn parse_truncate_with_options() { }]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, - }, + }), truncate ); } @@ -5013,14 +4969,14 @@ fn parse_truncate_with_table_list() { ]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, - }, + }), truncate ); } @@ -5578,7 +5534,7 @@ fn parse_create_domain() { data_type: DataType::Integer(None), collation: None, default: None, - constraints: vec![TableConstraint::Check { + constraints: vec![CheckConstraint { name: None, expr: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("VALUE"))), @@ -5586,7 +5542,8 @@ fn parse_create_domain() { right: Box::new(Expr::Value(test_utils::number("0").into())), }), enforced: None, - }], + } + .into()], }); assert_eq!(pg().verified_stmt(sql1), expected); @@ -5597,7 +5554,7 @@ fn parse_create_domain() { data_type: DataType::Integer(None), collation: Some(Ident::with_quote('"', "en_US")), default: None, - constraints: vec![TableConstraint::Check { + constraints: vec![CheckConstraint { name: None, expr: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("VALUE"))), @@ -5605,7 +5562,8 @@ fn parse_create_domain() { right: Box::new(Expr::Value(test_utils::number("0").into())), }), enforced: None, - }], + } + .into()], }); assert_eq!(pg().verified_stmt(sql2), expected); @@ -5616,7 +5574,7 @@ fn parse_create_domain() { data_type: DataType::Integer(None), collation: None, default: Some(Expr::Value(test_utils::number("1").into())), - constraints: vec![TableConstraint::Check { + constraints: vec![CheckConstraint { name: None, expr: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("VALUE"))), @@ -5624,7 +5582,8 @@ fn parse_create_domain() { right: Box::new(Expr::Value(test_utils::number("0").into())), }), enforced: None, - }], + } + .into()], }); assert_eq!(pg().verified_stmt(sql3), expected); @@ -5635,7 +5594,7 @@ fn parse_create_domain() { data_type: DataType::Integer(None), collation: Some(Ident::with_quote('"', "en_US")), default: Some(Expr::Value(test_utils::number("1").into())), - constraints: vec![TableConstraint::Check { + constraints: vec![CheckConstraint { name: None, expr: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("VALUE"))), @@ -5643,7 +5602,8 @@ fn parse_create_domain() { right: Box::new(Expr::Value(test_utils::number("0").into())), }), enforced: None, - }], + } + .into()], }); assert_eq!(pg().verified_stmt(sql4), expected); @@ -5654,7 +5614,7 @@ fn parse_create_domain() { data_type: DataType::Integer(None), collation: None, default: None, - constraints: vec![TableConstraint::Check { + constraints: vec![CheckConstraint { name: Some(Ident::new("my_constraint")), expr: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("VALUE"))), @@ -5662,7 +5622,8 @@ fn parse_create_domain() { right: Box::new(Expr::Value(test_utils::number("0").into())), }), enforced: None, - }], + } + .into()], }); assert_eq!(pg().verified_stmt(sql5), expected); @@ -5673,17 +5634,17 @@ fn parse_create_simple_before_insert_trigger() { let sql = "CREATE TRIGGER check_insert BEFORE INSERT ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_insert"; let expected = Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_insert")]), - period: TriggerPeriod::Before, + period: Some(TriggerPeriod::Before), period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5705,17 +5666,17 @@ fn parse_create_after_update_trigger_with_condition() { let sql = "CREATE TRIGGER check_update AFTER UPDATE ON accounts FOR EACH ROW WHEN (NEW.balance > 10000) EXECUTE FUNCTION check_account_update"; let expected = Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_update")]), - period: TriggerPeriod::After, + period: Some(TriggerPeriod::After), period_before_table: true, events: vec![TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: Some(Expr::Nested(Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("NEW"), @@ -5744,17 +5705,17 @@ fn parse_create_instead_of_delete_trigger() { let sql = "CREATE TRIGGER check_delete INSTEAD OF DELETE ON accounts FOR EACH ROW EXECUTE FUNCTION check_account_deletes"; let expected = Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_delete")]), - period: TriggerPeriod::InsteadOf, + period: Some(TriggerPeriod::InsteadOf), period_before_table: true, events: vec![TriggerEvent::Delete], table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5776,10 +5737,11 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { let sql = "CREATE CONSTRAINT TRIGGER check_multiple_events BEFORE INSERT OR UPDATE OR DELETE ON accounts DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION check_account_changes"; let expected = Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: true, name: ObjectName::from(vec![Ident::new("check_multiple_events")]), - period: TriggerPeriod::Before, + period: Some(TriggerPeriod::Before), period_before_table: true, events: vec![ TriggerEvent::Insert, @@ -5789,8 +5751,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5816,10 +5777,11 @@ fn parse_create_trigger_with_referencing() { let sql = "CREATE TRIGGER check_referencing BEFORE INSERT ON accounts REFERENCING NEW TABLE AS new_accounts OLD TABLE AS old_accounts FOR EACH ROW EXECUTE FUNCTION check_account_referencing"; let expected = Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("check_referencing")]), - period: TriggerPeriod::Before, + period: Some(TriggerPeriod::Before), period_before_table: true, events: vec![TriggerEvent::Insert], table_name: ObjectName::from(vec![Ident::new("accounts")]), @@ -5836,8 +5798,7 @@ fn parse_create_trigger_with_referencing() { transition_relation_name: ObjectName::from(vec![Ident::new("old_accounts")]), }, ], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5863,11 +5824,11 @@ fn parse_create_trigger_invalid_cases() { let invalid_cases = vec![ ( "CREATE TRIGGER check_update BEFORE UPDATE ON accounts FUNCTION check_account_update", - "Expected: FOR, found: FUNCTION" + "Expected: an SQL statement, found: FUNCTION" ), ( "CREATE TRIGGER check_update TOMORROW UPDATE ON accounts EXECUTE FUNCTION check_account_update", - "Expected: one of FOR or BEFORE or AFTER or INSTEAD, found: TOMORROW" + "Expected: one of INSERT or UPDATE or DELETE or TRUNCATE, found: TOMORROW" ), ( "CREATE TRIGGER check_update BEFORE SAVE ON accounts EXECUTE FUNCTION check_account_update", @@ -6132,17 +6093,17 @@ fn parse_trigger_related_functions() { create_trigger, Statement::CreateTrigger(CreateTrigger { or_alter: false, + temporary: false, or_replace: false, is_constraint: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), - period: TriggerPeriod::Before, + period: Some(TriggerPeriod::Before), period_before_table: true, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])], table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], - trigger_object: TriggerObject::Row, - include_each: true, + trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)), condition: None, exec_body: Some(TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -6403,7 +6364,7 @@ fn parse_varbit_datatype() { #[test] fn parse_alter_table_replica_identity() { match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY FULL") { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::ReplicaIdentity { @@ -6415,7 +6376,7 @@ fn parse_alter_table_replica_identity() { } match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY USING INDEX foo_idx") { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::ReplicaIdentity { @@ -6463,11 +6424,11 @@ fn parse_alter_table_constraint_not_valid() { match pg_and_generic().verified_stmt( "ALTER TABLE foo ADD CONSTRAINT bar FOREIGN KEY (baz) REFERENCES other(ref) NOT VALID", ) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::AddConstraint { - constraint: TableConstraint::ForeignKey { + constraint: ForeignKeyConstraint { name: Some("bar".into()), index_name: None, columns: vec!["baz".into()], @@ -6475,8 +6436,10 @@ fn parse_alter_table_constraint_not_valid() { referred_columns: vec!["ref".into()], on_delete: None, on_update: None, + match_kind: None, characteristics: None, - }, + } + .into(), not_valid: true, }] ); @@ -6488,7 +6451,7 @@ fn parse_alter_table_constraint_not_valid() { #[test] fn parse_alter_table_validate_constraint() { match pg_and_generic().verified_stmt("ALTER TABLE foo VALIDATE CONSTRAINT bar") { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::ValidateConstraint { name: "bar".into() }] @@ -6639,3 +6602,51 @@ fn parse_alter_schema() { _ => unreachable!(), } } + +#[test] +fn parse_foreign_key_match() { + let test_cases = [ + ("MATCH FULL", ConstraintReferenceMatchKind::Full), + ("MATCH SIMPLE", ConstraintReferenceMatchKind::Simple), + ("MATCH PARTIAL", ConstraintReferenceMatchKind::Partial), + ]; + + for (match_clause, expected_kind) in test_cases { + // Test column-level foreign key + let sql = format!("CREATE TABLE t (id INT REFERENCES other_table (id) {match_clause})"); + let statement = pg_and_generic().verified_stmt(&sql); + match statement { + Statement::CreateTable(CreateTable { columns, .. }) => { + match &columns[0].options[0].option { + ColumnOption::ForeignKey(constraint) => { + assert_eq!(constraint.match_kind, Some(expected_kind)); + } + _ => panic!("Expected ColumnOption::ForeignKey"), + } + } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } + + // Test table-level foreign key constraint + let sql = format!( + "CREATE TABLE t (id INT, FOREIGN KEY (id) REFERENCES other_table(id) {match_clause})" + ); + let statement = pg_and_generic().verified_stmt(&sql); + match statement { + Statement::CreateTable(CreateTable { constraints, .. }) => match &constraints[0] { + TableConstraint::ForeignKey(constraint) => { + assert_eq!(constraint.match_kind, Some(expected_kind)); + } + _ => panic!("Expected TableConstraint::ForeignKey"), + }, + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } + } +} + +#[test] +fn parse_foreign_key_match_with_actions() { + let sql = "CREATE TABLE orders (order_id INT REFERENCES another_table (id) MATCH FULL ON DELETE CASCADE ON UPDATE RESTRICT, customer_id INT, CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH SIMPLE ON DELETE SET NULL ON UPDATE CASCADE)"; + + pg_and_generic().verified_stmt(sql); +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e04bfaf5d..2be5eae8c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -53,11 +53,11 @@ fn parse_sf_create_secure_view_and_materialized_view() { "CREATE OR REPLACE SECURE MATERIALIZED VIEW v AS SELECT 1", ] { match snowflake().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { secure, materialized, .. - } => { + }) => { assert!(secure); if sql.contains("MATERIALIZED") { assert!(materialized); @@ -1047,7 +1047,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { test_utils::TestedDialects::new(vec![Box::new(SnowflakeDialect {}) as Box]); match dialect.verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, or_replace, @@ -1060,7 +1060,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(options, CreateTableOptions::None); @@ -3281,7 +3281,7 @@ fn parse_view_column_descriptions() { let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; match snowflake().verified_stmt(sql) { - Statement::CreateView { name, columns, .. } => { + Statement::CreateView(CreateView { name, columns, .. }) => { assert_eq!(name.to_string(), "v"); assert_eq!( columns, @@ -4638,6 +4638,21 @@ fn test_create_database() { assert!(err.contains("Expected"), "Unexpected error: {err}"); } +#[test] +fn test_timestamp_ntz_with_precision() { + snowflake().verified_stmt("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(1))"); + snowflake().verified_stmt("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(9))"); + + let select = + snowflake().verified_only_select("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(9))"); + match expr_from_projection(only(&select.projection)) { + Expr::Cast { data_type, .. } => { + assert_eq!(*data_type, DataType::TimestampNtz(Some(9))); + } + _ => unreachable!(), + } +} + #[test] fn test_drop_constraints() { snowflake().verified_stmt("ALTER TABLE tbl DROP PRIMARY KEY"); diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 114aca03a..f1f6cf49b 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -166,7 +166,7 @@ fn parse_create_virtual_table() { fn parse_create_view_temporary_if_not_exists() { let sql = "CREATE TEMPORARY VIEW IF NOT EXISTS myschema.myview AS SELECT foo FROM bar"; match sqlite_and_generic().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, query, @@ -179,7 +179,7 @@ fn parse_create_view_temporary_if_not_exists() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -217,10 +217,14 @@ fn parse_create_table_auto_increment() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None - }, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], + characteristics: None, + }), }, ColumnOptionDef { name: None, @@ -245,10 +249,14 @@ fn parse_create_table_primary_key_asc_desc() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { - is_primary: true, + option: ColumnOption::PrimaryKey(PrimaryKeyConstraint { + name: None, + index_name: None, + index_type: None, + columns: vec![], + index_options: vec![], characteristics: None, - }, + }), }, ColumnOptionDef { name: None, @@ -467,7 +475,7 @@ fn parse_update_tuple_row_values() { // See https://github.com/sqlparser-rs/sqlparser-rs/issues/1311 assert_eq!( sqlite().verified_stmt("UPDATE x SET (a, b) = (1, 2)"), - Statement::Update { + Statement::Update(Update { or: None, assignments: vec![Assignment { target: AssignmentTarget::Tuple(vec![ @@ -487,7 +495,7 @@ fn parse_update_tuple_row_values() { from: None, returning: None, limit: None - } + }) ); } @@ -596,7 +604,7 @@ fn test_regexp_operator() { #[test] fn test_update_delete_limit() { match sqlite().verified_stmt("UPDATE foo SET bar = 1 LIMIT 99") { - Statement::Update { limit, .. } => { + Statement::Update(Update { limit, .. }) => { assert_eq!(limit, Some(Expr::value(number("99")))); } _ => unreachable!(), @@ -610,6 +618,285 @@ fn test_update_delete_limit() { } } +#[test] +fn test_create_trigger() { + let statement1 = "CREATE TRIGGER trg_inherit_asset_models AFTER INSERT ON assets FOR EACH ROW BEGIN INSERT INTO users (name) SELECT pam.name FROM users AS pam; END"; + + match sqlite().verified_stmt(statement1) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_inherit_asset_models"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "assets"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert_eq!( + trigger_object, + Some(TriggerObjectKind::ForEach(TriggerObject::Row)) + ); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // Here we check that the variant of CREATE TRIGGER that omits the `FOR EACH ROW` clause, + // which in SQLite may be implicitly assumed, is parsed correctly. + let statement2 = "CREATE TRIGGER log_new_user AFTER INSERT ON users BEGIN INSERT INTO user_log (user_id, action, timestamp) VALUES (NEW.id, 'created', datetime('now')); END"; + + match sqlite().verified_stmt(statement2) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "log_new_user"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "users"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + let statement3 = "CREATE TRIGGER cleanup_orders AFTER DELETE ON customers BEGIN DELETE FROM orders WHERE customer_id = OLD.id; DELETE FROM invoices WHERE customer_id = OLD.id; END"; + match sqlite().verified_stmt(statement3) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "cleanup_orders"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Delete]); + assert_eq!(table_name.to_string(), "customers"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + let statement4 = "CREATE TRIGGER trg_before_update BEFORE UPDATE ON products FOR EACH ROW WHEN NEW.price < 0 BEGIN SELECT RAISE(ABORT, 'Price cannot be negative'); END"; + match sqlite().verified_stmt(statement4) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_before_update"); + assert_eq!(period, Some(TriggerPeriod::Before)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Update(Vec::new())]); + assert_eq!(table_name.to_string(), "products"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert_eq!( + trigger_object, + Some(TriggerObjectKind::ForEach(TriggerObject::Row)) + ); + assert!(condition.is_some()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a INSTEAD OF trigger on a view + let statement5 = "CREATE TRIGGER trg_instead_of_insert INSTEAD OF INSERT ON my_view BEGIN INSERT INTO my_table (col1, col2) VALUES (NEW.col1, NEW.col2); END"; + match sqlite().verified_stmt(statement5) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(!temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "trg_instead_of_insert"); + assert_eq!(period, Some(TriggerPeriod::InsteadOf)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "my_view"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a temporary trigger + let statement6 = "CREATE TEMPORARY TRIGGER temp_trigger AFTER INSERT ON temp_table BEGIN UPDATE log_table SET count = count + 1; END"; + match sqlite().verified_stmt(statement6) { + Statement::CreateTrigger(CreateTrigger { + or_alter, + temporary, + or_replace, + is_constraint, + name, + period, + period_before_table, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + exec_body: _, + statements_as, + statements: _, + characteristics, + }) => { + assert!(!or_alter); + assert!(temporary); + assert!(!or_replace); + assert!(!is_constraint); + assert_eq!(name.to_string(), "temp_trigger"); + assert_eq!(period, Some(TriggerPeriod::After)); + assert!(period_before_table); + assert_eq!(events, vec![TriggerEvent::Insert]); + assert_eq!(table_name.to_string(), "temp_table"); + assert!(referenced_table_name.is_none()); + assert!(referencing.is_empty()); + assert!(trigger_object.is_none()); + assert!(condition.is_none()); + assert!(!statements_as); + assert!(characteristics.is_none()); + } + _ => unreachable!("Expected CREATE TRIGGER statement"), + } + + // We test a trigger defined without a period (BEFORE/AFTER/INSTEAD OF) + let statement7 = "CREATE TRIGGER trg_inherit_asset_models INSERT ON assets FOR EACH ROW BEGIN INSERT INTO users (name) SELECT pam.name FROM users AS pam; END"; + sqlite().verified_stmt(statement7); +} + +#[test] +fn test_drop_trigger() { + let statement = "DROP TRIGGER IF EXISTS trg_inherit_asset_models"; + + match sqlite().verified_stmt(statement) { + Statement::DropTrigger(DropTrigger { + if_exists, + trigger_name, + table_name, + option, + }) => { + assert!(if_exists); + assert_eq!(trigger_name.to_string(), "trg_inherit_asset_models"); + assert!(table_name.is_none()); + assert!(option.is_none()); + } + _ => unreachable!("Expected DROP TRIGGER statement"), + } +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) }