diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0a93f5b7ec..98fc9e3d22 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5627,6 +5627,161 @@ impl Spanned for AlterFunction { } } +/// Text search object kind. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-intro.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 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"), + } + } +} + +/// `CREATE TEXT SEARCH ...` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtsdictionary.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 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 ... ( ... )`. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertsdictionary.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 AlterTextSearchOption { + /// Option name. + pub key: Ident, + /// Optional value (`option [= value]`). + pub value: Option, +} + +impl fmt::Display for AlterTextSearchOption { + 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 `ALTER TEXT SEARCH ...`. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertsdictionary.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 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 { + /// Text search 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)) + } + } + } +} + +/// `ALTER TEXT SEARCH ...` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertsdictionary.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 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 c0826f2008..21faab3e7c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -65,23 +65,25 @@ pub use self::ddl::{ AlterIndexOperation, AlterOperator, 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, CreateCollation, CreateCollationDefinition, - CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, + AlterTableLock, AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchOperation, + AlterTextSearchOption, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, + ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, + ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, 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, WithData, + 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, WithData, }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, @@ -3755,6 +3757,10 @@ pub enum Statement { /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createopclass.html) CreateOperatorClass(CreateOperatorClass), + /// A `CREATE TEXT SEARCH` statement. + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-intro.html) + CreateTextSearch(CreateTextSearch), /// ```sql /// ALTER TABLE /// ``` @@ -3819,6 +3825,10 @@ pub enum Statement { /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alteropclass.html) AlterOperatorClass(AlterOperatorClass), + /// An `ALTER TEXT SEARCH` statement. + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-configuration.html) + AlterTextSearch(AlterTextSearch), /// ```sql /// ALTER ROLE /// ``` @@ -5543,6 +5553,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}") @@ -5574,6 +5585,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}") } @@ -12197,6 +12209,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) @@ -12239,6 +12257,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 f6ba895478..a89782f3b2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -399,6 +399,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 { @@ -419,6 +420,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 a0a65be689..0e074e192d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -246,6 +246,7 @@ define_keywords!( COMPUTE, CONCURRENTLY, CONDITION, + CONFIGURATION, CONFLICT, CONNECT, CONNECTION, @@ -333,6 +334,7 @@ define_keywords!( DETACH, DETAIL, DETERMINISTIC, + DICTIONARY, DIMENSIONS, DIRECTORY, DISABLE, @@ -766,6 +768,7 @@ define_keywords!( PARALLEL, PARAMETER, PARQUET, + PARSER, PART, PARTIAL, PARTITION, @@ -1036,6 +1039,7 @@ define_keywords!( TASK, TBLPROPERTIES, TEMP, + TEMPLATE, TEMPORARY, TEMPTABLE, TERMINATED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 07497b04f6..5631585728 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5144,6 +5144,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, volatile, multiset) .map(Into::into) @@ -5219,6 +5221,80 @@ 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), + unexpected_keyword => Err(ParserError::ParserError(format!( + "Internal parser error: expected any of {{DICTIONARY, CONFIGURATION, TEMPLATE, PARSER}}, got {unexpected_keyword:?}" + ))), + } + } + + /// Parse a `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_option(&mut self) -> Result { + let key = self.parse_identifier()?; + let value = if self.consume_token(&Token::Eq) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(AlterTextSearchOption { key, value }) + } + + /// Parse an `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 = 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_option)?; + self.expect_token(&Token::RParen)?; + AlterTextSearchOperation::SetOptions { options } + } else { + let expected = "RENAME TO, OWNER TO, SET SCHEMA, or (...) after ALTER TEXT SEARCH"; + return self.expected_ref(expected, 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()?; @@ -10797,6 +10873,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, @@ -10862,7 +10942,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, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), + format!("Internal parser error: expected any of {{TEXT SEARCH, VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), )), } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 11e76dbc7c..3595a53ce0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8495,6 +8495,59 @@ fn parse_alter_function_and_aggregate() { .is_err()); } +#[test] +fn parse_create_text_search() { + // CREATE: one per object type + let stmt = + pg_and_generic().verified_stmt("CREATE TEXT SEARCH DICTIONARY d (template = simple)"); + assert_eq!(Span::empty(), stmt.span()); + 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(r#"CREATE TEXT SEARCH TEMPLATE t ("Init" = init_function)"#); + + // Object type must be an unquoted keyword-like token in this position. + assert!(pg() + .parse_sql_statements(r#"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_alter_text_search() { + // One test per operation kind. + let stmt = pg_and_generic().verified_stmt("ALTER TEXT SEARCH DICTIONARY d (opt = val)"); + assert_eq!(Span::empty(), stmt.span()); + if let Statement::AlterTextSearch(alter_text_search) = stmt { + assert_eq!(Span::empty(), alter_text_search.span()); + } else { + unreachable!("expected ALTER TEXT SEARCH statement"); + } + 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"); + + // The parser accepts text search operations permissively across object types. + pg_and_generic().verified_stmt("ALTER TEXT SEARCH TEMPLATE t OWNER TO some_user"); + pg_and_generic().verified_stmt("ALTER TEXT SEARCH PARSER p (opt = val)"); + + let err = pg() + .parse_sql_statements("ALTER TEXT SEARCH DICTIONARY d RESET foo") + .unwrap_err(); + assert!(err + .to_string() + .contains("RENAME TO, OWNER TO, SET SCHEMA, or (...) after ALTER TEXT SEARCH")); +} + #[test] fn parse_drop_operator_family() { for if_exists in [true, false] {