From 089a07bba8b16dfa47edd4eaf08d05a0787caa4a Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 17 Apr 2026 11:22:00 +0900 Subject: [PATCH 1/6] feat(ast): add CREATE TEXT SEARCH CONFIGURATION/DICTIONARY/PARSER/TEMPLATE types --- src/ast/ddl.rs | 124 +++++++++++++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 31 +++++++++++- src/ast/spans.rs | 8 +++ src/keywords.rs | 4 ++ 4 files changed, 166 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 67aefb392..cf3be004c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5757,3 +5757,127 @@ impl From for crate::ast::Statement { crate::ast::Statement::AlterPolicy(v) } } + +/// `CREATE TEXT SEARCH CONFIGURATION` statement. +/// +/// Note: this is a PostgreSQL-specific 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 CreateTextSearchConfiguration { + /// Name of the text search configuration being created. + pub name: ObjectName, + /// Options list — must include `PARSER = parser_name`. + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchConfiguration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH CONFIGURATION {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchConfiguration) -> Self { + crate::ast::Statement::CreateTextSearchConfiguration(v) + } +} + +/// `CREATE TEXT SEARCH DICTIONARY` statement. +/// +/// Note: this is a PostgreSQL-specific 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 CreateTextSearchDictionary { + /// Name of the text search dictionary being created. + pub name: ObjectName, + /// Options list — must include `TEMPLATE = template_name`. + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchDictionary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH DICTIONARY {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchDictionary) -> Self { + crate::ast::Statement::CreateTextSearchDictionary(v) + } +} + +/// `CREATE TEXT SEARCH PARSER` statement. +/// +/// Note: this is a PostgreSQL-specific 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 CreateTextSearchParser { + /// Name of the text search parser being created. + pub name: ObjectName, + /// Options list — must include `START`, `GETTOKEN`, `END`, `LEXTYPES` (and optionally `HEADLINE`). + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchParser { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH PARSER {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchParser) -> Self { + crate::ast::Statement::CreateTextSearchParser(v) + } +} + +/// `CREATE TEXT SEARCH TEMPLATE` statement. +/// +/// Note: this is a PostgreSQL-specific 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 CreateTextSearchTemplate { + /// Name of the text search template being created. + pub name: ObjectName, + /// Options list — must include `LEXIZE` (and optionally `INIT`). + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchTemplate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH TEMPLATE {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchTemplate) -> Self { + crate::ast::Statement::CreateTextSearchTemplate(v) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 886bea26d..64bea70b8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -71,7 +71,8 @@ pub use self::ddl::{ ColumnPolicyProperty, ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, - CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, + CreateTable, CreateTextSearchConfiguration, CreateTextSearchDictionary, CreateTextSearchParser, + CreateTextSearchTemplate, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, @@ -4013,6 +4014,30 @@ pub enum Statement { /// CreateCollation(CreateCollation), /// ```sql + /// CREATE TEXT SEARCH CONFIGURATION name ( PARSER = parser_name ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchConfiguration(CreateTextSearchConfiguration), + /// ```sql + /// CREATE TEXT SEARCH DICTIONARY name ( TEMPLATE = template_name [, option = value, ...] ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchDictionary(CreateTextSearchDictionary), + /// ```sql + /// CREATE TEXT SEARCH PARSER name ( START = start_fn, GETTOKEN = gettoken_fn, END = end_fn, LEXTYPES = lextypes_fn [, HEADLINE = headline_fn] ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchParser(CreateTextSearchParser), + /// ```sql + /// CREATE TEXT SEARCH TEMPLATE name ( [INIT = init_fn,] LEXIZE = lexize_fn ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchTemplate(CreateTextSearchTemplate), + /// ```sql /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] /// ``` /// Note: this is a PostgreSQL-specific statement. @@ -5495,6 +5520,10 @@ impl fmt::Display for Statement { Statement::CreateIndex(create_index) => create_index.fmt(f), Statement::CreateExtension(create_extension) => write!(f, "{create_extension}"), Statement::CreateCollation(create_collation) => write!(f, "{create_collation}"), + Statement::CreateTextSearchConfiguration(v) => write!(f, "{v}"), + Statement::CreateTextSearchDictionary(v) => write!(f, "{v}"), + Statement::CreateTextSearchParser(v) => write!(f, "{v}"), + Statement::CreateTextSearchTemplate(v) => write!(f, "{v}"), Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"), Statement::DropOperator(drop_operator) => write!(f, "{drop_operator}"), Statement::DropOperatorFamily(drop_operator_family) => { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index adc1443fc..edb582c47 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -272,6 +272,10 @@ impl Spanned for Values { /// - [Statement::Declare] /// - [Statement::CreateExtension] /// - [Statement::CreateCollation] +/// - [Statement::CreateTextSearchConfiguration] +/// - [Statement::CreateTextSearchDictionary] +/// - [Statement::CreateTextSearchParser] +/// - [Statement::CreateTextSearchTemplate] /// - [Statement::AlterCollation] /// - [Statement::Fetch] /// - [Statement::Flush] @@ -387,6 +391,10 @@ impl Spanned for Statement { Statement::CreateRole(create_role) => create_role.span(), Statement::CreateExtension(create_extension) => create_extension.span(), Statement::CreateCollation(create_collation) => create_collation.span(), + Statement::CreateTextSearchConfiguration(_) => Span::empty(), + Statement::CreateTextSearchDictionary(_) => Span::empty(), + Statement::CreateTextSearchParser(_) => Span::empty(), + Statement::CreateTextSearchTemplate(_) => Span::empty(), Statement::DropExtension(drop_extension) => drop_extension.span(), Statement::DropOperator(drop_operator) => drop_operator.span(), Statement::DropOperatorFamily(drop_operator_family) => drop_operator_family.span(), diff --git a/src/keywords.rs b/src/keywords.rs index 808e5f03d..6d79fac0e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -244,6 +244,7 @@ define_keywords!( COMPRESSION, COMPUPDATE, COMPUTE, + CONFIGURATION, CONCURRENTLY, CONDITION, CONFLICT, @@ -333,6 +334,7 @@ define_keywords!( DETACH, DETAIL, DETERMINISTIC, + DICTIONARY, DIMENSIONS, DIRECTORY, DISABLE, @@ -765,6 +767,7 @@ define_keywords!( PARALLEL, PARAMETER, PARQUET, + PARSER, PART, PARTIAL, PARTITION, @@ -1035,6 +1038,7 @@ define_keywords!( TASK, TBLPROPERTIES, TEMP, + TEMPLATE, TEMPORARY, TEMPTABLE, TERMINATED, From b5fe68c663afeaeedbc81defd8f43aa8a462fdb8 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 17 Apr 2026 11:22:04 +0900 Subject: [PATCH 2/6] feat(parser): parse CREATE TEXT SEARCH statements --- src/parser/mod.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7501919a0..9e17e9334 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5212,6 +5212,8 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::SERVER) { self.parse_pg_create_server() + } else if self.parse_keywords(&[Keyword::TEXT, Keyword::SEARCH]) { + self.parse_create_text_search() } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) } @@ -8176,6 +8178,49 @@ impl<'a> Parser<'a> { }) } + /// Parse a PostgreSQL-specific `CREATE TEXT SEARCH CONFIGURATION | DICTIONARY | PARSER | TEMPLATE` statement. + pub fn parse_create_text_search(&mut self) -> Result { + if self.parse_keyword(Keyword::CONFIGURATION) { + 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(Statement::CreateTextSearchConfiguration( + CreateTextSearchConfiguration { name, options }, + )) + } else if self.parse_keyword(Keyword::DICTIONARY) { + 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(Statement::CreateTextSearchDictionary( + CreateTextSearchDictionary { name, options }, + )) + } else if self.parse_keyword(Keyword::PARSER) { + 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(Statement::CreateTextSearchParser(CreateTextSearchParser { + name, + options, + })) + } else if self.parse_keyword(Keyword::TEMPLATE) { + 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(Statement::CreateTextSearchTemplate( + CreateTextSearchTemplate { name, options }, + )) + } else { + self.expected_ref( + "CONFIGURATION, DICTIONARY, PARSER, or TEMPLATE after CREATE TEXT SEARCH", + self.peek_token_ref(), + ) + } + } + /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. pub fn parse_drop_extension(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); From 7a5450ace9e7b5ee408f82bece9a2a07e7eaf04c Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 17 Apr 2026 11:22:08 +0900 Subject: [PATCH 3/6] test: add CREATE TEXT SEARCH round-trip tests --- tests/sqlparser_postgres.rs | 132 ++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 86315b1ef..f401624b1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -947,6 +947,138 @@ fn parse_alter_collation() { ); } +#[test] +fn parse_create_text_search_configuration() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH CONFIGURATION public.myconfig (PARSER = myparser)" + ), + Statement::CreateTextSearchConfiguration(CreateTextSearchConfiguration { + name: ObjectName::from(vec![Ident::new("public"), Ident::new("myconfig")]), + options: vec![SqlOption::KeyValue { + key: Ident::new("PARSER"), + value: Expr::Identifier(Ident::new("myparser")), + }], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH CONFIGURATION myconfig PARSER = pg_catalog.default"), + Err(ParserError::ParserError( + "Expected: (, found: PARSER".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_dictionary() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH DICTIONARY public.mydict (TEMPLATE = snowball, language = english)" + ), + Statement::CreateTextSearchDictionary(CreateTextSearchDictionary { + name: ObjectName::from(vec![Ident::new("public"), Ident::new("mydict")]), + options: vec![ + SqlOption::KeyValue { + key: Ident::new("TEMPLATE"), + value: Expr::Identifier(Ident::new("snowball")), + }, + SqlOption::KeyValue { + key: Ident::new("language"), + value: Expr::Identifier(Ident::new("english")), + }, + ], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH DICTIONARY mydict"), + Err(ParserError::ParserError( + "Expected: (, found: EOF".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_parser() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH PARSER myparser (START = prsd_start, GETTOKEN = prsd_nexttoken, END = prsd_end, LEXTYPES = prsd_lextype, HEADLINE = prsd_headline)" + ), + Statement::CreateTextSearchParser(CreateTextSearchParser { + name: ObjectName::from(vec![Ident::new("myparser")]), + options: vec![ + SqlOption::KeyValue { + key: Ident::new("START"), + value: Expr::Identifier(Ident::new("prsd_start")), + }, + SqlOption::KeyValue { + key: Ident::new("GETTOKEN"), + value: Expr::Identifier(Ident::new("prsd_nexttoken")), + }, + SqlOption::KeyValue { + key: Ident::new("END"), + value: Expr::Identifier(Ident::new("prsd_end")), + }, + SqlOption::KeyValue { + key: Ident::new("LEXTYPES"), + value: Expr::Identifier(Ident::new("prsd_lextype")), + }, + SqlOption::KeyValue { + key: Ident::new("HEADLINE"), + value: Expr::Identifier(Ident::new("prsd_headline")), + }, + ], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH PARSER myparser START = prsd_start"), + Err(ParserError::ParserError( + "Expected: (, found: START".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_template() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH TEMPLATE mytemplate (INIT = dinit, LEXIZE = dlexize)" + ), + Statement::CreateTextSearchTemplate(CreateTextSearchTemplate { + name: ObjectName::from(vec![Ident::new("mytemplate")]), + options: vec![ + SqlOption::KeyValue { + key: Ident::new("INIT"), + value: Expr::Identifier(Ident::new("dinit")), + }, + SqlOption::KeyValue { + key: Ident::new("LEXIZE"), + value: Expr::Identifier(Ident::new("dlexize")), + }, + ], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH TEMPLATE mytemplate LEXIZE = dlexize"), + Err(ParserError::ParserError( + "Expected: (, found: LEXIZE".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_invalid_subtype() { + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH UNKNOWN myname (option = value)"), + Err(ParserError::ParserError( + "Expected: CONFIGURATION, DICTIONARY, PARSER, or TEMPLATE after CREATE TEXT SEARCH, found: UNKNOWN".to_string() + )) + ); +} + #[test] fn parse_drop_and_comment_collation_ast() { assert_eq!( From e568fead8acaf76d977a875c9a7686f173afb7e9 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 18 Apr 2026 18:17:56 +0900 Subject: [PATCH 4/6] chore: apply cargo fmt and sort new keywords --- src/keywords.rs | 2 +- tests/sqlparser_postgres.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keywords.rs b/src/keywords.rs index 6d79fac0e..3793208ee 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -244,9 +244,9 @@ define_keywords!( COMPRESSION, COMPUPDATE, COMPUTE, - CONFIGURATION, CONCURRENTLY, CONDITION, + CONFIGURATION, CONFLICT, CONNECT, CONNECTION, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index f401624b1..50cef1fcd 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -950,9 +950,7 @@ fn parse_alter_collation() { #[test] fn parse_create_text_search_configuration() { assert_eq!( - pg().verified_stmt( - "CREATE TEXT SEARCH CONFIGURATION public.myconfig (PARSER = myparser)" - ), + pg().verified_stmt("CREATE TEXT SEARCH CONFIGURATION public.myconfig (PARSER = myparser)"), Statement::CreateTextSearchConfiguration(CreateTextSearchConfiguration { name: ObjectName::from(vec![Ident::new("public"), Ident::new("myconfig")]), options: vec![SqlOption::KeyValue { @@ -963,7 +961,9 @@ fn parse_create_text_search_configuration() { ); assert_eq!( - pg().parse_sql_statements("CREATE TEXT SEARCH CONFIGURATION myconfig PARSER = pg_catalog.default"), + pg().parse_sql_statements( + "CREATE TEXT SEARCH CONFIGURATION myconfig PARSER = pg_catalog.default" + ), Err(ParserError::ParserError( "Expected: (, found: PARSER".to_string() )) From d8210883a6b35bba282d791d3eada779e063d772 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 18 Apr 2026 20:20:43 +0900 Subject: [PATCH 5/6] review: collapse duplicate parse arms, real spans, honest doc comments - Collapse the four near-identical parse arms in parse_create_text_search; parse the subtype keyword once, then parse name and options once, then map the subtype to the Statement variant. - Spanned impls now return the name's span instead of Span::empty(), matching sibling Create* statements. - Doc comments on each `options: Vec` no longer claim the parser enforces required keys; they note PostgreSQL's requirements and clarify that enforcement is left to the engine. - Add a test exercising schema-qualified option values (e.g. PARSER = pg_catalog.default) to guard the round-trip. --- src/ast/ddl.rs | 14 +++++--- src/ast/spans.rs | 8 ++--- src/parser/mod.rs | 66 ++++++++++++++++++------------------- tests/sqlparser_postgres.rs | 10 ++++++ 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index cf3be004c..bc052e11f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5768,7 +5768,9 @@ impl From for crate::ast::Statement { pub struct CreateTextSearchConfiguration { /// Name of the text search configuration being created. pub name: ObjectName, - /// Options list — must include `PARSER = parser_name`. + /// Options list. PostgreSQL requires `PARSER = parser_name`; the + /// parser does not enforce required keys (matching other options-list + /// handling in this crate). pub options: Vec, } @@ -5799,7 +5801,8 @@ impl From for crate::ast::Statement { pub struct CreateTextSearchDictionary { /// Name of the text search dictionary being created. pub name: ObjectName, - /// Options list — must include `TEMPLATE = template_name`. + /// Options list. PostgreSQL requires `TEMPLATE = template_name`; the + /// parser does not enforce required keys. pub options: Vec, } @@ -5830,7 +5833,9 @@ impl From for crate::ast::Statement { pub struct CreateTextSearchParser { /// Name of the text search parser being created. pub name: ObjectName, - /// Options list — must include `START`, `GETTOKEN`, `END`, `LEXTYPES` (and optionally `HEADLINE`). + /// Options list. PostgreSQL requires `START`, `GETTOKEN`, `END`, and + /// `LEXTYPES` (with `HEADLINE` optional); the parser does not enforce + /// required keys. pub options: Vec, } @@ -5861,7 +5866,8 @@ impl From for crate::ast::Statement { pub struct CreateTextSearchTemplate { /// Name of the text search template being created. pub name: ObjectName, - /// Options list — must include `LEXIZE` (and optionally `INIT`). + /// Options list. PostgreSQL requires `LEXIZE` (with `INIT` optional); + /// the parser does not enforce required keys. pub options: Vec, } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index edb582c47..0c7eaa974 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -391,10 +391,10 @@ impl Spanned for Statement { Statement::CreateRole(create_role) => create_role.span(), Statement::CreateExtension(create_extension) => create_extension.span(), Statement::CreateCollation(create_collation) => create_collation.span(), - Statement::CreateTextSearchConfiguration(_) => Span::empty(), - Statement::CreateTextSearchDictionary(_) => Span::empty(), - Statement::CreateTextSearchParser(_) => Span::empty(), - Statement::CreateTextSearchTemplate(_) => Span::empty(), + Statement::CreateTextSearchConfiguration(stmt) => stmt.name.span(), + Statement::CreateTextSearchDictionary(stmt) => stmt.name.span(), + Statement::CreateTextSearchParser(stmt) => stmt.name.span(), + Statement::CreateTextSearchTemplate(stmt) => stmt.name.span(), Statement::DropExtension(drop_extension) => drop_extension.span(), Statement::DropOperator(drop_operator) => drop_operator.span(), Statement::DropOperatorFamily(drop_operator_family) => drop_operator_family.span(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9e17e9334..c405be7d3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8180,45 +8180,45 @@ impl<'a> Parser<'a> { /// Parse a PostgreSQL-specific `CREATE TEXT SEARCH CONFIGURATION | DICTIONARY | PARSER | TEMPLATE` statement. pub fn parse_create_text_search(&mut self) -> Result { - if self.parse_keyword(Keyword::CONFIGURATION) { - 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(Statement::CreateTextSearchConfiguration( - CreateTextSearchConfiguration { name, options }, - )) + let subtype = if self.parse_keyword(Keyword::CONFIGURATION) { + Keyword::CONFIGURATION } else if self.parse_keyword(Keyword::DICTIONARY) { - 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(Statement::CreateTextSearchDictionary( - CreateTextSearchDictionary { name, options }, - )) + Keyword::DICTIONARY } else if self.parse_keyword(Keyword::PARSER) { - 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(Statement::CreateTextSearchParser(CreateTextSearchParser { - name, - options, - })) + Keyword::PARSER } else if self.parse_keyword(Keyword::TEMPLATE) { - 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(Statement::CreateTextSearchTemplate( - CreateTextSearchTemplate { name, options }, - )) + Keyword::TEMPLATE } else { - self.expected_ref( + return self.expected_ref( "CONFIGURATION, DICTIONARY, PARSER, or TEMPLATE after CREATE TEXT SEARCH", self.peek_token_ref(), - ) - } + ); + }; + + 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(match subtype { + Keyword::CONFIGURATION => Statement::CreateTextSearchConfiguration( + CreateTextSearchConfiguration { name, options }, + ), + Keyword::DICTIONARY => { + Statement::CreateTextSearchDictionary(CreateTextSearchDictionary { name, options }) + } + Keyword::PARSER => { + Statement::CreateTextSearchParser(CreateTextSearchParser { name, options }) + } + Keyword::TEMPLATE => { + Statement::CreateTextSearchTemplate(CreateTextSearchTemplate { name, options }) + } + unexpected => { + return Err(ParserError::ParserError(format!( + "Internal parser error: unexpected CREATE TEXT SEARCH subtype `{unexpected}`" + ))) + } + }) } /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 50cef1fcd..a166e2f82 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1069,6 +1069,16 @@ fn parse_create_text_search_template() { ); } +#[test] +fn parse_create_text_search_schema_qualified_option_value() { + // PostgreSQL's TEXT SEARCH options accept schema-qualified names as + // values (e.g. `PARSER = pg_catalog.default`). Ensure they round-trip. + pg().verified_stmt( + "CREATE TEXT SEARCH CONFIGURATION public.myconfig (PARSER = pg_catalog.default)", + ); + pg().verified_stmt("CREATE TEXT SEARCH DICTIONARY public.d (TEMPLATE = pg_catalog.simple)"); +} + #[test] fn parse_create_text_search_invalid_subtype() { assert_eq!( From 6b56694b0a2caa9554557e18ffaffaff1641ffe0 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 18 Apr 2026 20:24:09 +0900 Subject: [PATCH 6/6] chore: cargo fmt --- src/parser/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c405be7d3..810707903 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8201,9 +8201,12 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(match subtype { - Keyword::CONFIGURATION => Statement::CreateTextSearchConfiguration( - CreateTextSearchConfiguration { name, options }, - ), + Keyword::CONFIGURATION => { + Statement::CreateTextSearchConfiguration(CreateTextSearchConfiguration { + name, + options, + }) + } Keyword::DICTIONARY => { Statement::CreateTextSearchDictionary(CreateTextSearchDictionary { name, options }) }