From 33df1663e7c661774cf6ad52bd55644eafd88263 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:10:21 +0100 Subject: [PATCH 1/3] ast: add PostgreSQL text search DDL statement nodes Introduce AST structures for CREATE/ALTER TEXT SEARCH object types\n(dictionary, configuration, template, parser), including display\nimplementations, statement variants, From conversions, and span wiring. --- src/ast/ddl.rs | 145 +++++++++++++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 46 +++++++++++---- src/ast/spans.rs | 2 + 3 files changed, 183 insertions(+), 10 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0c4f93e647..a00c28f529 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5121,6 +5121,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 1e430171ee..b96e5f505f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -64,20 +64,22 @@ 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, 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, - OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, - PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, - TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, + CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTextSearch, 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, OperatorFamilyDropItem, OperatorFamilyItem, + OperatorOption, OperatorPurpose, Owner, Partition, PartitionBoundValue, ProcedureParam, + ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, + TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, }; @@ -3707,6 +3709,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), @@ -3759,6 +3766,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 { @@ -5443,6 +5455,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}") @@ -5472,6 +5485,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}") } @@ -11912,6 +11926,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) @@ -11942,6 +11962,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 0b95c3ed70..949ed584f0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -388,6 +388,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 { @@ -406,6 +407,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(), From ea543eb300b29c51438ae279ffc09bc883c4607e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:10:35 +0100 Subject: [PATCH 2/3] parser: support PostgreSQL CREATE/ALTER TEXT SEARCH DDL Add parser support for CREATE/ALTER TEXT SEARCH DICTIONARY,\nCONFIGURATION, TEMPLATE, and PARSER forms, including operation-specific\nALTER clauses and strict CREATE option parsing.\n\nRegister text-search object names as parser keywords and reject unsupported\nCREATE modifiers for text-search objects. --- src/keywords.rs | 4 ++ src/parser/mod.rs | 154 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/keywords.rs b/src/keywords.rs index cc2b9e9dd0..83243452ff 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -243,6 +243,7 @@ define_keywords!( COMPUTE, CONCURRENTLY, CONDITION, + CONFIGURATION, CONFLICT, CONNECT, CONNECTION, @@ -328,6 +329,7 @@ define_keywords!( DETACH, DETAIL, DETERMINISTIC, + DICTIONARY, DIMENSIONS, DIRECTORY, DISABLE, @@ -756,6 +758,7 @@ define_keywords!( PARALLEL, PARAMETER, PARQUET, + PARSER, PART, PARTIAL, PARTITION, @@ -1023,6 +1026,7 @@ define_keywords!( TASK, TBLPROPERTIES, TEMP, + TEMPLATE, TEMPORARY, TEMPTABLE, TERMINATED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bea566bbe8..a9f0f62757 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5100,7 +5100,14 @@ impl<'a> Parser<'a> { let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); let create_view_params = self.parse_create_view_params()?; - if self.parse_keyword(Keyword::TABLE) { + if self.parse_keywords(&[Keyword::TEXT, Keyword::SEARCH]) { + if or_replace || or_alter || temporary || global.is_some() || transient || persistent { + return Err(ParserError::ParserError( + "CREATE TEXT SEARCH does not support CREATE modifiers".to_string(), + )); + } + 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) } else if self.peek_keyword(Keyword::MATERIALIZED) @@ -5173,6 +5180,145 @@ 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:?}" + ))), + } + } + + fn parse_text_search_option(&mut self) -> Result { + let key = self.parse_identifier()?; + self.expect_token(&Token::Eq)?; + let value = self.parse_expr()?; + Ok(SqlOption::KeyValue { key, value }) + } + + /// Parse a PostgreSQL `CREATE TEXT SEARCH ...` statement. + pub fn parse_create_text_search(&mut self) -> Result { + 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_text_search_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 { + 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()?; @@ -10428,6 +10574,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.parse_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, @@ -10487,7 +10637,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:?}"), )), } } From 2abd77c792c72b6e80bd9e6a0036ce4044fb6ce8 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:10:42 +0100 Subject: [PATCH 3/3] tests: add PostgreSQL text search DDL regression coverage Add regression coverage for the provided CREATE/ALTER TEXT SEARCH\nstatements and guardrails for rejected forms (quoted object type,\nmissing key/value option syntax, and unsupported CREATE modifiers). --- tests/sqlparser_postgres.rs | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7c19f51e5e..1fc7dda6c3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -7932,6 +7932,73 @@ fn parse_alter_operator_class() { .is_err()); } +#[test] +fn parse_create_and_alter_text_search_failure_cases() { + let sql_cases = [ + "CREATE TEXT SEARCH DICTIONARY alt_ts_dict1 (template=simple)", + "CREATE TEXT SEARCH DICTIONARY alt_ts_dict2 (template=simple)", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict1 RENAME TO alt_ts_dict2", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict1 RENAME TO alt_ts_dict3", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict2 OWNER TO regress_alter_generic_user2", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict2 OWNER TO regress_alter_generic_user3", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict2 SET SCHEMA alt_nsp2", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict3 RENAME TO alt_ts_dict4", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict1 RENAME TO alt_ts_dict4", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict3 OWNER TO regress_alter_generic_user2", + "ALTER TEXT SEARCH DICTIONARY alt_ts_dict3 SET SCHEMA alt_nsp2", + "CREATE TEXT SEARCH CONFIGURATION alt_ts_conf1 (copy=english)", + "CREATE TEXT SEARCH CONFIGURATION alt_ts_conf2 (copy=english)", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf1 RENAME TO alt_ts_conf2", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf1 RENAME TO alt_ts_conf3", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf2 OWNER TO regress_alter_generic_user2", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf2 OWNER TO regress_alter_generic_user3", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf2 SET SCHEMA alt_nsp2", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf3 RENAME TO alt_ts_conf4", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf1 RENAME TO alt_ts_conf4", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf3 OWNER TO regress_alter_generic_user2", + "ALTER TEXT SEARCH CONFIGURATION alt_ts_conf3 SET SCHEMA alt_nsp2", + "CREATE TEXT SEARCH TEMPLATE alt_ts_temp1 (lexize=dsimple_lexize)", + "CREATE TEXT SEARCH TEMPLATE alt_ts_temp2 (lexize=dsimple_lexize)", + "ALTER TEXT SEARCH TEMPLATE alt_ts_temp1 RENAME TO alt_ts_temp2", + "ALTER TEXT SEARCH TEMPLATE alt_ts_temp1 RENAME TO alt_ts_temp3", + "ALTER TEXT SEARCH TEMPLATE alt_ts_temp2 SET SCHEMA alt_nsp2", + "CREATE TEXT SEARCH TEMPLATE tstemp_case (\"Init\" = init_function)", + "CREATE TEXT SEARCH PARSER alt_ts_prs1 (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, lextypes = prsd_lextype)", + "CREATE TEXT SEARCH PARSER alt_ts_prs2 (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, lextypes = prsd_lextype)", + "ALTER TEXT SEARCH PARSER alt_ts_prs1 RENAME TO alt_ts_prs2", + "ALTER TEXT SEARCH PARSER alt_ts_prs1 RENAME TO alt_ts_prs3", + "ALTER TEXT SEARCH PARSER alt_ts_prs2 SET SCHEMA alt_nsp2", + "CREATE TEXT SEARCH PARSER tspars_case (\"Start\" = start_function)", + ]; + + for sql in sql_cases { + if let Err(err) = pg().parse_sql_statements(sql) { + panic!("Failed to parse `{sql}`: {err}"); + } + } + + // 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()); + + // CREATE TEXT SEARCH does not support generic CREATE modifiers. + assert!(pg() + .parse_sql_statements("CREATE OR REPLACE TEXT SEARCH DICTIONARY d (template = simple)") + .is_err()); + assert!(pg() + .parse_sql_statements("CREATE OR ALTER TEXT SEARCH DICTIONARY d (template = simple)") + .is_err()); + assert!(pg() + .parse_sql_statements("CREATE TEMP TEXT SEARCH DICTIONARY d (template = simple)") + .is_err()); +} + #[test] fn parse_drop_operator_family() { for if_exists in [true, false] {