diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index ece602373..d8c0e0455 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5994,3 +5994,165 @@ impl From for crate::ast::Statement { crate::ast::Statement::AlterPolicy(v) } } + +/// CREATE AGGREGATE statement. +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateAggregate { + /// True if `OR REPLACE` was specified. + pub or_replace: bool, + /// The aggregate name (can be schema-qualified). + pub name: ObjectName, + /// The argument list preceding the options list. + pub args: CreateAggregateArgs, + /// The options listed inside the required parentheses after the argument + /// list (e.g. `SFUNC`, `STYPE`, `FINALFUNC`, `PARALLEL`, …). + pub options: Vec, +} + +impl fmt::Display for CreateAggregate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE")?; + if self.or_replace { + write!(f, " OR REPLACE")?; + } + write!(f, " AGGREGATE {}", self.name)?; + match &self.args { + CreateAggregateArgs::Legacy => {} + CreateAggregateArgs::Star => write!(f, " (*)")?, + CreateAggregateArgs::List(args) => write!(f, " ({})", display_comma_separated(args))?, + } + write!(f, " ({})", display_comma_separated(&self.options)) + } +} + +/// The argument list of a [`CreateAggregate`] statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateAggregateArgs { + /// No argument-list parentheses: the historical form that packs everything, + /// usually including a `BASETYPE` option, into a single list. + Legacy, + /// The wildcard form `(*)`, used by zero-argument aggregates such as + /// `count(*)`. + Star, + /// An explicit argument list, possibly empty: `()`, `(NUMERIC)`, + /// `(input INT, VARIADIC tail TEXT)`. + List(Vec), +} + +impl From for crate::ast::Statement { + fn from(v: CreateAggregate) -> Self { + crate::ast::Statement::CreateAggregate(v) + } +} + +/// A single option in a `CREATE AGGREGATE` options list. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateAggregateOption { + /// `SFUNC = state_transition_function` + StateTransitionFunction(ObjectName), + /// `STYPE = state_data_type` + StateDataType(DataType), + /// `SSPACE = state_data_size` (in bytes) + StateDataSize(u64), + /// `FINALFUNC = final_function` + FinalFunction(ObjectName), + /// `FINALFUNC_EXTRA`. Passes extra dummy arguments to the final function. + FinalFunctionExtra, + /// `FINALFUNC_MODIFY = { READ_ONLY | SHAREABLE | READ_WRITE }` + FinalFunctionModify(AggregateModifyKind), + /// `COMBINEFUNC = combine_function` + CombineFunction(ObjectName), + /// `SERIALFUNC = serial_function` + SerialFunction(ObjectName), + /// `DESERIALFUNC = deserial_function` + DeserialFunction(ObjectName), + /// `INITCOND = initial_condition` (a string literal) + InitialCondition(ValueWithSpan), + /// `MSFUNC = moving_state_transition_function` + MovingStateTransitionFunction(ObjectName), + /// `MINVFUNC = moving_inverse_transition_function` + MovingInverseTransitionFunction(ObjectName), + /// `MSTYPE = moving_state_data_type` + MovingStateDataType(DataType), + /// `MSSPACE = moving_state_data_size` (in bytes) + MovingStateDataSize(u64), + /// `MFINALFUNC = moving_final_function` + MovingFinalFunction(ObjectName), + /// `MFINALFUNC_EXTRA` + MovingFinalFunctionExtra, + /// `MFINALFUNC_MODIFY = { READ_ONLY | SHAREABLE | READ_WRITE }` + MovingFinalFunctionModify(AggregateModifyKind), + /// `MINITCOND = moving_initial_condition` (a string literal) + MovingInitialCondition(ValueWithSpan), + /// `SORTOP = sort_operator` + SortOperator(ObjectName), + /// `PARALLEL = { SAFE | RESTRICTED | UNSAFE }` + Parallel(FunctionParallel), + /// `HYPOTHETICAL`. Marks the aggregate as hypothetical-set. + Hypothetical, + /// `BASETYPE = base_type` (old aggregate syntax). + BaseType(DataType), +} + +impl fmt::Display for CreateAggregateOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::StateTransitionFunction(name) => write!(f, "SFUNC = {name}"), + Self::StateDataType(data_type) => write!(f, "STYPE = {data_type}"), + Self::StateDataSize(size) => write!(f, "SSPACE = {size}"), + Self::FinalFunction(name) => write!(f, "FINALFUNC = {name}"), + Self::FinalFunctionExtra => write!(f, "FINALFUNC_EXTRA"), + Self::FinalFunctionModify(kind) => write!(f, "FINALFUNC_MODIFY = {kind}"), + Self::CombineFunction(name) => write!(f, "COMBINEFUNC = {name}"), + Self::SerialFunction(name) => write!(f, "SERIALFUNC = {name}"), + Self::DeserialFunction(name) => write!(f, "DESERIALFUNC = {name}"), + Self::InitialCondition(cond) => write!(f, "INITCOND = {cond}"), + Self::MovingStateTransitionFunction(name) => write!(f, "MSFUNC = {name}"), + Self::MovingInverseTransitionFunction(name) => write!(f, "MINVFUNC = {name}"), + Self::MovingStateDataType(data_type) => write!(f, "MSTYPE = {data_type}"), + Self::MovingStateDataSize(size) => write!(f, "MSSPACE = {size}"), + Self::MovingFinalFunction(name) => write!(f, "MFINALFUNC = {name}"), + Self::MovingFinalFunctionExtra => write!(f, "MFINALFUNC_EXTRA"), + Self::MovingFinalFunctionModify(kind) => write!(f, "MFINALFUNC_MODIFY = {kind}"), + Self::MovingInitialCondition(cond) => write!(f, "MINITCOND = {cond}"), + Self::SortOperator(name) => write!(f, "SORTOP = {name}"), + Self::Parallel(parallel) => write!(f, "PARALLEL = {}", parallel.as_str()), + Self::Hypothetical => write!(f, "HYPOTHETICAL"), + Self::BaseType(data_type) => write!(f, "BASETYPE = {data_type}"), + } + } +} + +/// Modifier kind for `FINALFUNC_MODIFY` / `MFINALFUNC_MODIFY` in `CREATE AGGREGATE`. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AggregateModifyKind { + /// The final function does not modify the transition state. + ReadOnly, + /// The transition state may be shared between aggregate calls. + Shareable, + /// The final function may modify the transition state. + ReadWrite, +} + +impl fmt::Display for AggregateModifyKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ReadOnly => write!(f, "READ_ONLY"), + Self::Shareable => write!(f, "SHAREABLE"), + Self::ReadWrite => write!(f, "READ_WRITE"), + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b1a81c769..cf9ffbfc1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -62,16 +62,17 @@ pub use self::dcl::{ SetConfigValue, Use, }; pub use self::ddl::{ - Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner, - AlterFunction, AlterFunctionAction, AlterFunctionKind, AlterFunctionOperation, - AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, - AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, - AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, - AlterTableLock, AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchOperation, - AlterTextSearchOption, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, - AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, - ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, CreateConnector, + AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, + AlterConnectorOwner, AlterFunction, AlterFunctionAction, AlterFunctionKind, + AlterFunctionOperation, AlterIndexOperation, AlterOperator, AlterOperatorClass, + AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, + AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, + AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, + AlterTextSearch, AlterTextSearchOperation, AlterTextSearchOption, AlterType, AlterTypeAddValue, + AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, + CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, @@ -3779,6 +3780,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-intro.html) CreateTextSearch(CreateTextSearch), /// ```sql + /// CREATE AGGREGATE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createaggregate.html) + CreateAggregate(CreateAggregate), + /// ```sql /// ALTER TABLE /// ``` AlterTable(AlterTable), @@ -5614,6 +5620,7 @@ impl fmt::Display for Statement { create_operator_family.fmt(f) } Statement::CreateOperatorClass(create_operator_class) => create_operator_class.fmt(f), + Statement::CreateAggregate(create_aggregate) => create_aggregate.fmt(f), Statement::CreateTextSearch(create_text_search) => create_text_search.fmt(f), Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { @@ -10183,16 +10190,23 @@ pub enum FunctionParallel { Safe, } -impl fmt::Display for FunctionParallel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl FunctionParallel { + /// Returns the bare keyword for this parallel mode, without the `PARALLEL` prefix. + pub fn as_str(&self) -> &'static str { match self { - FunctionParallel::Unsafe => write!(f, "PARALLEL UNSAFE"), - FunctionParallel::Restricted => write!(f, "PARALLEL RESTRICTED"), - FunctionParallel::Safe => write!(f, "PARALLEL SAFE"), + FunctionParallel::Unsafe => "UNSAFE", + FunctionParallel::Restricted => "RESTRICTED", + FunctionParallel::Safe => "SAFE", } } } +impl fmt::Display for FunctionParallel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PARALLEL {}", self.as_str()) + } +} + /// [BigQuery] Determinism specifier used in a UDF definition. /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 09a40f665..0deb4ec71 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -18,8 +18,8 @@ use crate::{ ast::{ ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, AlterTable, - ColumnOptions, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreateView, - ExportData, Owner, TypedString, + ColumnOptions, CreateAggregate, CreateOperator, CreateOperatorClass, CreateOperatorFamily, + CreateView, ExportData, Owner, TypedString, }, tokenizer::TokenWithSpan, }; @@ -523,6 +523,7 @@ impl Spanned for Statement { Statement::Vacuum(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), + Statement::CreateAggregate(create_aggregate) => create_aggregate.span(), } } } @@ -2513,6 +2514,12 @@ impl Spanned for AlterTable { } } +impl Spanned for CreateAggregate { + fn span(&self) -> Span { + Span::empty() + } +} + impl Spanned for CreateOperator { fn span(&self) -> Span { Span::empty() diff --git a/src/keywords.rs b/src/keywords.rs index c2d0a47ec..b7795cf76 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -149,6 +149,7 @@ define_keywords!( BACKUP, BACKWARD, BASE64, + BASETYPE, BASE_LOCATION, BEFORE, BEGIN, @@ -236,6 +237,7 @@ define_keywords!( COLUMN, COLUMNS, COLUMNSTORE, + COMBINEFUNC, COMMENT, COMMIT, COMMITTED, @@ -332,6 +334,7 @@ define_keywords!( DEREF, DESC, DESCRIBE, + DESERIALFUNC, DETACH, DETAIL, DETERMINISTIC, @@ -428,6 +431,9 @@ define_keywords!( FILL, FILTER, FINAL, + FINALFUNC, + FINALFUNC_EXTRA, + FINALFUNC_MODIFY, FIRST, FIRST_VALUE, FIXEDSTRING, @@ -494,6 +500,7 @@ define_keywords!( HOUR, HOURS, HUGEINT, + HYPOTHETICAL, IAM_ROLE, ICEBERG, ID, @@ -518,6 +525,7 @@ define_keywords!( INDICATOR, INHERIT, INHERITS, + INITCOND, INITIALIZE, INITIALLY, INNER, @@ -646,6 +654,9 @@ define_keywords!( METRIC, METRICS, MFA, + MFINALFUNC, + MFINALFUNC_EXTRA, + MFINALFUNC_MODIFY, MICROSECOND, MICROSECONDS, MILLENIUM, @@ -653,10 +664,12 @@ define_keywords!( MILLISECOND, MILLISECONDS, MIN, + MINITCOND, MINUS, MINUTE, MINUTES, MINVALUE, + MINVFUNC, MIN_ROWS, MOD, MODE, @@ -668,6 +681,9 @@ define_keywords!( MONTH, MONTHS, MSCK, + MSFUNC, + MSSPACE, + MSTYPE, MULTIRANGE_TYPE_NAME, MULTISET, MUTATION, @@ -842,6 +858,7 @@ define_keywords!( READ, READS, READ_ONLY, + READ_WRITE, REAL, RECEIVE, RECLUSTER, @@ -944,6 +961,7 @@ define_keywords!( SEQUENCES, SERDE, SERDEPROPERTIES, + SERIALFUNC, SERIALIZABLE, SERVER, SERVICE, @@ -954,7 +972,9 @@ define_keywords!( SETOF, SETS, SETTINGS, + SFUNC, SHARE, + SHAREABLE, SHARED, SHARING, SHOW, @@ -970,6 +990,7 @@ define_keywords!( SORT, SORTED, SORTKEY, + SORTOP, SOURCE, SPATIAL, SPECIFIC, @@ -986,6 +1007,7 @@ define_keywords!( SQL_SMALL_RESULT, SQRT, SRID, + SSPACE, STABLE, STAGE, START, @@ -1012,6 +1034,7 @@ define_keywords!( STRICT, STRING, STRUCT, + STYPE, SUBMULTISET, SUBSCRIPT, SUBSTR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6df5cf309..8bcc1e655 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5283,11 +5283,13 @@ impl<'a> Parser<'a> { self.parse_create_secret(or_replace, temporary, persistent) } else if self.parse_keyword(Keyword::USER) { self.parse_create_user(or_replace).map(Into::into) + } else if self.parse_keyword(Keyword::AGGREGATE) { + self.parse_create_aggregate(or_replace).map(Into::into) } else if self.parse_keyword(Keyword::WAREHOUSE) { self.parse_create_warehouse(or_replace).map(Into::into) } else if or_replace { self.expected_ref( - "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or WAREHOUSE after CREATE OR REPLACE", + "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or AGGREGATE or WAREHOUSE after CREATE OR REPLACE", self.peek_token_ref(), ) } else if self.parse_keyword(Keyword::EXTENSION) { @@ -5900,16 +5902,7 @@ impl<'a> Parser<'a> { body.called_on_null = Some(FunctionCalledOnNull::Strict); } else if self.parse_keyword(Keyword::PARALLEL) { ensure_not_set(&body.parallel, "PARALLEL { UNSAFE | RESTRICTED | SAFE }")?; - if self.parse_keyword(Keyword::UNSAFE) { - body.parallel = Some(FunctionParallel::Unsafe); - } else if self.parse_keyword(Keyword::RESTRICTED) { - body.parallel = Some(FunctionParallel::Restricted); - } else if self.parse_keyword(Keyword::SAFE) { - body.parallel = Some(FunctionParallel::Safe); - } else { - return self - .expected_ref("one of UNSAFE | RESTRICTED | SAFE", self.peek_token_ref()); - } + body.parallel = Some(self.parse_function_parallel()?); } else if self.parse_keyword(Keyword::SECURITY) { ensure_not_set(&body.security, "SECURITY { DEFINER | INVOKER }")?; if self.parse_keyword(Keyword::DEFINER) { @@ -7425,6 +7418,218 @@ impl<'a> Parser<'a> { }) } + /// Parse a [Statement::CreateAggregate] + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createaggregate.html) + pub fn parse_create_aggregate( + &mut self, + or_replace: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + + // The legacy and modern forms (see [`CreateAggregateArgs::Legacy`]) differ + // only in whether a second parenthesized list follows the first, so look + // that far ahead before committing to either branch. + let args = if self.peek_create_aggregate_arg_list()? { + self.parse_create_aggregate_args()? + } else { + CreateAggregateArgs::Legacy + }; + self.expect_token(&Token::LParen)?; + + let mut seen: Vec = Vec::new(); + let options = self.parse_comma_separated(|parser| { + let start = parser.peek_token_ref().span.start; + let keyword = parser.parse_create_aggregate_option_key()?; + if seen.contains(&keyword) { + return parser_err!( + format!("Duplicate CREATE AGGREGATE option: {keyword:?}"), + start + ); + } + seen.push(keyword); + parser.parse_create_aggregate_option(keyword) + })?; + self.expect_token(&Token::RParen)?; + + Ok(CreateAggregate { + or_replace, + name, + args, + options, + }) + } + + /// Parse the argument list of a `CREATE AGGREGATE`: `(*)` or `(arg [, ...])`. + fn parse_create_aggregate_args(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let args = if self.consume_token(&Token::Mul) { + CreateAggregateArgs::Star + } else { + CreateAggregateArgs::List( + self.parse_comma_separated0(Parser::parse_function_arg, Token::RParen)?, + ) + }; + self.expect_token(&Token::RParen)?; + Ok(args) + } + + /// True when the list at the current position is the modern form's argument + /// list, meaning a second parenthesized list follows it. + /// + /// Errors when the list is never closed rather than guessing a form, since + /// committing to either one buries the missing `)` under whichever error + /// that branch happens to hit first. + fn peek_create_aggregate_arg_list(&self) -> Result { + let mut tokens = self + .tokens + .iter() + .skip(self.index) + .filter(|token| !matches!(token.token, Token::Whitespace(_))); + if tokens.next().map(|token| &token.token) != Some(&Token::LParen) { + return Ok(false); + } + let mut depth = 1usize; + while let Some(token) = tokens.next() { + match token.token { + Token::LParen => depth += 1, + Token::RParen => { + depth -= 1; + if depth == 0 { + return Ok(tokens.next().map(|token| &token.token) == Some(&Token::LParen)); + } + } + // The list is still open at the statement boundary, so stop + // before the next statement's parentheses answer for this one. + Token::SemiColon => return self.expected_ref(")", token), + _ => {} + } + } + self.expected_ref(")", &EOF_TOKEN) + } + + /// Parse `PARALLEL` qualifier value: `{ SAFE | RESTRICTED | UNSAFE }`. + fn parse_function_parallel(&mut self) -> Result { + match self.expect_one_of_keywords(&[Keyword::SAFE, Keyword::RESTRICTED, Keyword::UNSAFE])? { + Keyword::SAFE => Ok(FunctionParallel::Safe), + Keyword::RESTRICTED => Ok(FunctionParallel::Restricted), + Keyword::UNSAFE => Ok(FunctionParallel::Unsafe), + _ => self.expected_ref("one of SAFE | RESTRICTED | UNSAFE", self.peek_token_ref()), + } + } + + /// Every key listed here needs an arm in [`Self::parse_create_aggregate_option`]. + fn parse_create_aggregate_option_key(&mut self) -> Result { + self.expect_one_of_keywords(&[ + Keyword::SFUNC, + Keyword::STYPE, + Keyword::SSPACE, + Keyword::FINALFUNC, + Keyword::FINALFUNC_EXTRA, + Keyword::FINALFUNC_MODIFY, + Keyword::COMBINEFUNC, + Keyword::SERIALFUNC, + Keyword::DESERIALFUNC, + Keyword::INITCOND, + Keyword::MSFUNC, + Keyword::MINVFUNC, + Keyword::MSTYPE, + Keyword::MSSPACE, + Keyword::MFINALFUNC, + Keyword::MFINALFUNC_EXTRA, + Keyword::MFINALFUNC_MODIFY, + Keyword::MINITCOND, + Keyword::SORTOP, + Keyword::PARALLEL, + Keyword::HYPOTHETICAL, + Keyword::BASETYPE, + ]) + } + + /// Parse the value of a single `CREATE AGGREGATE` option, given its + /// already-consumed key. + fn parse_create_aggregate_option( + &mut self, + keyword: Keyword, + ) -> Result { + // Every option but the three valueless flags is spelled `KEY = value`. + if !matches!( + keyword, + Keyword::FINALFUNC_EXTRA | Keyword::MFINALFUNC_EXTRA | Keyword::HYPOTHETICAL + ) { + self.expect_token(&Token::Eq)?; + } + + Ok(match keyword { + Keyword::FINALFUNC_EXTRA => CreateAggregateOption::FinalFunctionExtra, + Keyword::MFINALFUNC_EXTRA => CreateAggregateOption::MovingFinalFunctionExtra, + Keyword::HYPOTHETICAL => CreateAggregateOption::Hypothetical, + Keyword::SFUNC => { + CreateAggregateOption::StateTransitionFunction(self.parse_object_name(false)?) + } + Keyword::STYPE => CreateAggregateOption::StateDataType(self.parse_data_type()?), + Keyword::SSPACE => CreateAggregateOption::StateDataSize(self.parse_literal_uint()?), + Keyword::FINALFUNC => { + CreateAggregateOption::FinalFunction(self.parse_object_name(false)?) + } + Keyword::FINALFUNC_MODIFY => { + CreateAggregateOption::FinalFunctionModify(self.parse_aggregate_modify_kind()?) + } + Keyword::COMBINEFUNC => { + CreateAggregateOption::CombineFunction(self.parse_object_name(false)?) + } + Keyword::SERIALFUNC => { + CreateAggregateOption::SerialFunction(self.parse_object_name(false)?) + } + Keyword::DESERIALFUNC => { + CreateAggregateOption::DeserialFunction(self.parse_object_name(false)?) + } + Keyword::INITCOND => CreateAggregateOption::InitialCondition(self.parse_value()?), + Keyword::MSFUNC => { + CreateAggregateOption::MovingStateTransitionFunction(self.parse_object_name(false)?) + } + Keyword::MINVFUNC => CreateAggregateOption::MovingInverseTransitionFunction( + self.parse_object_name(false)?, + ), + Keyword::MSTYPE => CreateAggregateOption::MovingStateDataType(self.parse_data_type()?), + Keyword::MSSPACE => { + CreateAggregateOption::MovingStateDataSize(self.parse_literal_uint()?) + } + Keyword::MFINALFUNC => { + CreateAggregateOption::MovingFinalFunction(self.parse_object_name(false)?) + } + Keyword::MFINALFUNC_MODIFY => CreateAggregateOption::MovingFinalFunctionModify( + self.parse_aggregate_modify_kind()?, + ), + Keyword::MINITCOND => { + CreateAggregateOption::MovingInitialCondition(self.parse_value()?) + } + Keyword::SORTOP => CreateAggregateOption::SortOperator(self.parse_operator_name()?), + Keyword::PARALLEL => CreateAggregateOption::Parallel(self.parse_function_parallel()?), + Keyword::BASETYPE => CreateAggregateOption::BaseType(self.parse_data_type()?), + _ => { + return self.expected_ref("a CREATE AGGREGATE option", self.peek_token_ref()); + } + }) + } + + /// Parse the value of `FINALFUNC_MODIFY` / `MFINALFUNC_MODIFY`. + fn parse_aggregate_modify_kind(&mut self) -> Result { + match self.expect_one_of_keywords(&[ + Keyword::READ_ONLY, + Keyword::SHAREABLE, + Keyword::READ_WRITE, + ])? { + Keyword::READ_ONLY => Ok(AggregateModifyKind::ReadOnly), + Keyword::SHAREABLE => Ok(AggregateModifyKind::Shareable), + Keyword::READ_WRITE => Ok(AggregateModifyKind::ReadWrite), + _ => self.expected_ref( + "one of READ_ONLY | SHAREABLE | READ_WRITE", + self.peek_token_ref(), + ), + } + } + /// Parse a [Statement::CreateOperatorFamily] /// /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createopfamily.html) @@ -11298,17 +11503,9 @@ impl<'a> Parser<'a> { security, }) } else if self.parse_keyword(Keyword::PARALLEL) { - let parallel = if self.parse_keyword(Keyword::UNSAFE) { - FunctionParallel::Unsafe - } else if self.parse_keyword(Keyword::RESTRICTED) { - FunctionParallel::Restricted - } else if self.parse_keyword(Keyword::SAFE) { - FunctionParallel::Safe - } else { - return self - .expected_ref("one of UNSAFE | RESTRICTED | SAFE", self.peek_token_ref()); - }; - Some(AlterFunctionAction::Parallel(parallel)) + Some(AlterFunctionAction::Parallel( + self.parse_function_parallel()?, + )) } else if self.parse_keyword(Keyword::COST) { Some(AlterFunctionAction::Cost(self.parse_number()?)) } else if self.parse_keyword(Keyword::ROWS) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9efa1daca..61958312b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9539,6 +9539,267 @@ fn parse_lock_table() { } } +/// The rendered argument list of a `CREATE AGGREGATE`, which must not be the +/// legacy or wildcard form. +fn aggregate_args(args: &CreateAggregateArgs) -> Vec { + match args { + CreateAggregateArgs::List(args) => args.iter().map(ToString::to_string).collect(), + other => panic!("Expected an argument list, got: {other:?}"), + } +} + +#[test] +fn parse_create_aggregate_basic() { + let sql = "CREATE AGGREGATE myavg (NUMERIC) (SFUNC = numeric_avg_accum, STYPE = internal, FINALFUNC = numeric_avg, INITCOND = '0')"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(!agg.or_replace); + assert_eq!(agg.name.to_string(), "myavg"); + assert_eq!(aggregate_args(&agg.args), ["NUMERIC"]); + assert_eq!(agg.options.len(), 4); + assert_eq!(agg.options[0].to_string(), "SFUNC = numeric_avg_accum"); + assert_eq!(agg.options[1].to_string(), "STYPE = internal"); + assert_eq!(agg.options[2].to_string(), "FINALFUNC = numeric_avg"); + assert_eq!(agg.options[3].to_string(), "INITCOND = '0'"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_or_replace_with_parallel() { + let sql = "CREATE OR REPLACE AGGREGATE sum2 (INT4, INT4) (SFUNC = int4pl, STYPE = INT4, PARALLEL = SAFE)"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(agg.or_replace); + assert_eq!(agg.name.to_string(), "sum2"); + assert_eq!(aggregate_args(&agg.args), ["INT4", "INT4"]); + assert_eq!(agg.options.len(), 3); + assert_eq!(agg.options[2].to_string(), "PARALLEL = SAFE"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_with_moving_aggregate_options() { + let sql = "CREATE AGGREGATE moving_sum (FLOAT8) (SFUNC = float8pl, STYPE = FLOAT8, MSFUNC = float8pl, MINVFUNC = float8mi, MSTYPE = FLOAT8, MFINALFUNC_EXTRA, MFINALFUNC_MODIFY = READ_ONLY)"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(!agg.or_replace); + assert_eq!(agg.name.to_string(), "moving_sum"); + assert_eq!(aggregate_args(&agg.args), ["FLOAT8"]); + assert_eq!(agg.options.len(), 7); + assert_eq!(agg.options[4].to_string(), "MSTYPE = FLOAT8"); + assert_eq!(agg.options[5].to_string(), "MFINALFUNC_EXTRA"); + assert_eq!(agg.options[6].to_string(), "MFINALFUNC_MODIFY = READ_ONLY"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_star_args() { + let canonical = "CREATE AGGREGATE my_count (*) (SFUNC = int8inc, STYPE = INT8, INITCOND = '0')"; + let stmt = pg_and_generic().verified_stmt(canonical); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::Star); + assert_eq!(agg.name.to_string(), "my_count"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } + + pg_and_generic().one_statement_parses_to( + "CREATE AGGREGATE my_count ( * ) (SFUNC = int8inc, STYPE = INT8)", + "CREATE AGGREGATE my_count (*) (SFUNC = int8inc, STYPE = INT8)", + ); +} + +#[test] +fn parse_create_aggregate_empty_args() { + let stmt = pg_and_generic() + .verified_stmt("CREATE AGGREGATE my_agg () (SFUNC = my_sfunc, STYPE = INT)"); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::List(vec![])); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_named_and_variadic_args() { + let sql = + "CREATE AGGREGATE my_agg (input INT, VARIADIC tail TEXT) (SFUNC = my_sfunc, STYPE = INT)"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!( + aggregate_args(&agg.args), + ["input INT", "VARIADIC tail TEXT"] + ); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_additional_options() { + pg_and_generic().verified_stmt( + "CREATE AGGREGATE percentile (FLOAT8) (SFUNC = ordered_set_transition, STYPE = internal, FINALFUNC = percentile_final, FINALFUNC_MODIFY = READ_WRITE, HYPOTHETICAL)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_min (INT) (SFUNC = my_sfunc, STYPE = INT, SSPACE = 128, SORTOP = <)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_min2 (INT) (SFUNC = my_sfunc, STYPE = INT, SORTOP = pg_catalog.<)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_sum (INT) (SFUNC = my_sfunc, STYPE = internal, COMBINEFUNC = my_combine, SERIALFUNC = my_serial, DESERIALFUNC = my_deserial)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_extra (INT) (SFUNC = my_sfunc, STYPE = internal, FINALFUNC = my_final, FINALFUNC_EXTRA)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_shareable (INT) (SFUNC = my_sfunc, STYPE = internal, FINALFUNC_MODIFY = SHAREABLE, MFINALFUNC = my_mfinal, MSSPACE = 64, MINITCOND = '0')", + ); +} + +#[test] +fn parse_create_aggregate_parenthesized_arg_type() { + // The two forms are told apart by counting parenthesized lists, so a + // parenthesized type must not be mistaken for the end of the argument list. + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_numeric (NUMERIC(10,2)) (SFUNC = my_sfunc, STYPE = NUMERIC(10,2))", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_numeric2 (BASETYPE = NUMERIC(10,2), SFUNC = my_sfunc, STYPE = INT)", + ); +} + +#[test] +fn parse_create_aggregate_legacy_syntax() { + let stmt = pg_and_generic() + .verified_stmt("CREATE AGGREGATE my_avg (BASETYPE = INT, SFUNC = my_sfunc, STYPE = INT)"); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::Legacy); + assert_eq!(agg.options.len(), 3); + assert_eq!( + agg.options[0], + CreateAggregateOption::BaseType(DataType::Int(None)) + ); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } + + let stmt = pg_and_generic() + .verified_stmt("CREATE AGGREGATE my_avg (SFUNC = my_sfunc, BASETYPE = INT, STYPE = INT)"); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::Legacy); + assert_eq!(agg.options.len(), 3); + assert_eq!( + agg.options[1], + CreateAggregateOption::BaseType(DataType::Int(None)) + ); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } + + // An option whose value cannot start an argument definition. + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_min (BASETYPE = INT, SFUNC = my_sfunc, STYPE = INT, SORTOP = <)", + ); + + // Without BASETYPE the statement is still a single-list one, and must not + // gain an empty argument list on the way back out. + pg_and_generic().verified_stmt("CREATE AGGREGATE my_avg (SFUNC = my_sfunc, STYPE = INT)"); +} + +#[test] +fn parse_create_aggregate_rejects_bad_options() { + let unknown = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT) (UNKNOWN_OPTION = bar)") + .unwrap_err() + .to_string(); + assert!( + unknown.starts_with("sql parser error: Expected: one of SFUNC") + && unknown.ends_with(", found: UNKNOWN_OPTION"), + "unexpected error: {unknown}" + ); + + // Option keys are keywords here, as they are in CREATE OPERATOR, so a + // quoted key is an ordinary identifier and does not name an option. + let quoted = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT) (\"SFUNC\" = my_sfunc, STYPE = INT)") + .unwrap_err() + .to_string(); + assert!( + quoted.starts_with("sql parser error: Expected: one of SFUNC") + && quoted.ends_with(", found: \"SFUNC\""), + "unexpected error: {quoted}" + ); + + let duplicate = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT) (SFUNC = f, SFUNC = g, STYPE = INT)") + .unwrap_err(); + assert_eq!( + duplicate.to_string(), + "sql parser error: Duplicate CREATE AGGREGATE option: SFUNC" + ); +} + +#[test] +fn parse_create_aggregate_reports_argument_list_errors() { + // A malformed argument list must report itself, not be reinterpreted as the + // legacy single-list form and blamed on the options. + let err = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT, ) (SFUNC = f, STYPE = INT)") + .unwrap_err(); + assert_eq!( + err.to_string(), + "sql parser error: Expected: a data type name, found: )" + ); + + // Every unclosed-list shape reports the same missing `)`, rather than the + // form-specific error a guessed branch would hit first. + for sql in [ + "CREATE AGGREGATE foo (INT", + "CREATE AGGREGATE foo (SORTOP = <, SFUNC = f, STYPE = INT", + "CREATE AGGREGATE foo (BASETYPE = INT, SFUNC = f", + ] { + let unclosed = pg_and_generic().parse_sql_statements(sql).unwrap_err(); + assert_eq!( + unclosed.to_string(), + "sql parser error: Expected: ), found: EOF", + "unexpected error for {sql}" + ); + } + + // A statement boundary ends the search, so the following statement's + // parentheses cannot answer for this one. + let semicolon = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT; SELECT (1) (2)") + .unwrap_err(); + assert_eq!( + semicolon.to_string(), + "sql parser error: Expected: ), found: ;" + ); + + let missing = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo )") + .unwrap_err(); + assert_eq!( + missing.to_string(), + "sql parser error: Expected: (, found: )" + ); +} + #[test] fn exclude_as_column_name() { // `EXCLUDE` is a non-reserved keyword, so it stays usable as a column name