diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 879740f03..0e936db35 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5222,6 +5222,151 @@ impl Spanned for AlterOperatorClass { } } +/// PostgreSQL text search object kind. +#[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 TextSearchObjectType { + /// `DICTIONARY` + Dictionary, + /// `CONFIGURATION` + Configuration, + /// `TEMPLATE` + Template, + /// `PARSER` + Parser, +} + +impl fmt::Display for TextSearchObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TextSearchObjectType::Dictionary => write!(f, "DICTIONARY"), + TextSearchObjectType::Configuration => write!(f, "CONFIGURATION"), + TextSearchObjectType::Template => write!(f, "TEMPLATE"), + TextSearchObjectType::Parser => write!(f, "PARSER"), + } + } +} + +/// PostgreSQL `CREATE TEXT SEARCH ...` 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 CreateTextSearch { + /// The specific text search object type. + pub object_type: TextSearchObjectType, + /// Object name. + pub name: ObjectName, + /// Parenthesized options. + pub options: Vec, +} + +impl fmt::Display for CreateTextSearch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH {} {} ({})", + self.object_type, + self.name, + display_comma_separated(&self.options) + ) + } +} + +impl Spanned for CreateTextSearch { + fn span(&self) -> Span { + Span::empty() + } +} + +/// Option assignment used by `ALTER TEXT SEARCH DICTIONARY ... ( ... )`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTextSearchDictionaryOption { + /// Option name. + pub key: Ident, + /// Optional value (`option [= value]`). + pub value: Option, +} + +impl fmt::Display for AlterTextSearchDictionaryOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.value { + Some(value) => write!(f, "{} = {}", self.key, value), + None => write!(f, "{}", self.key), + } + } +} + +/// Operation for PostgreSQL `ALTER TEXT SEARCH ...`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTextSearchOperation { + /// `RENAME TO new_name` + RenameTo { + /// New name. + new_name: Ident, + }, + /// `OWNER TO ...` + OwnerTo(Owner), + /// `SET SCHEMA schema_name` + SetSchema { + /// Target schema. + schema_name: ObjectName, + }, + /// `( option [= value] [, ...] )` + SetOptions { + /// Dictionary options to apply. + options: Vec, + }, +} + +impl fmt::Display for AlterTextSearchOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterTextSearchOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterTextSearchOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterTextSearchOperation::SetSchema { schema_name } => { + write!(f, "SET SCHEMA {schema_name}") + } + AlterTextSearchOperation::SetOptions { options } => { + write!(f, "({})", display_comma_separated(options)) + } + } + } +} + +/// PostgreSQL `ALTER TEXT SEARCH ...` 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 AlterTextSearch { + /// The specific text search object type. + pub object_type: TextSearchObjectType, + /// Object name. + pub name: ObjectName, + /// Operation to apply. + pub operation: AlterTextSearchOperation, +} + +impl fmt::Display for AlterTextSearch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ALTER TEXT SEARCH {} {} {}", + self.object_type, self.name, self.operation + ) + } +} + +impl Spanned for AlterTextSearch { + fn span(&self) -> Span { + Span::empty() + } +} + /// CREATE POLICY statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d7a3679a4..0889db866 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -64,23 +64,24 @@ pub use self::ddl::{ AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, - AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchDictionaryOption, + AlterTextSearchOperation, 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, - DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, - DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, - ForValues, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, - OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, - OperatorOption, OperatorPurpose, Owner, Partition, PartitionBoundValue, ProcedureParam, - ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, - Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, - UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, - UserDefinedTypeStorage, ViewColumnDef, + CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTextSearch, CreateTrigger, + CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, + DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, + DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, + NullsDistinctOption, OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, + OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, PartitionBoundValue, + ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, + TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, + UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, + UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, @@ -3719,6 +3720,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createopclass.html) CreateOperatorClass(CreateOperatorClass), /// ```sql + /// CREATE TEXT SEARCH { DICTIONARY | CONFIGURATION | TEMPLATE | PARSER } + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-intro.html) + CreateTextSearch(CreateTextSearch), + /// ```sql /// ALTER TABLE /// ``` AlterTable(AlterTable), @@ -3771,6 +3777,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alteropclass.html) AlterOperatorClass(AlterOperatorClass), /// ```sql + /// ALTER TEXT SEARCH { DICTIONARY | CONFIGURATION | TEMPLATE | PARSER } + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-configuration.html) + AlterTextSearch(AlterTextSearch), + /// ```sql /// ALTER ROLE /// ``` AlterRole { @@ -5467,6 +5478,7 @@ impl fmt::Display for Statement { create_operator_family.fmt(f) } Statement::CreateOperatorClass(create_operator_class) => create_operator_class.fmt(f), + Statement::CreateTextSearch(create_text_search) => create_text_search.fmt(f), Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") @@ -5496,6 +5508,7 @@ impl fmt::Display for Statement { Statement::AlterOperatorClass(alter_operator_class) => { write!(f, "{alter_operator_class}") } + Statement::AlterTextSearch(alter_text_search) => write!(f, "{alter_text_search}"), Statement::AlterRole { name, operation } => { write!(f, "ALTER ROLE {name} {operation}") } @@ -12074,6 +12087,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(c: CreateTextSearch) -> Self { + Self::CreateTextSearch(c) + } +} + impl From for Statement { fn from(a: AlterSchema) -> Self { Self::AlterSchema(a) @@ -12104,6 +12123,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(a: AlterTextSearch) -> Self { + Self::AlterTextSearch(a) + } +} + impl From for Statement { fn from(m: Merge) -> Self { Self::Merge(m) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d80a3f4d5..b1b116861 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -389,6 +389,7 @@ impl Spanned for Statement { create_operator_family.span() } Statement::CreateOperatorClass(create_operator_class) => create_operator_class.span(), + Statement::CreateTextSearch(create_text_search) => create_text_search.span(), Statement::AlterTable(alter_table) => alter_table.span(), Statement::AlterIndex { name, operation } => name.span().union(&operation.span()), Statement::AlterView { @@ -407,6 +408,7 @@ impl Spanned for Statement { Statement::AlterOperator { .. } => Span::empty(), Statement::AlterOperatorFamily { .. } => Span::empty(), Statement::AlterOperatorClass { .. } => Span::empty(), + Statement::AlterTextSearch { .. } => Span::empty(), Statement::AlterRole { .. } => Span::empty(), Statement::AlterSession { .. } => Span::empty(), Statement::AttachDatabase { .. } => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index f0f37b1c0..3738303c2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -245,6 +245,7 @@ define_keywords!( COMPUTE, CONCURRENTLY, CONDITION, + CONFIGURATION, CONFLICT, CONNECT, CONNECTION, @@ -330,6 +331,7 @@ define_keywords!( DETACH, DETAIL, DETERMINISTIC, + DICTIONARY, DIMENSIONS, DIRECTORY, DISABLE, @@ -762,6 +764,7 @@ define_keywords!( PARALLEL, PARAMETER, PARQUET, + PARSER, PART, PARTIAL, PARTITION, @@ -1031,6 +1034,7 @@ define_keywords!( TASK, TBLPROPERTIES, TEMP, + TEMPLATE, TEMPORARY, TEMPTABLE, TERMINATED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6282ed3d7..99ae15e7e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5113,6 +5113,8 @@ impl<'a> Parser<'a> { let create_view_params = self.parse_create_view_params()?; if self.peek_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) { self.parse_create_snapshot_table().map(Into::into) + } else if self.peek_keywords(&[Keyword::TEXT, Keyword::SEARCH]) { + self.parse_create_text_search().map(Into::into) } else if self.parse_keyword(Keyword::TABLE) { self.parse_create_table(or_replace, temporary, global, transient) .map(Into::into) @@ -5186,6 +5188,140 @@ impl<'a> Parser<'a> { } } + fn parse_text_search_object_type(&mut self) -> Result { + match self.expect_one_of_keywords(&[ + Keyword::DICTIONARY, + Keyword::CONFIGURATION, + Keyword::TEMPLATE, + Keyword::PARSER, + ])? { + Keyword::DICTIONARY => Ok(TextSearchObjectType::Dictionary), + Keyword::CONFIGURATION => Ok(TextSearchObjectType::Configuration), + Keyword::TEMPLATE => Ok(TextSearchObjectType::Template), + Keyword::PARSER => Ok(TextSearchObjectType::Parser), + // unreachable because expect_one_of_keywords used above + unexpected_keyword => Err(ParserError::ParserError(format!( + "Internal parser error: expected any of {{DICTIONARY, CONFIGURATION, TEMPLATE, PARSER}}, got {unexpected_keyword:?}" + ))), + } + } + + /// Parse a PostgreSQL `CREATE TEXT SEARCH ...` statement. + pub fn parse_create_text_search(&mut self) -> Result { + self.expect_keywords(&[Keyword::TEXT, Keyword::SEARCH])?; + let object_type = self.parse_text_search_object_type()?; + let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(CreateTextSearch { + object_type, + name, + options, + }) + } + + fn parse_alter_text_search_dictionary_option( + &mut self, + ) -> Result { + let key = self.parse_identifier()?; + let value = if self.consume_token(&Token::Eq) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(AlterTextSearchDictionaryOption { key, value }) + } + + /// Parse a PostgreSQL `ALTER TEXT SEARCH ...` statement. + pub fn parse_alter_text_search(&mut self) -> Result { + self.expect_keywords(&[Keyword::TEXT, Keyword::SEARCH])?; + let object_type = self.parse_text_search_object_type()?; + let name = self.parse_object_name(false)?; + + let operation = match object_type { + TextSearchObjectType::Dictionary => { + if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterTextSearchOperation::RenameTo { + new_name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterTextSearchOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterTextSearchOperation::SetSchema { + schema_name: self.parse_object_name(false)?, + } + } else if self.consume_token(&Token::LParen) { + let options = self + .parse_comma_separated(Parser::parse_alter_text_search_dictionary_option)?; + self.expect_token(&Token::RParen)?; + AlterTextSearchOperation::SetOptions { options } + } else { + return self.expected_ref( + "RENAME TO, OWNER TO, SET SCHEMA, or (...) after ALTER TEXT SEARCH DICTIONARY", + self.peek_token_ref(), + ); + } + } + TextSearchObjectType::Configuration => { + if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterTextSearchOperation::RenameTo { + new_name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterTextSearchOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterTextSearchOperation::SetSchema { + schema_name: self.parse_object_name(false)?, + } + } else { + return self.expected_ref( + "RENAME TO, OWNER TO, or SET SCHEMA after ALTER TEXT SEARCH CONFIGURATION", + self.peek_token_ref(), + ); + } + } + TextSearchObjectType::Template => { + if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterTextSearchOperation::RenameTo { + new_name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterTextSearchOperation::SetSchema { + schema_name: self.parse_object_name(false)?, + } + } else { + return self.expected_ref( + "RENAME TO or SET SCHEMA after ALTER TEXT SEARCH TEMPLATE", + self.peek_token_ref(), + ); + } + } + TextSearchObjectType::Parser => { + if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterTextSearchOperation::RenameTo { + new_name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterTextSearchOperation::SetSchema { + schema_name: self.parse_object_name(false)?, + } + } else { + return self.expected_ref( + "RENAME TO or SET SCHEMA after ALTER TEXT SEARCH PARSER", + self.peek_token_ref(), + ); + } + } + }; + + Ok(AlterTextSearch { + object_type, + name, + operation, + }) + } + fn parse_create_user(&mut self, or_replace: bool) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_identifier()?; @@ -10577,6 +10713,10 @@ impl<'a> Parser<'a> { /// Parse an `ALTER ` statement and dispatch to the appropriate alter handler. pub fn parse_alter(&mut self) -> Result { + if self.peek_keywords(&[Keyword::TEXT, Keyword::SEARCH]) { + return self.parse_alter_text_search().map(Into::into); + } + let object_type = self.expect_one_of_keywords(&[ Keyword::VIEW, Keyword::TYPE, @@ -10636,7 +10776,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, USER, OPERATOR, TEXT SEARCH}}, got {unexpected_keyword:?}"), )), } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index af0f2be33..52b40b504 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8060,6 +8060,37 @@ fn parse_alter_operator_class() { .is_err()); } +#[test] +fn parse_create_and_alter_text_search() { + // CREATE — one per object type + pg_and_generic().verified_stmt("CREATE TEXT SEARCH DICTIONARY d (template = simple)"); + pg_and_generic().verified_stmt("CREATE TEXT SEARCH CONFIGURATION c (copy = english)"); + pg_and_generic().verified_stmt("CREATE TEXT SEARCH TEMPLATE t (lexize = dsimple_lexize)"); + pg_and_generic().verified_stmt( + "CREATE TEXT SEARCH PARSER p (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, lextypes = prsd_lextype)", + ); + + // CREATE with quoted option key + pg_and_generic().verified_stmt("CREATE TEXT SEARCH TEMPLATE t (\"Init\" = init_function)"); + + // ALTER — one test per object type arm, one per operation kind + pg_and_generic().verified_stmt("ALTER TEXT SEARCH DICTIONARY d (opt = val)"); + pg_and_generic().verified_stmt("ALTER TEXT SEARCH DICTIONARY d (opt)"); + pg_and_generic().verified_stmt("ALTER TEXT SEARCH CONFIGURATION c OWNER TO some_user"); + pg_and_generic().verified_stmt("ALTER TEXT SEARCH TEMPLATE t SET SCHEMA s"); + pg_and_generic().verified_stmt("ALTER TEXT SEARCH PARSER p RENAME TO p2"); + + // Object type must be an unquoted keyword-like token in this position. + assert!(pg() + .parse_sql_statements("CREATE TEXT SEARCH \"DICTIONARY\" d (template = simple)") + .is_err()); + + // CREATE options are key-value pairs in PostgreSQL syntax. + assert!(pg() + .parse_sql_statements("CREATE TEXT SEARCH DICTIONARY d (template)") + .is_err()); +} + #[test] fn parse_drop_operator_family() { for if_exists in [true, false] {