Skip to content
68 changes: 68 additions & 0 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5822,3 +5822,71 @@ impl From<AlterPolicy> for crate::ast::Statement {
crate::ast::Statement::AlterPolicy(v)
}
}

/// Kind of object created by a `CREATE TEXT SEARCH` statement.
///
/// Note: this is a PostgreSQL-specific concept.
#[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 {
/// `CREATE TEXT SEARCH CONFIGURATION`
Configuration,
/// `CREATE TEXT SEARCH DICTIONARY`
Dictionary,
/// `CREATE TEXT SEARCH PARSER`
Parser,
/// `CREATE TEXT SEARCH TEMPLATE`
Template,
}

impl fmt::Display for TextSearchObjectType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
TextSearchObjectType::Configuration => "CONFIGURATION",
TextSearchObjectType::Dictionary => "DICTIONARY",
TextSearchObjectType::Parser => "PARSER",
TextSearchObjectType::Template => "TEMPLATE",
})
}
}

/// `CREATE TEXT SEARCH { CONFIGURATION | DICTIONARY | PARSER | TEMPLATE }` statement.
///
/// Note: this is a PostgreSQL-specific statement.
/// - <https://www.postgresql.org/docs/current/sql-createtsconfig.html>
/// - <https://www.postgresql.org/docs/current/sql-createtsdictionary.html>
/// - <https://www.postgresql.org/docs/current/sql-createtsparser.html>
/// - <https://www.postgresql.org/docs/current/sql-createtstemplate.html>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct CreateTextSearch {
/// Kind of text search object being created.
pub kind: TextSearchObjectType,
/// Name of the text search object being created.
pub name: ObjectName,
/// Options list. PostgreSQL requires kind-specific keys (e.g. `PARSER`
/// for configurations, `TEMPLATE` for dictionaries); the parser does
/// not enforce required keys (matching other options-list handling in
/// this crate).
pub options: Vec<SqlOption>,
}

impl fmt::Display for CreateTextSearch {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"CREATE TEXT SEARCH {kind} {name} ({options})",
kind = self.kind,
name = self.name,
options = display_comma_separated(&self.options),
)
}
}

impl Spanned for CreateTextSearch {
fn span(&self) -> Span {
self.name.span()
}
}
39 changes: 28 additions & 11 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,18 @@ 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,
DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily,
DropOperatorSignature, DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs,
GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind,
IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType,
KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem,
OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition,
PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity,
TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef,
UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation,
UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData,
CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial,
DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass,
DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, ForValues,
FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty,
IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn,
IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes,
OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose,
Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind,
ReplicaIdentity, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate,
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
UserDefinedTypeStorage, ViewColumnDef, WithData,
};
pub use self::dml::{
Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr,
Expand Down Expand Up @@ -4007,6 +4008,15 @@ pub enum Statement {
/// <https://www.postgresql.org/docs/current/sql-createcollation.html>
CreateCollation(CreateCollation),
/// ```sql
/// CREATE TEXT SEARCH { CONFIGURATION | DICTIONARY | PARSER | TEMPLATE } name ( option = value [, ...] )
/// ```
/// Note: this is a PostgreSQL-specific statement.
/// - <https://www.postgresql.org/docs/current/sql-createtsconfig.html>
/// - <https://www.postgresql.org/docs/current/sql-createtsdictionary.html>
/// - <https://www.postgresql.org/docs/current/sql-createtsparser.html>
/// - <https://www.postgresql.org/docs/current/sql-createtstemplate.html>
CreateTextSearch(CreateTextSearch),
/// ```sql
/// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ]
/// ```
/// Note: this is a PostgreSQL-specific statement.
Expand Down Expand Up @@ -5489,6 +5499,7 @@ 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::CreateTextSearch(create_text_search) => write!(f, "{create_text_search}"),
Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"),
Statement::DropOperator(drop_operator) => write!(f, "{drop_operator}"),
Statement::DropOperatorFamily(drop_operator_family) => {
Expand Down Expand Up @@ -12101,6 +12112,12 @@ impl From<CreateCollation> for Statement {
}
}

impl From<CreateTextSearch> for Statement {
fn from(c: CreateTextSearch) -> Self {
Self::CreateTextSearch(c)
}
}

impl From<DropExtension> for Statement {
fn from(de: DropExtension) -> Self {
Self::DropExtension(de)
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ 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::CreateTextSearch(stmt) => stmt.span(),
Statement::DropExtension(drop_extension) => drop_extension.span(),
Statement::DropOperator(drop_operator) => drop_operator.span(),
Statement::DropOperatorFamily(drop_operator_family) => drop_operator_family.span(),
Expand Down
4 changes: 4 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ define_keywords!(
COMPUTE,
CONCURRENTLY,
CONDITION,
CONFIGURATION,
CONFLICT,
CONNECT,
CONNECTION,
Expand Down Expand Up @@ -334,6 +335,7 @@ define_keywords!(
DETACH,
DETAIL,
DETERMINISTIC,
DICTIONARY,
DIMENSIONS,
DIRECTORY,
DISABLE,
Expand Down Expand Up @@ -767,6 +769,7 @@ define_keywords!(
PARALLEL,
PARAMETER,
PARQUET,
PARSER,
PART,
PARTIAL,
PARTITION,
Expand Down Expand Up @@ -1037,6 +1040,7 @@ define_keywords!(
TASK,
TBLPROPERTIES,
TEMP,
TEMPLATE,
TEMPORARY,
TEMPTABLE,
TERMINATED,
Expand Down
34 changes: 34 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5214,6 +5214,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())
}
Expand Down Expand Up @@ -8193,6 +8195,38 @@ 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<Statement, ParserError> {
let kind = match self.parse_one_of_keywords(&[
Keyword::CONFIGURATION,
Keyword::DICTIONARY,
Keyword::PARSER,
Keyword::TEMPLATE,
]) {
Some(Keyword::CONFIGURATION) => TextSearchObjectType::Configuration,
Some(Keyword::DICTIONARY) => TextSearchObjectType::Dictionary,
Some(Keyword::PARSER) => TextSearchObjectType::Parser,
Some(Keyword::TEMPLATE) => TextSearchObjectType::Template,
_ => {
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(Statement::CreateTextSearch(CreateTextSearch {
kind,
name,
options,
}))
}

/// Parse a PostgreSQL-specific [Statement::DropExtension] statement.
pub fn parse_drop_extension(&mut self) -> Result<Statement, ParserError> {
let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
Expand Down
151 changes: 151 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,157 @@ fn parse_alter_collation() {
);
}

#[test]
fn parse_create_text_search_configuration() {
assert_eq!(
pg_and_generic()
.verified_stmt("CREATE TEXT SEARCH CONFIGURATION public.myconfig (PARSER = myparser)"),
Statement::CreateTextSearch(CreateTextSearch {
kind: TextSearchObjectType::Configuration,
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_and_generic().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_and_generic().verified_stmt(
"CREATE TEXT SEARCH DICTIONARY public.mydict (TEMPLATE = snowball, language = english)"
),
Statement::CreateTextSearch(CreateTextSearch {
kind: TextSearchObjectType::Dictionary,
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_and_generic().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_and_generic().verified_stmt(
"CREATE TEXT SEARCH PARSER myparser (START = prsd_start, GETTOKEN = prsd_nexttoken, END = prsd_end, LEXTYPES = prsd_lextype, HEADLINE = prsd_headline)"
),
Statement::CreateTextSearch(CreateTextSearch {
kind: TextSearchObjectType::Parser,
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_and_generic()
.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_and_generic().verified_stmt(
"CREATE TEXT SEARCH TEMPLATE mytemplate (INIT = dinit, LEXIZE = dlexize)"
),
Statement::CreateTextSearch(CreateTextSearch {
kind: TextSearchObjectType::Template,
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_and_generic()
.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_and_generic().verified_stmt(
"CREATE TEXT SEARCH CONFIGURATION public.myconfig (PARSER = pg_catalog.default)",
);
pg_and_generic()
.verified_stmt("CREATE TEXT SEARCH DICTIONARY public.d (TEMPLATE = pg_catalog.simple)");
}

#[test]
fn parse_create_text_search_invalid_subtype() {
assert_eq!(
pg_and_generic()
.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!(
Expand Down
Loading