diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0c4f93e64..a3cac7ca7 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -49,8 +49,8 @@ use crate::ast::{ MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, - TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, - WrappedCollection, + TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, UtilityOption, Value, + ValueWithSpan, WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; @@ -3808,6 +3808,410 @@ impl fmt::Display for AlterSchema { } } +/// `CREATE TABLESPACE` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtablespace.html) +/// and [MySQL](https://dev.mysql.com/doc/refman/8.4/en/create-tablespace.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 CreateTablespace { + /// Name of the tablespace being created. + pub name: Ident, + /// Dialect-specific definition payload. + pub definition: CreateTablespaceDefinition, +} + +/// Dialect-specific `CREATE TABLESPACE` definitions. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateTablespaceDefinition { + /// PostgreSQL form: + /// `CREATE TABLESPACE name LOCATION 'path' [WITH (...)]` + PostgreSql { + /// Filesystem location for the tablespace. + location: String, + /// Optional storage parameters (`WITH (...)`). + with_options: Vec, + }, + /// MySQL form: + /// `CREATE [UNDO] TABLESPACE name ` + MySql { + /// Whether `UNDO` was specified before `TABLESPACE`. + undo: bool, + /// Ordered MySQL options/clauses. + options: Vec, + }, +} + +/// A MySQL `CREATE TABLESPACE` clause. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MySqlCreateTablespaceOption { + /// `ADD DATAFILE 'file_name'`. + AddDatafile(String), + /// `USE LOGFILE GROUP logfile_group`. + UseLogfileGroup(Ident), + /// `FILE_BLOCK_SIZE [=] value`. + FileBlockSize(Expr), + /// `EXTENT_SIZE [=] extent_size`. + ExtentSize(Expr), + /// `INITIAL_SIZE [=] initial_size`. + InitialSize(Expr), + /// `AUTOEXTEND_SIZE [=] autoextend_size`. + AutoextendSize(Expr), + /// `MAX_SIZE [=] max_size`. + MaxSize(Expr), + /// `NODEGROUP [=] nodegroup_id`. + Nodegroup(Expr), + /// `WAIT`. + Wait, + /// `COMMENT [=] 'comment'`. + Comment(String), + /// `ENCRYPTION [=] {'Y' | 'N'}`. + Encryption(Expr), + /// `ENGINE [=] engine_name`. + Engine(Ident), + /// `ENGINE_ATTRIBUTE [=] 'string'`. + EngineAttribute(String), +} + +impl fmt::Display for MySqlCreateTablespaceOption { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MySqlCreateTablespaceOption::AddDatafile(file) => { + write!(f, "ADD DATAFILE '{}'", escape_single_quote_string(file)) + } + MySqlCreateTablespaceOption::UseLogfileGroup(group) => { + write!(f, "USE LOGFILE GROUP {group}") + } + MySqlCreateTablespaceOption::FileBlockSize(value) => { + write!(f, "FILE_BLOCK_SIZE = {value}") + } + MySqlCreateTablespaceOption::ExtentSize(value) => { + write!(f, "EXTENT_SIZE = {value}") + } + MySqlCreateTablespaceOption::InitialSize(value) => { + write!(f, "INITIAL_SIZE = {value}") + } + MySqlCreateTablespaceOption::AutoextendSize(value) => { + write!(f, "AUTOEXTEND_SIZE = {value}") + } + MySqlCreateTablespaceOption::MaxSize(value) => write!(f, "MAX_SIZE = {value}"), + MySqlCreateTablespaceOption::Nodegroup(value) => write!(f, "NODEGROUP = {value}"), + MySqlCreateTablespaceOption::Wait => write!(f, "WAIT"), + MySqlCreateTablespaceOption::Comment(comment) => { + write!(f, "COMMENT = '{}'", escape_single_quote_string(comment)) + } + MySqlCreateTablespaceOption::Encryption(value) => write!(f, "ENCRYPTION = {value}"), + MySqlCreateTablespaceOption::Engine(engine) => write!(f, "ENGINE = {engine}"), + MySqlCreateTablespaceOption::EngineAttribute(attr) => { + write!( + f, + "ENGINE_ATTRIBUTE = '{}'", + escape_single_quote_string(attr) + ) + } + } + } +} + +impl fmt::Display for CreateTablespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.definition { + CreateTablespaceDefinition::PostgreSql { + location, + with_options, + } => { + write!( + f, + "CREATE TABLESPACE {} LOCATION '{}'", + self.name, + escape_single_quote_string(location) + )?; + if !with_options.is_empty() { + write!(f, " WITH ({})", display_comma_separated(with_options))?; + } + } + CreateTablespaceDefinition::MySql { undo, options } => { + write!( + f, + "CREATE {}TABLESPACE {}", + if *undo { "UNDO " } else { "" }, + self.name + )?; + for option in options { + write!(f, " {option}")?; + } + } + } + Ok(()) + } +} + +/// A single option in `ALTER TABLESPACE ... RESET (...)`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TablespaceResetOption { + /// A plain parameter name. + Name(Ident), + /// A permissive `name = value` form. + Assign { + /// Parameter name. + key: Ident, + /// Parameter value expression. + value: Expr, + }, +} + +impl fmt::Display for TablespaceResetOption { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TablespaceResetOption::Name(name) => write!(f, "{name}"), + TablespaceResetOption::Assign { key, value } => write!(f, "{key} = {value}"), + } + } +} + +/// `ALTER TABLESPACE` operation. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertablespace.html) +/// and [MySQL](https://dev.mysql.com/doc/refman/8.4/en/alter-tablespace.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTablespaceOperation { + /// Rename the tablespace. + RenameTo(Ident), + /// Change tablespace owner. + OwnerTo(Owner), + /// Set tablespace parameters. + Set { + /// Parameters to set. + options: Vec, + }, + /// Reset tablespace parameters. + Reset { + /// Parameters to reset. + options: Vec, + }, + /// MySQL-specific operation. + MySql { + /// Whether `UNDO` was specified before `TABLESPACE`. + undo: bool, + /// Ordered MySQL options/clauses. + options: Vec, + }, +} + +/// MySQL `ALTER TABLESPACE` operation body. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MySqlAlterTablespaceOperation { + /// `ADD DATAFILE 'file_name'`. + AddDatafile(String), + /// `DROP DATAFILE 'file_name'`. + DropDatafile(String), + /// `INITIAL_SIZE [=] size`. + InitialSize(Expr), + /// `AUTOEXTEND_SIZE [=] autoextend_size`. + AutoextendSize(Expr), + /// `WAIT`. + Wait, + /// `RENAME TO tablespace_name`. + RenameTo(Ident), + /// `SET ACTIVE`. + SetActive, + /// `SET INACTIVE`. + SetInactive, + /// `ENCRYPTION [=] {'Y' | 'N'}`. + Encryption(Expr), + /// `ENGINE [=] engine_name`. + Engine(Ident), + /// `ENGINE_ATTRIBUTE [=] 'string'`. + EngineAttribute(String), +} + +impl fmt::Display for MySqlAlterTablespaceOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MySqlAlterTablespaceOperation::AddDatafile(file) => { + write!(f, "ADD DATAFILE '{}'", escape_single_quote_string(file)) + } + MySqlAlterTablespaceOperation::DropDatafile(file) => { + write!(f, "DROP DATAFILE '{}'", escape_single_quote_string(file)) + } + MySqlAlterTablespaceOperation::InitialSize(size) => write!(f, "INITIAL_SIZE = {size}"), + MySqlAlterTablespaceOperation::AutoextendSize(size) => { + write!(f, "AUTOEXTEND_SIZE = {size}") + } + MySqlAlterTablespaceOperation::Wait => write!(f, "WAIT"), + MySqlAlterTablespaceOperation::RenameTo(name) => write!(f, "RENAME TO {name}"), + MySqlAlterTablespaceOperation::SetActive => write!(f, "SET ACTIVE"), + MySqlAlterTablespaceOperation::SetInactive => write!(f, "SET INACTIVE"), + MySqlAlterTablespaceOperation::Encryption(value) => write!(f, "ENCRYPTION = {value}"), + MySqlAlterTablespaceOperation::Engine(engine) => write!(f, "ENGINE = {engine}"), + MySqlAlterTablespaceOperation::EngineAttribute(attr) => { + write!( + f, + "ENGINE_ATTRIBUTE = '{}'", + escape_single_quote_string(attr) + ) + } + } + } +} + +impl fmt::Display for AlterTablespaceOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AlterTablespaceOperation::RenameTo(name) => write!(f, "RENAME TO {name}"), + AlterTablespaceOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterTablespaceOperation::Set { options } => { + write!(f, "SET ({})", display_comma_separated(options)) + } + AlterTablespaceOperation::Reset { options } => { + write!(f, "RESET ({})", display_comma_separated(options)) + } + AlterTablespaceOperation::MySql { undo, options } => { + if *undo { + write!(f, "UNDO ")?; + } + write!(f, "{}", display_separated(options, " ")) + } + } + } +} + +/// `ALTER TABLESPACE` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertablespace.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 AlterTablespace { + /// Name of the tablespace. + pub name: Ident, + /// Operation to apply. + pub operation: AlterTablespaceOperation, +} + +impl fmt::Display for AlterTablespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let AlterTablespaceOperation::MySql { undo, options } = &self.operation { + write!( + f, + "ALTER {}TABLESPACE {} {}", + if *undo { "UNDO " } else { "" }, + self.name, + display_separated(options, " ") + ) + } else { + write!(f, "ALTER TABLESPACE {} {}", self.name, self.operation) + } + } +} + +/// `DROP TABLESPACE` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-droptablespace.html) +/// and [MySQL](https://dev.mysql.com/doc/refman/8.4/en/drop-tablespace.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 DropTablespace { + /// Whether `IF EXISTS` was specified. + pub if_exists: bool, + /// Whether `UNDO` was specified before `TABLESPACE`. + pub undo: bool, + /// Name of the tablespace. + pub name: Ident, + /// Optional MySQL storage engine. + pub engine: Option, +} + +impl fmt::Display for DropTablespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "DROP {}TABLESPACE {}{}", + if self.undo { "UNDO " } else { "" }, + if self.if_exists { "IF EXISTS " } else { "" }, + self.name + )?; + if let Some(engine) = &self.engine { + write!(f, " ENGINE = {engine}")?; + } + Ok(()) + } +} + +/// `REINDEX` object type. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-reindex.html) +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ReindexObjectType { + /// Reindex an index. + Index, + /// Reindex a table. + Table, + /// Reindex a schema. + Schema, + /// Reindex a database. + Database, + /// Reindex system catalogs. + System, +} + +impl fmt::Display for ReindexObjectType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + ReindexObjectType::Index => "INDEX", + ReindexObjectType::Table => "TABLE", + ReindexObjectType::Schema => "SCHEMA", + ReindexObjectType::Database => "DATABASE", + ReindexObjectType::System => "SYSTEM", + }) + } +} + +/// `REINDEX` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-reindex.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 ReindexStatement { + /// Optional utility-style options (e.g. `(TABLESPACE foo)`). + pub options: Option>, + /// Target object type. + pub object_type: ReindexObjectType, + /// Whether `CONCURRENTLY` was specified. + pub concurrently: bool, + /// Target object name. + pub name: ObjectName, +} + +impl fmt::Display for ReindexStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "REINDEX ")?; + if let Some(options) = &self.options { + write!(f, "({}) ", display_comma_separated(options))?; + } + write!(f, "{}", self.object_type)?; + if self.concurrently { + write!(f, " CONCURRENTLY")?; + } + write!(f, " {}", self.name) + } +} + impl Spanned for RenameTableNameKind { fn span(&self) -> Span { match self { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1e430171e..37086ee48 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -64,20 +64,23 @@ pub use self::ddl::{ AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, - AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, - AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, - ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, - CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, - CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTrigger, CreateView, Deduplicate, + AlterTableOperation, AlterTableType, AlterTablespace, AlterTablespaceOperation, AlterType, + AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, + AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, + ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, + CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, + CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, + CreateTablespace, CreateTablespaceDefinition, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, - DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, ForValues, GeneratedAs, - GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, - KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, + DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTablespace, DropTrigger, ForValues, + GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, + IndexOption, IndexType, KeyOrIndexDisplay, Msck, MySqlAlterTablespaceOperation, + MySqlCreateTablespaceOption, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, - PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, - TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, + PartitionBoundValue, ProcedureParam, ReferentialAction, ReindexObjectType, ReindexStatement, + RenameTableNameKind, ReplicaIdentity, TablespaceResetOption, TagsColumnOption, + TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, }; @@ -3682,6 +3685,11 @@ pub enum Statement { /// A `CREATE SERVER` statement. CreateServer(CreateServerStatement), /// ```sql + /// CREATE TABLESPACE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtablespace.html) + CreateTablespace(CreateTablespace), + /// ```sql /// CREATE POLICY /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) @@ -3716,6 +3724,11 @@ pub enum Statement { /// See [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#alter_schema_collate_statement) AlterSchema(AlterSchema), /// ```sql + /// ALTER TABLESPACE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertablespace.html) + AlterTablespace(AlterTablespace), + /// ```sql /// ALTER INDEX /// ``` AlterIndex { @@ -3869,6 +3882,10 @@ pub enum Statement { table: Option, }, /// ```sql + /// DROP [UNDO] TABLESPACE + /// ``` + DropTablespace(DropTablespace), + /// ```sql /// DROP FUNCTION /// ``` DropFunction(DropFunction), @@ -4824,6 +4841,13 @@ pub enum Statement { /// ``` /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) Vacuum(VacuumStatement), + /// Rebuild indexes. + /// + /// ```sql + /// REINDEX + /// ``` + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-reindex.html) + Reindex(ReindexStatement), /// Restore the value of a run-time parameter to the default value. /// /// ```sql @@ -5436,6 +5460,7 @@ impl fmt::Display for Statement { Statement::CreateServer(stmt) => { write!(f, "{stmt}") } + Statement::CreateTablespace(stmt) => write!(f, "{stmt}"), Statement::CreatePolicy(policy) => write!(f, "{policy}"), Statement::CreateConnector(create_connector) => create_connector.fmt(f), Statement::CreateOperator(create_operator) => create_operator.fmt(f), @@ -5547,6 +5572,7 @@ impl fmt::Display for Statement { }; Ok(()) } + Statement::DropTablespace(drop_tablespace) => write!(f, "{drop_tablespace}"), Statement::DropFunction(drop_function) => write!(f, "{drop_function}"), Statement::DropDomain(DropDomain { if_exists, @@ -6252,7 +6278,9 @@ impl fmt::Display for Statement { Statement::ExportData(e) => write!(f, "{e}"), Statement::CreateUser(s) => write!(f, "{s}"), Statement::AlterSchema(s) => write!(f, "{s}"), + Statement::AlterTablespace(s) => write!(f, "{s}"), Statement::Vacuum(s) => write!(f, "{s}"), + Statement::Reindex(s) => write!(f, "{s}"), Statement::AlterUser(s) => write!(f, "{s}"), Statement::Reset(s) => write!(f, "{s}"), } @@ -8254,6 +8282,8 @@ pub enum ObjectType { User, /// A stream. Stream, + /// A tablespace. + Tablespace, } impl fmt::Display for ObjectType { @@ -8271,6 +8301,7 @@ impl fmt::Display for ObjectType { ObjectType::Type => "TYPE", ObjectType::User => "USER", ObjectType::Stream => "STREAM", + ObjectType::Tablespace => "TABLESPACE", }) } } @@ -11888,6 +11919,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(c: CreateTablespace) -> Self { + Self::CreateTablespace(c) + } +} + impl From for Statement { fn from(c: CreateConnector) -> Self { Self::CreateConnector(c) @@ -11918,6 +11955,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(a: AlterTablespace) -> Self { + Self::AlterTablespace(a) + } +} + impl From for Statement { fn from(a: AlterType) -> Self { Self::AlterType(a) @@ -11960,6 +12003,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(d: DropTablespace) -> Self { + Self::DropTablespace(d) + } +} + impl From for Statement { fn from(s: ShowCharset) -> Self { Self::ShowCharset(s) @@ -12068,6 +12117,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(r: ReindexStatement) -> Self { + Self::Reindex(r) + } +} + impl From for Statement { fn from(r: ResetStatement) -> Self { Self::Reset(r) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0b95c3ed7..2e27bf226 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -382,6 +382,7 @@ impl Spanned for Statement { Statement::DropOperatorClass(drop_operator_class) => drop_operator_class.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), + Statement::CreateTablespace(..) => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), Statement::CreateOperator(create_operator) => create_operator.span(), Statement::CreateOperatorFamily(create_operator_family) => { @@ -412,6 +413,7 @@ impl Spanned for Statement { Statement::AttachDuckDBDatabase { .. } => Span::empty(), Statement::DetachDuckDBDatabase { .. } => Span::empty(), Statement::Drop { .. } => Span::empty(), + Statement::DropTablespace(_) => Span::empty(), Statement::DropFunction(drop_function) => drop_function.span(), Statement::DropDomain { .. } => Span::empty(), Statement::DropProcedure { .. } => Span::empty(), @@ -499,7 +501,9 @@ impl Spanned for Statement { ), Statement::CreateUser(..) => Span::empty(), Statement::AlterSchema(s) => s.span(), + Statement::AlterTablespace(..) => Span::empty(), Statement::Vacuum(..) => Span::empty(), + Statement::Reindex(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bea566bbe..32d8bf00c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -508,10 +508,10 @@ impl<'a> Parser<'a> { Token::EOF => break, // end of statement - Token::Word(word) => { - if expecting_statement_delimiter && word.keyword == Keyword::END { - break; - } + Token::Word(word) + if expecting_statement_delimiter && word.keyword == Keyword::END => + { + break; } _ => {} } @@ -714,6 +714,10 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_vacuum() } + Keyword::REINDEX => { + self.prev_token(); + self.parse_reindex() + } Keyword::RESET => self.parse_reset().map(Into::into), _ => self.expected("an SQL statement", next_token), }, @@ -1298,41 +1302,40 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { - t @ (Token::Word(_) | Token::SingleQuotedString(_)) => { - if self.peek_token_ref().token == Token::Period { - let mut id_parts: Vec = vec![match t { - Token::Word(w) => w.into_ident(next_token.span), - Token::SingleQuotedString(s) => Ident::with_quote('\'', s), - _ => { - return Err(ParserError::ParserError( - "Internal parser error: unexpected token type".to_string(), - )) + t @ (Token::Word(_) | Token::SingleQuotedString(_)) + if self.peek_token_ref().token == Token::Period => + { + let mut id_parts: Vec = vec![match t { + Token::Word(w) => w.into_ident(next_token.span), + Token::SingleQuotedString(s) => Ident::with_quote('\'', s), + _ => { + return Err(ParserError::ParserError( + "Internal parser error: unexpected token type".to_string(), + )) + } + }]; + + while self.consume_token(&Token::Period) { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => id_parts.push(w.into_ident(next_token.span)), + Token::SingleQuotedString(s) => { + // SQLite has single-quoted identifiers + id_parts.push(Ident::with_quote('\'', s)) } - }]; - - while self.consume_token(&Token::Period) { - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => id_parts.push(w.into_ident(next_token.span)), - Token::SingleQuotedString(s) => { - // SQLite has single-quoted identifiers - id_parts.push(Ident::with_quote('\'', s)) - } - Token::Placeholder(s) => { - // Snowflake uses $1, $2, etc. for positional column references - // in staged data queries like: SELECT t.$1 FROM @stage t - id_parts.push(Ident::new(s)) - } - Token::Mul => { - return Ok(Expr::QualifiedWildcard( - ObjectName::from(id_parts), - AttachedToken(next_token), - )); - } - _ => { - return self - .expected("an identifier or a '*' after '.'", next_token); - } + Token::Placeholder(s) => { + // Snowflake uses $1, $2, etc. for positional column references + // in staged data queries like: SELECT t.$1 FROM @stage t + id_parts.push(Ident::new(s)) + } + Token::Mul => { + return Ok(Expr::QualifiedWildcard( + ObjectName::from(id_parts), + AttachedToken(next_token), + )); + } + _ => { + return self.expected("an identifier or a '*' after '.'", next_token); } } } @@ -4577,6 +4580,39 @@ impl<'a> Parser<'a> { } } + /// If the current token is an unquoted word matching `expected` (case-insensitive), + /// consume it and return `true`. + #[must_use] + pub fn peek_unquoted_word(&self, expected: &str) -> bool { + matches!( + &self.peek_token_ref().token, + Token::Word(word) + if word.quote_style.is_none() && word.value.eq_ignore_ascii_case(expected) + ) + } + + /// If the current token is an unquoted word matching `expected` (case-insensitive), + /// consume it and return `true`. + #[must_use] + pub fn parse_unquoted_word(&mut self, expected: &str) -> bool { + if self.peek_unquoted_word(expected) { + self.advance_token(); + true + } else { + false + } + } + + /// Expect the current token to be an unquoted word matching `expected` + /// (case-insensitive), consuming it on success. + pub fn expect_unquoted_word(&mut self, expected: &str) -> Result<(), ParserError> { + if self.parse_unquoted_word(expected) { + Ok(()) + } else { + self.expected_ref(expected, self.peek_token_ref()) + } + } + #[must_use] /// Check if the current token is the expected keyword without consuming it. /// @@ -4990,10 +5026,10 @@ impl<'a> Parser<'a> { loop { match &self.peek_nth_token_ref(0).token { Token::EOF => break, - Token::Word(w) => { - if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { - break; - } + Token::Word(w) + if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) => + { + break; } _ => {} } @@ -5168,6 +5204,13 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::SERVER) { self.parse_pg_create_server() + } else if dialect_of!(self is MySqlDialect | GenericDialect) + && self.parse_unquoted_word("UNDO") + { + self.expect_keyword(Keyword::TABLESPACE)?; + self.parse_create_tablespace(true).map(Into::into) + } else if self.parse_keyword(Keyword::TABLESPACE) { + self.parse_create_tablespace(false).map(Into::into) } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) } @@ -5513,6 +5556,136 @@ impl<'a> Parser<'a> { }) } + /// Parse a `CREATE TABLESPACE` statement. + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtablespace.html) + /// and [MySQL](https://dev.mysql.com/doc/refman/8.4/en/create-tablespace.html). + pub fn parse_create_tablespace(&mut self, undo: bool) -> Result { + let is_pg = dialect_of!(self is PostgreSqlDialect); + let is_mysql = dialect_of!(self is MySqlDialect); + let is_generic = dialect_of!(self is GenericDialect); + + if !is_pg && !is_mysql && !is_generic { + return Err(ParserError::ParserError( + "CREATE TABLESPACE is only supported for PostgreSqlDialect, MySqlDialect, and GenericDialect" + .to_string(), + )); + } + if undo && is_pg { + return Err(ParserError::ParserError( + "CREATE UNDO TABLESPACE is not supported in PostgreSqlDialect".to_string(), + )); + } + + let name = self.parse_identifier()?; + + let parse_as_postgres = if is_pg { + true + } else if is_mysql { + false + } else { + !undo && self.peek_keyword(Keyword::LOCATION) + }; + + if parse_as_postgres { + self.expect_keyword(Keyword::LOCATION)?; + let location = self.parse_literal_string()?; + let with_options = if self.peek_keyword(Keyword::WITH) { + self.parse_options(Keyword::WITH)? + } else { + vec![] + }; + + return Ok(CreateTablespace { + name, + definition: CreateTablespaceDefinition::PostgreSql { + location, + with_options, + }, + }); + } + + let options = self.parse_mysql_create_tablespace_options()?; + + Ok(CreateTablespace { + name, + definition: CreateTablespaceDefinition::MySql { undo, options }, + }) + } + + fn parse_mysql_create_tablespace_options( + &mut self, + ) -> Result, ParserError> { + let mut options = vec![]; + + loop { + let option = if self.parse_keyword(Keyword::USE) { + self.expect_unquoted_word("LOGFILE")?; + self.expect_keyword(Keyword::GROUP)?; + Some(MySqlCreateTablespaceOption::UseLogfileGroup( + self.parse_identifier()?, + )) + } else if self.parse_unquoted_word("FILE_BLOCK_SIZE") { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::FileBlockSize( + self.parse_expr()?, + )) + } else if self.parse_unquoted_word("EXTENT_SIZE") { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::ExtentSize(self.parse_expr()?)) + } else if self.parse_unquoted_word("INITIAL_SIZE") { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::InitialSize(self.parse_expr()?)) + } else if self.parse_keyword(Keyword::AUTOEXTEND_SIZE) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::AutoextendSize( + self.parse_expr()?, + )) + } else if self.parse_unquoted_word("MAX_SIZE") { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::MaxSize(self.parse_expr()?)) + } else if self.parse_unquoted_word("NODEGROUP") { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::Nodegroup(self.parse_expr()?)) + } else if self.parse_unquoted_word("WAIT") { + Some(MySqlCreateTablespaceOption::Wait) + } else if self.parse_keyword(Keyword::COMMENT) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::Comment( + self.parse_literal_string()?, + )) + } else if self.parse_keyword(Keyword::ENCRYPTION) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::Encryption(self.parse_expr()?)) + } else if self.parse_keyword(Keyword::ENGINE) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::Engine( + self.parse_identifier()?, + )) + } else if self.parse_keyword(Keyword::ENGINE_ATTRIBUTE) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlCreateTablespaceOption::EngineAttribute( + self.parse_literal_string()?, + )) + } else if self.parse_keyword(Keyword::ADD) { + self.expect_unquoted_word("DATAFILE")?; + Some(MySqlCreateTablespaceOption::AddDatafile( + self.parse_literal_string()?, + )) + } else { + None + }; + + if let Some(option) = option { + options.push(option); + } else { + break; + } + } + + Ok(options) + } + /// Parse an optional `USING` clause for `CREATE FUNCTION`. pub fn parse_optional_create_function_using( &mut self, @@ -7198,6 +7371,16 @@ impl<'a> Parser<'a> { let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); + if dialect_of!(self is MySqlDialect | GenericDialect) && self.parse_unquoted_word("UNDO") { + if temporary { + return Err(ParserError::ParserError( + "DROP TEMPORARY UNDO TABLESPACE is not supported".to_string(), + )); + } + self.expect_keyword(Keyword::TABLESPACE)?; + return self.parse_drop_tablespace(true); + } + let object_type = if self.parse_keyword(Keyword::TABLE) { ObjectType::Table } else if self.parse_keyword(Keyword::VIEW) { @@ -7222,6 +7405,13 @@ impl<'a> Parser<'a> { ObjectType::User } else if self.parse_keyword(Keyword::STREAM) { ObjectType::Stream + } else if self.parse_keyword(Keyword::TABLESPACE) { + if temporary { + return Err(ParserError::ParserError( + "DROP TEMPORARY TABLESPACE is not supported".to_string(), + )); + } + return self.parse_drop_tablespace(false); } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function().map(Into::into); } else if self.parse_keyword(Keyword::POLICY) { @@ -7249,7 +7439,7 @@ impl<'a> Parser<'a> { }; } else { return self.expected_ref( - "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", + "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TABLESPACE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", self.peek_token_ref(), ); }; @@ -7288,6 +7478,58 @@ impl<'a> Parser<'a> { }) } + fn parse_drop_tablespace(&mut self, undo: bool) -> Result { + let is_pg = dialect_of!(self is PostgreSqlDialect); + let is_mysql = dialect_of!(self is MySqlDialect); + let is_generic = dialect_of!(self is GenericDialect); + + if !is_pg && !is_mysql && !is_generic { + return Err(ParserError::ParserError( + "DROP TABLESPACE is only supported for PostgreSqlDialect, MySqlDialect, and GenericDialect" + .to_string(), + )); + } + if undo && is_pg { + return Err(ParserError::ParserError( + "DROP UNDO TABLESPACE is not supported in PostgreSqlDialect".to_string(), + )); + } + + let if_exists = if self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]) { + if is_mysql { + return Err(ParserError::ParserError( + "DROP TABLESPACE IF EXISTS is not supported in MySqlDialect".to_string(), + )); + } + true + } else { + false + }; + let name = self.parse_identifier()?; + let engine = if self.parse_keyword(Keyword::ENGINE) { + if is_pg { + return Err(ParserError::ParserError( + "DROP TABLESPACE ENGINE is not supported in PostgreSqlDialect".to_string(), + )); + } + let _ = self.consume_token(&Token::Eq); + Some(self.parse_identifier()?) + } else { + None + }; + + if self.peek_keyword(Keyword::CASCADE) || self.peek_keyword(Keyword::RESTRICT) { + return self.expected_ref("end of DROP TABLESPACE statement", self.peek_token_ref()); + } + + Ok(Statement::DropTablespace(DropTablespace { + if_exists, + undo, + name, + engine, + })) + } + fn parse_optional_drop_behavior(&mut self) -> Option { match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { Some(Keyword::CASCADE) => Some(DropBehavior::Cascade), @@ -8173,70 +8415,60 @@ impl<'a> Parser<'a> { Keyword::LINES, Keyword::NULL, ]) { - Some(Keyword::FIELDS) => { - if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) { + Some(Keyword::FIELDS) + if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::FieldsTerminatedBy, + char: self.parse_identifier()?, + }); + + if self.parse_keywords(&[Keyword::ESCAPED, Keyword::BY]) { row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::FieldsTerminatedBy, + delimiter: HiveDelimiter::FieldsEscapedBy, char: self.parse_identifier()?, }); - - if self.parse_keywords(&[Keyword::ESCAPED, Keyword::BY]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::FieldsEscapedBy, - char: self.parse_identifier()?, - }); - } - } else { - break; } } - Some(Keyword::COLLECTION) => { + Some(Keyword::COLLECTION) if self.parse_keywords(&[ Keyword::ITEMS, Keyword::TERMINATED, Keyword::BY, - ]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::CollectionItemsTerminatedBy, - char: self.parse_identifier()?, - }); - } else { - break; - } + ]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::CollectionItemsTerminatedBy, + char: self.parse_identifier()?, + }); } - Some(Keyword::MAP) => { + Some(Keyword::MAP) if self.parse_keywords(&[ Keyword::KEYS, Keyword::TERMINATED, Keyword::BY, - ]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::MapKeysTerminatedBy, - char: self.parse_identifier()?, - }); - } else { - break; - } + ]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::MapKeysTerminatedBy, + char: self.parse_identifier()?, + }); } - Some(Keyword::LINES) => { - if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::LinesTerminatedBy, - char: self.parse_identifier()?, - }); - } else { - break; - } + Some(Keyword::LINES) + if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::LinesTerminatedBy, + char: self.parse_identifier()?, + }); } - Some(Keyword::NULL) => { - if self.parse_keywords(&[Keyword::DEFINED, Keyword::AS]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::NullDefinedAs, - char: self.parse_identifier()?, - }); - } else { - break; - } + Some(Keyword::NULL) + if self.parse_keywords(&[Keyword::DEFINED, Keyword::AS]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::NullDefinedAs, + char: self.parse_identifier()?, + }); } _ => { break; @@ -10428,6 +10660,11 @@ impl<'a> Parser<'a> { /// Parse an `ALTER ` statement and dispatch to the appropriate alter handler. pub fn parse_alter(&mut self) -> Result { + if dialect_of!(self is MySqlDialect | GenericDialect) && self.parse_unquoted_word("UNDO") { + self.expect_keyword(Keyword::TABLESPACE)?; + return self.parse_alter_tablespace(true).map(Into::into); + } + let object_type = self.expect_one_of_keywords(&[ Keyword::VIEW, Keyword::TYPE, @@ -10438,6 +10675,7 @@ impl<'a> Parser<'a> { Keyword::CONNECTOR, Keyword::ICEBERG, Keyword::SCHEMA, + Keyword::TABLESPACE, Keyword::USER, Keyword::OPERATOR, ])?; @@ -10447,6 +10685,7 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_alter_schema() } + Keyword::TABLESPACE => self.parse_alter_tablespace(false).map(Into::into), Keyword::VIEW => self.parse_alter_view(), Keyword::TYPE => self.parse_alter_type(), Keyword::TABLE => self.parse_alter_table(false), @@ -10487,7 +10726,7 @@ impl<'a> Parser<'a> { Keyword::USER => self.parse_alter_user().map(Into::into), // unreachable because expect_one_of_keywords used above unexpected_keyword => Err(ParserError::ParserError( - format!("Internal parser error: expected any of {{VIEW, TYPE, TABLE, INDEX, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), + format!("Internal parser error: expected any of {{VIEW, TYPE, TABLE, INDEX, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, TABLESPACE, USER, OPERATOR}}, got {unexpected_keyword:?}"), )), } } @@ -10950,6 +11189,199 @@ impl<'a> Parser<'a> { })) } + /// Parse an `ALTER TABLESPACE` statement. + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertablespace.html) + /// and [MySQL](https://dev.mysql.com/doc/refman/8.4/en/alter-tablespace.html). + pub fn parse_alter_tablespace(&mut self, undo: bool) -> Result { + let is_pg = dialect_of!(self is PostgreSqlDialect); + let is_mysql = dialect_of!(self is MySqlDialect); + let is_generic = dialect_of!(self is GenericDialect); + + if !is_pg && !is_mysql && !is_generic { + return Err(ParserError::ParserError( + "ALTER TABLESPACE is only supported for PostgreSqlDialect, MySqlDialect, and GenericDialect" + .to_string(), + )); + } + if undo && is_pg { + return Err(ParserError::ParserError( + "ALTER UNDO TABLESPACE is not supported in PostgreSqlDialect".to_string(), + )); + } + + let name = self.parse_identifier()?; + + if is_mysql || (is_generic && undo) { + let options = self.parse_mysql_alter_tablespace_options()?; + return Ok(AlterTablespace { + name, + operation: AlterTablespaceOperation::MySql { undo, options }, + }); + } + + if is_pg { + return Ok(AlterTablespace { + name, + operation: self.parse_postgres_alter_tablespace_operation()?, + }); + } + + let operation = { + let start_idx = self.index; + match self.parse_postgres_alter_tablespace_operation() { + Ok(operation) if !self.peek_mysql_alter_tablespace_option_start() => operation, + Ok(_) | Err(_) => { + self.index = start_idx; + let options = self.parse_mysql_alter_tablespace_options()?; + AlterTablespaceOperation::MySql { undo, options } + } + } + }; + + Ok(AlterTablespace { name, operation }) + } + + fn peek_mysql_alter_tablespace_option_start(&self) -> bool { + if self.peek_keyword(Keyword::ADD) + || self.peek_keyword(Keyword::DROP) + || self.peek_keyword(Keyword::AUTOEXTEND_SIZE) + || self.peek_keyword(Keyword::ENCRYPTION) + || self.peek_keyword(Keyword::ENGINE) + || self.peek_keyword(Keyword::ENGINE_ATTRIBUTE) + || self.peek_unquoted_word("INITIAL_SIZE") + || self.peek_unquoted_word("WAIT") + { + return true; + } + + if self.peek_keyword(Keyword::SET) { + match &self.peek_nth_token_ref(1).token { + Token::Word(word) + if word.quote_style.is_none() + && (word.value.eq_ignore_ascii_case("ACTIVE") + || word.value.eq_ignore_ascii_case("INACTIVE")) => + { + return true; + } + _ => {} + } + } + + false + } + + fn parse_postgres_alter_tablespace_operation( + &mut self, + ) -> Result { + if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + Ok(AlterTablespaceOperation::RenameTo(self.parse_identifier()?)) + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + Ok(AlterTablespaceOperation::OwnerTo(self.parse_owner()?)) + } else if self.parse_keyword(Keyword::SET) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(AlterTablespaceOperation::Set { options }) + } else if self.parse_keyword(Keyword::RESET) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_tablespace_reset_option)?; + self.expect_token(&Token::RParen)?; + Ok(AlterTablespaceOperation::Reset { options }) + } else { + self.expected_ref( + "RENAME TO, OWNER TO, SET, or RESET after ALTER TABLESPACE", + self.peek_token_ref(), + ) + } + } + + fn parse_mysql_alter_tablespace_options( + &mut self, + ) -> Result, ParserError> { + let mut options = vec![]; + loop { + let option = if self.parse_keyword(Keyword::ADD) { + self.expect_unquoted_word("DATAFILE")?; + Some(MySqlAlterTablespaceOperation::AddDatafile( + self.parse_literal_string()?, + )) + } else if self.parse_keyword(Keyword::DROP) { + self.expect_unquoted_word("DATAFILE")?; + Some(MySqlAlterTablespaceOperation::DropDatafile( + self.parse_literal_string()?, + )) + } else if self.parse_unquoted_word("INITIAL_SIZE") { + let _ = self.consume_token(&Token::Eq); + Some(MySqlAlterTablespaceOperation::InitialSize( + self.parse_expr()?, + )) + } else if self.parse_keyword(Keyword::AUTOEXTEND_SIZE) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlAlterTablespaceOperation::AutoextendSize( + self.parse_expr()?, + )) + } else if self.parse_unquoted_word("WAIT") { + Some(MySqlAlterTablespaceOperation::Wait) + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + Some(MySqlAlterTablespaceOperation::RenameTo( + self.parse_identifier()?, + )) + } else if self.parse_keyword(Keyword::SET) { + if self.parse_unquoted_word("ACTIVE") { + Some(MySqlAlterTablespaceOperation::SetActive) + } else if self.parse_unquoted_word("INACTIVE") { + Some(MySqlAlterTablespaceOperation::SetInactive) + } else { + return self + .expected_ref("ACTIVE or INACTIVE after SET", self.peek_token_ref()); + } + } else if self.parse_keyword(Keyword::ENCRYPTION) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlAlterTablespaceOperation::Encryption( + self.parse_expr()?, + )) + } else if self.parse_keyword(Keyword::ENGINE) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlAlterTablespaceOperation::Engine( + self.parse_identifier()?, + )) + } else if self.parse_keyword(Keyword::ENGINE_ATTRIBUTE) { + let _ = self.consume_token(&Token::Eq); + Some(MySqlAlterTablespaceOperation::EngineAttribute( + self.parse_literal_string()?, + )) + } else { + None + }; + + if let Some(option) = option { + options.push(option); + } else { + break; + } + } + + if options.is_empty() { + self.expected_ref( + "ADD DATAFILE, DROP DATAFILE, INITIAL_SIZE, AUTOEXTEND_SIZE, WAIT, RENAME TO, SET ACTIVE, SET INACTIVE, ENCRYPTION, ENGINE, or ENGINE_ATTRIBUTE after ALTER TABLESPACE", + self.peek_token_ref(), + ) + } else { + Ok(options) + } + } + + fn parse_tablespace_reset_option(&mut self) -> Result { + let key = self.parse_identifier()?; + if self.consume_token(&Token::Eq) { + let value = self.parse_expr()?; + Ok(TablespaceResetOption::Assign { key, value }) + } else { + Ok(TablespaceResetOption::Name(key)) + } + } + /// Parse a `CALL procedure_name(arg1, arg2, ...)` /// or `CALL procedure_name` statement pub fn parse_call(&mut self) -> Result { @@ -19515,6 +19947,52 @@ impl<'a> Parser<'a> { })) } + /// Parse a `REINDEX` statement. + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-reindex.html) + fn parse_reindex(&mut self) -> Result { + self.expect_keyword(Keyword::REINDEX)?; + if !dialect_of!(self is PostgreSqlDialect | GenericDialect) { + return Err(ParserError::ParserError( + "REINDEX is only supported for PostgreSqlDialect and GenericDialect".to_string(), + )); + } + + let options = if self.peek_token_ref().token == Token::LParen { + Some(self.parse_utility_options()?) + } else { + None + }; + + let object_type = match self.expect_one_of_keywords(&[ + Keyword::INDEX, + Keyword::TABLE, + Keyword::SCHEMA, + Keyword::DATABASE, + Keyword::SYSTEM, + ])? { + Keyword::INDEX => ReindexObjectType::Index, + Keyword::TABLE => ReindexObjectType::Table, + Keyword::SCHEMA => ReindexObjectType::Schema, + Keyword::DATABASE => ReindexObjectType::Database, + Keyword::SYSTEM => ReindexObjectType::System, + unexpected_keyword => { + return Err(ParserError::ParserError(format!( + "Internal parser error: unexpected keyword `{unexpected_keyword}` in REINDEX" + ))) + } + }; + let concurrently = self.parse_keyword(Keyword::CONCURRENTLY); + let name = self.parse_object_name(false)?; + + Ok(Statement::Reindex(ReindexStatement { + options, + object_type, + concurrently, + name, + })) + } + /// Consume the parser and return its underlying token buffer pub fn into_tokens(self) -> Vec { self.tokens diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 30405623d..4b6382bc7 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4773,3 +4773,107 @@ fn parse_create_database_with_charset_option_ordering() { "CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci", ); } + +#[test] +fn parse_mysql_create_tablespace() { + let sql = + "CREATE TABLESPACE ts ADD DATAFILE 'ts.ibd' FILE_BLOCK_SIZE = 4096 ENCRYPTION = 'Y' ENGINE = InnoDB ENGINE_ATTRIBUTE = 'ndb_general'"; + let stmt = mysql_and_generic().verified_stmt(sql); + + let Statement::CreateTablespace(CreateTablespace { name, definition }) = stmt else { + panic!("Expected CREATE TABLESPACE"); + }; + assert_eq!(name.value, "ts"); + + let CreateTablespaceDefinition::MySql { undo, options } = definition else { + panic!("Expected MySQL CREATE TABLESPACE definition"); + }; + assert!(!undo); + assert_eq!( + options[0], + MySqlCreateTablespaceOption::AddDatafile("ts.ibd".to_string()) + ); + assert_eq!(options[1].to_string(), "FILE_BLOCK_SIZE = 4096"); + assert_eq!(options[2].to_string(), "ENCRYPTION = 'Y'"); + assert_eq!(options[3].to_string(), "ENGINE = InnoDB"); + assert_eq!(options[4].to_string(), "ENGINE_ATTRIBUTE = 'ndb_general'"); +} + +#[test] +fn parse_mysql_create_tablespace_without_add_datafile() { + mysql_and_generic().one_statement_parses_to( + "CREATE TABLESPACE ts ENGINE InnoDB", + "CREATE TABLESPACE ts ENGINE = InnoDB", + ); +} + +#[test] +fn parse_mysql_create_undo_tablespace() { + mysql_and_generic().one_statement_parses_to( + "CREATE UNDO TABLESPACE undo_ts ADD DATAFILE 'undo_001.ibu' ENGINE InnoDB", + "CREATE UNDO TABLESPACE undo_ts ADD DATAFILE 'undo_001.ibu' ENGINE = InnoDB", + ); +} + +#[test] +fn parse_mysql_alter_tablespace() { + let stmt = mysql_and_generic().verified_stmt( + "ALTER TABLESPACE ts ADD DATAFILE 'ts2.ibd' INITIAL_SIZE = 16 AUTOEXTEND_SIZE = 32 WAIT ENGINE = InnoDB ENGINE_ATTRIBUTE = 'ndb_general'", + ); + let Statement::AlterTablespace(AlterTablespace { + name, + operation: AlterTablespaceOperation::MySql { undo, options }, + }) = stmt + else { + panic!("Expected MySQL ALTER TABLESPACE ADD DATAFILE"); + }; + + assert_eq!(name.value, "ts"); + assert!(!undo); + assert_eq!( + options + .iter() + .map(ToString::to_string) + .collect::>(), + vec![ + "ADD DATAFILE 'ts2.ibd'", + "INITIAL_SIZE = 16", + "AUTOEXTEND_SIZE = 32", + "WAIT", + "ENGINE = InnoDB", + "ENGINE_ATTRIBUTE = 'ndb_general'", + ] + ); + + mysql_and_generic().one_statement_parses_to( + "ALTER UNDO TABLESPACE undo_ts SET ACTIVE", + "ALTER UNDO TABLESPACE undo_ts SET ACTIVE", + ); +} + +#[test] +fn parse_mysql_drop_tablespace() { + let stmt = mysql_and_generic().one_statement_parses_to( + "DROP UNDO TABLESPACE undo_ts ENGINE InnoDB", + "DROP UNDO TABLESPACE undo_ts ENGINE = InnoDB", + ); + let Statement::DropTablespace(DropTablespace { + if_exists, + undo, + name, + engine, + }) = stmt + else { + panic!("Expected DROP TABLESPACE"); + }; + + assert!(!if_exists); + assert!(undo); + assert_eq!(name.value, "undo_ts"); + assert_eq!(engine.as_ref().map(|i| i.value.as_str()), Some("InnoDB")); + + mysql_and_generic().one_statement_parses_to( + "DROP TABLESPACE ts ENGINE InnoDB", + "DROP TABLESPACE ts ENGINE = InnoDB", + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7c19f51e5..7ce033b9d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -24,7 +24,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; use sqlparser::ast::*; -use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; +use sqlparser::dialect::{GenericDialect, MySqlDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; use sqlparser::tokenizer::Span; use test_utils::*; @@ -8602,3 +8602,130 @@ fn parse_pg_analyze() { _ => panic!("Expected Analyze, got: {stmt:?}"), } } + +#[test] +fn parse_tablespace_and_reindex_regression_cases() { + for sql in [ + "CREATE TABLESPACE regress_tblspace LOCATION 'relative'", + "CREATE TABLESPACE regress_tblspace LOCATION ''", + "CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (some_nonexistent_parameter = true)", + "CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)", + "DROP TABLESPACE regress_tblspacewith", + "ALTER TABLESPACE regress_tblspace SET (random_page_cost = 1.0, seq_page_cost = 1.1)", + "ALTER TABLESPACE regress_tblspace SET (some_nonexistent_parameter = true)", + "ALTER TABLESPACE regress_tblspace RESET (random_page_cost = 2.0)", + "ALTER TABLESPACE regress_tblspace RESET (random_page_cost, effective_io_concurrency)", + "REINDEX (TABLESPACE regress_tblspace) TABLE pg_am", + "REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_am", + "REINDEX (TABLESPACE regress_tblspace) TABLE pg_authid", + "REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid", + "REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index", + "REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index", + "REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262", + "REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262", + "REINDEX (TABLESPACE pg_global) TABLE pg_authid", + "REINDEX (TABLESPACE pg_global) TABLE CONCURRENTLY pg_authid", + "REINDEX (TABLESPACE pg_global) INDEX regress_tblspace_test_tbl_idx", + "REINDEX (TABLESPACE pg_global) INDEX CONCURRENTLY regress_tblspace_test_tbl_idx", + "REINDEX (TABLESPACE regress_tblspace) INDEX regress_tblspace_test_tbl_idx", + "REINDEX (TABLESPACE regress_tblspace) TABLE regress_tblspace_test_tbl", + ] { + pg_and_generic().verified_stmt(sql); + } +} + +#[test] +fn parse_alter_tablespace_reset_assignment_option() { + let stmt = pg_and_generic() + .verified_stmt("ALTER TABLESPACE regress_tblspace RESET (random_page_cost = 2.0)"); + let Statement::AlterTablespace(AlterTablespace { + name, + operation: AlterTablespaceOperation::Reset { options }, + }) = stmt + else { + panic!("Expected ALTER TABLESPACE RESET statement"); + }; + + assert_eq!(name.value, "regress_tblspace"); + assert_eq!(options.len(), 1); + let TablespaceResetOption::Assign { key, value } = &options[0] else { + panic!("Expected assignment form in RESET option list"); + }; + assert_eq!(key.value, "random_page_cost"); + assert_eq!(value.to_string(), "2.0"); +} + +#[test] +fn parse_reindex_with_tablespace_utility_option() { + let stmt = pg_and_generic() + .verified_stmt("REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_am"); + let Statement::Reindex(ReindexStatement { + options, + object_type, + concurrently, + name, + }) = stmt + else { + panic!("Expected REINDEX statement"); + }; + + assert_eq!(object_type, ReindexObjectType::Table); + assert!(concurrently); + assert_eq!(name.to_string(), "pg_am"); + + let options = options.expect("Expected utility options"); + assert_eq!(options.len(), 1); + assert_eq!(options[0].name.value, "TABLESPACE"); + assert_eq!( + options[0].arg.as_ref().map(ToString::to_string), + Some("regress_tblspace".to_string()) + ); +} + +#[test] +fn reject_postgres_tablespace_forms_in_mysql_dialect() { + let mysql = TestedDialects::new(vec![Box::new(MySqlDialect {})]); + assert!(mysql + .parse_sql_statements("CREATE TABLESPACE t LOCATION ''") + .is_err()); + assert!(mysql + .parse_sql_statements("ALTER TABLESPACE t SET (random_page_cost = 1.0)") + .is_err()); + assert!(mysql + .parse_sql_statements("DROP TABLESPACE IF EXISTS t") + .is_err()); + assert!(mysql + .parse_sql_statements("REINDEX (TABLESPACE t) TABLE pg_am") + .is_err()); +} + +#[test] +fn parse_drop_tablespace_in_postgres() { + let stmt = pg_and_generic().verified_stmt("DROP TABLESPACE IF EXISTS regress_tblspace"); + let Statement::DropTablespace(DropTablespace { + if_exists, + undo, + name, + engine, + }) = stmt + else { + panic!("Expected DROP TABLESPACE statement"); + }; + + assert!(if_exists); + assert!(!undo); + assert_eq!(name.value, "regress_tblspace"); + assert!(engine.is_none()); +} + +#[test] +fn reject_drop_tablespace_cascade_in_postgres() { + let err = pg() + .parse_sql_statements("DROP TABLESPACE regress_tblspace CASCADE") + .unwrap_err() + .to_string(); + assert!( + err.contains("Expected: end of DROP TABLESPACE statement"), + "unexpected error: {err}" + ); +}