diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 67aefb392..bc052e11f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5757,3 +5757,133 @@ 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. PostgreSQL requires `PARSER = parser_name`; the + /// parser does not enforce required keys (matching other options-list + /// handling in this crate). + 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. PostgreSQL requires `TEMPLATE = template_name`; the + /// parser does not enforce required keys. + 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. PostgreSQL requires `START`, `GETTOKEN`, `END`, and + /// `LEXTYPES` (with `HEADLINE` optional); the parser does not enforce + /// required keys. + 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. PostgreSQL requires `LEXIZE` (with `INIT` optional); + /// the parser does not enforce required keys. + 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..0c7eaa974 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(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/keywords.rs b/src/keywords.rs index 808e5f03d..3793208ee 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, @@ -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, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7501919a0..810707903 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,52 @@ 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 { + let subtype = if self.parse_keyword(Keyword::CONFIGURATION) { + Keyword::CONFIGURATION + } else if self.parse_keyword(Keyword::DICTIONARY) { + Keyword::DICTIONARY + } else if self.parse_keyword(Keyword::PARSER) { + Keyword::PARSER + } else if self.parse_keyword(Keyword::TEMPLATE) { + Keyword::TEMPLATE + } else { + 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. pub fn parse_drop_extension(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 86315b1ef..a166e2f82 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -947,6 +947,148 @@ 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_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!( + 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!(