From cb2aeb8cec91c9ae7d53f73e310ce8e6cbe31a06 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:10:21 +0100 Subject: [PATCH 1/5] 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 0a93f5b7ec..2fb45f2f0b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5627,6 +5627,151 @@ impl Spanned for AlterFunction { } } +/// 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 c0826f2008..4450ed0d23 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -65,21 +65,23 @@ pub use self::ddl::{ AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, - AlterTableLock, AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, + AlterTableLock, AlterTableOperation, AlterTableType, AlterTextSearch, + AlterTextSearchDictionaryOption, AlterTextSearchOperation, 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, + 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, }; @@ -3756,6 +3758,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), @@ -3820,6 +3827,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 { @@ -5543,6 +5555,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 +5587,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 +12211,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 +12259,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(), From 29d49fa775ea0d59dcee37e456ebe01165c52011 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:10:35 +0100 Subject: [PATCH 2/5] 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 | 152 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 1 deletion(-) 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..dbe4aeed1d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5144,6 +5144,13 @@ 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.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, volatile, multiset) .map(Into::into) @@ -5219,6 +5226,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()?; @@ -10797,6 +10943,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, @@ -10862,7 +11012,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:?}"), )), } } From 93b95ef96d6f05037023099479f96be309ec2cba Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:10:42 +0100 Subject: [PATCH 3/5] 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 11e76dbc7c..1fe0e6596b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8495,6 +8495,73 @@ fn parse_alter_function_and_aggregate() { .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] { From 36319dfb819efc8bfd3a07228cec03a85e3d071a Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sun, 29 Mar 2026 23:02:09 +0200 Subject: [PATCH 4/5] Addressed reviewer concerns and extended test coveraged checked with tarpaulin --- src/ast/mod.rs | 18 +++++----- src/parser/mod.rs | 20 +++-------- tests/sqlparser_postgres.rs | 70 +++++++++---------------------------- 3 files changed, 31 insertions(+), 77 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4450ed0d23..6d37cd6de5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -75,15 +75,15 @@ pub use self::ddl::{ 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, + 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, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dbe4aeed1d..c428487a18 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5144,12 +5144,7 @@ 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.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(), - )); - } + } 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) @@ -5244,19 +5239,13 @@ impl<'a> Parser<'a> { } } - 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 { + 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_text_search_option)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; self.expect_token(&Token::RParen)?; Ok(CreateTextSearch { object_type, @@ -5279,6 +5268,7 @@ impl<'a> Parser<'a> { /// 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)?; @@ -10943,7 +10933,7 @@ 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]) { + if self.peek_keywords(&[Keyword::TEXT, Keyword::SEARCH]) { return self.parse_alter_text_search().map(Into::into); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1fe0e6596b..b5ddac8b1b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8496,49 +8496,24 @@ fn parse_alter_function_and_aggregate() { } #[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)", - ]; +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)", + ); - for sql in sql_cases { - if let Err(err) = pg().parse_sql_statements(sql) { - panic!("Failed to parse `{sql}`: {err}"); - } - } + // 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() @@ -8549,17 +8524,6 @@ fn parse_create_and_alter_text_search_failure_cases() { 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] From 8bb555107fc23bacdc0fcc3eb2f736c61832a031 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Thu, 7 May 2026 23:30:46 +0200 Subject: [PATCH 5/5] Address text search DDL review feedback --- src/ast/ddl.rs | 28 +++++++---- src/ast/mod.rs | 22 ++++----- src/parser/mod.rs | 98 +++++++------------------------------ tests/sqlparser_postgres.rs | 46 ++++++++++++----- 4 files changed, 82 insertions(+), 112 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 2fb45f2f0b..98fc9e3d22 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5627,7 +5627,9 @@ impl Spanned for AlterFunction { } } -/// PostgreSQL text search object kind. +/// 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))] @@ -5653,7 +5655,9 @@ impl fmt::Display for TextSearchObjectType { } } -/// PostgreSQL `CREATE TEXT SEARCH ...` statement. +/// `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))] @@ -5684,18 +5688,20 @@ impl Spanned for CreateTextSearch { } } -/// Option assignment used by `ALTER TEXT SEARCH DICTIONARY ... ( ... )`. +/// 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 AlterTextSearchDictionaryOption { +pub struct AlterTextSearchOption { /// Option name. pub key: Ident, /// Optional value (`option [= value]`). pub value: Option, } -impl fmt::Display for AlterTextSearchDictionaryOption { +impl fmt::Display for AlterTextSearchOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.value { Some(value) => write!(f, "{} = {}", self.key, value), @@ -5704,7 +5710,9 @@ impl fmt::Display for AlterTextSearchDictionaryOption { } } -/// Operation for PostgreSQL `ALTER TEXT SEARCH ...`. +/// 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))] @@ -5723,8 +5731,8 @@ pub enum AlterTextSearchOperation { }, /// `( option [= value] [, ...] )` SetOptions { - /// Dictionary options to apply. - options: Vec, + /// Text search options to apply. + options: Vec, }, } @@ -5743,7 +5751,9 @@ impl fmt::Display for AlterTextSearchOperation { } } -/// PostgreSQL `ALTER TEXT SEARCH ...` statement. +/// `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))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6d37cd6de5..21faab3e7c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -65,12 +65,12 @@ pub use self::ddl::{ AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, - AlterTableLock, AlterTableOperation, AlterTableType, AlterTextSearch, - AlterTextSearchDictionaryOption, AlterTextSearchOperation, 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, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, @@ -3757,9 +3757,8 @@ pub enum Statement { /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createopclass.html) CreateOperatorClass(CreateOperatorClass), - /// ```sql - /// CREATE TEXT SEARCH { DICTIONARY | CONFIGURATION | TEMPLATE | PARSER } - /// ``` + /// A `CREATE TEXT SEARCH` statement. + /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-intro.html) CreateTextSearch(CreateTextSearch), /// ```sql @@ -3826,9 +3825,8 @@ pub enum Statement { /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alteropclass.html) AlterOperatorClass(AlterOperatorClass), - /// ```sql - /// ALTER TEXT SEARCH { DICTIONARY | CONFIGURATION | TEMPLATE | PARSER } - /// ``` + /// An `ALTER TEXT SEARCH` statement. + /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-configuration.html) AlterTextSearch(AlterTextSearch), /// ```sql diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c428487a18..5631585728 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5232,14 +5232,13 @@ impl<'a> Parser<'a> { 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. + /// 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()?; @@ -5254,98 +5253,39 @@ impl<'a> Parser<'a> { }) } - fn parse_alter_text_search_dictionary_option( - &mut self, - ) -> Result { + 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(AlterTextSearchDictionaryOption { key, value }) + Ok(AlterTextSearchOption { key, value }) } - /// Parse a PostgreSQL `ALTER TEXT SEARCH ...` statement. + /// 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 = 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(), - ); - } + let operation = if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterTextSearchOperation::RenameTo { + new_name: self.parse_identifier()?, } - 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(), - ); - } + } 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 { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b5ddac8b1b..3595a53ce0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8496,9 +8496,11 @@ fn parse_alter_function_and_aggregate() { } #[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)"); +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( @@ -8506,18 +8508,11 @@ fn parse_create_and_alter_text_search() { ); // 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"); + 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("CREATE TEXT SEARCH \"DICTIONARY\" d (template = simple)") + .parse_sql_statements(r#"CREATE TEXT SEARCH "DICTIONARY" d (template = simple)"#) .is_err()); // CREATE options are key-value pairs in PostgreSQL syntax. @@ -8526,6 +8521,33 @@ fn parse_create_and_alter_text_search() { .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] {