diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 5d5b964d17..c073f16c06 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -//! AST types specific to CREATE/ALTER variants of [`Statement`](crate::ast::Statement) +//! AST types specific to CREATE/ALTER variants of [`Statement`] //! (commonly referred to as Data Definition Language, or DDL) #[cfg(not(feature = "std"))] @@ -48,9 +48,10 @@ use crate::ast::{ FunctionSecurity, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, - ResetConfig, RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageLifecyclePolicy, - StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, TriggerExecBody, TriggerObject, - TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, WrappedCollection, + ResetConfig, RowAccessPolicy, SequenceOptions, Spanned, SqlOption, Statement, + StorageLifecyclePolicy, StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, + TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, + WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; @@ -6041,6 +6042,146 @@ impl From for crate::ast::Statement { } } +/// A PostgreSQL rule with source spans derived from its identifiers, condition and actions. +/// +/// ```sql +/// CREATE [ OR REPLACE ] RULE name AS ON event +/// TO table_name [ WHERE condition ] +/// DO [ ALSO | INSTEAD ] { NOTHING | command | ( command ; command ... ) } +/// ``` +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrule.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 CreateRule { + /// `true` when `OR REPLACE` was present. + pub or_replace: bool, + /// Name of the rule. + pub name: Ident, + /// Event the rule fires on (`SELECT`, `INSERT`, `UPDATE` or `DELETE`). + pub event: CreateRuleEvent, + /// Table the rule is defined on. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// Optional expression for the `WHERE` clause. + pub condition: Option, + /// Optional `ALSO` or `INSTEAD` keyword following `DO`. + pub do_kind: Option, + /// Action the rule performs. + pub action: CreateRuleAction, +} + +impl fmt::Display for CreateRule { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {or_replace}RULE {name} AS ON {event} TO {table_name}", + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + name = self.name, + event = self.event, + table_name = self.table_name, + )?; + if let Some(condition) = &self.condition { + write!(f, " WHERE {condition}")?; + } + write!(f, " DO")?; + if let Some(do_kind) = &self.do_kind { + write!(f, " {do_kind}")?; + } + write!(f, " {}", self.action) + } +} + +/// Event that fires a rule (`ON` clause). +/// ```sql +/// AS ON [SELECT | INSERT | UPDATE | DELETE] +/// ``` +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrule.html) +#[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 CreateRuleEvent { + /// Fires on SELECT. + Select, + /// Fires on INSERT. + Insert, + /// Fires on UPDATE. + Update, + /// Fires on DELETE. + Delete, +} + +impl fmt::Display for CreateRuleEvent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateRuleEvent::Select => write!(f, "SELECT"), + CreateRuleEvent::Insert => write!(f, "INSERT"), + CreateRuleEvent::Update => write!(f, "UPDATE"), + CreateRuleEvent::Delete => write!(f, "DELETE"), + } + } +} + +/// Keyword following `DO` in a `CREATE RULE` statement. +/// ```sql +/// DO [ ALSO | INSTEAD ] +/// ``` +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrule.html) +#[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 CreateRuleDoKind { + /// The action runs in addition to the original statement. + Also, + /// The action runs instead of the original statement. + Instead, +} + +impl fmt::Display for CreateRuleDoKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateRuleDoKind::Also => write!(f, "ALSO"), + CreateRuleDoKind::Instead => write!(f, "INSTEAD"), + } + } +} + +/// Action of a `CREATE RULE` statement. +/// ```sql +/// { NOTHING | command | ( command ; command ... ) } +/// ``` +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrule.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateRuleAction { + /// `NOTHING` + Nothing, + /// A single command, e.g. `DO INSTEAD SELECT * FROM t`. + Statement(Box), + /// A parenthesized, semicolon-separated list of commands. + Statements(Vec), +} + +impl fmt::Display for CreateRuleAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateRuleAction::Nothing => write!(f, "NOTHING"), + CreateRuleAction::Statement(statement) => write!(f, "{statement}"), + CreateRuleAction::Statements(statements) => { + write!(f, "({})", display_separated(statements, "; ")) + } + } + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateRule) -> Self { + crate::ast::Statement::CreateRule(v) + } +} + /// A `CREATE FOREIGN TABLE` statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb5bc6cc75..0ce2e477c4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -74,15 +74,16 @@ pub use self::ddl::{ ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, - 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, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, + CreateRule, CreateRuleAction, CreateRuleDoKind, CreateRuleEvent, 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, + SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, @@ -3822,6 +3823,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) CreatePolicy(CreatePolicy), /// ```sql + /// CREATE RULE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrule.html) + CreateRule(CreateRule), + /// ```sql /// CREATE CONNECTOR /// ``` /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-CreateDataConnectorCreateConnector) @@ -5686,6 +5692,7 @@ impl fmt::Display for Statement { } Statement::CreateForeignTable(stmt) => write!(f, "{stmt}"), Statement::CreatePolicy(policy) => write!(f, "{policy}"), + Statement::CreateRule(rule) => write!(f, "{rule}"), Statement::CreateConnector(create_connector) => create_connector.fmt(f), Statement::CreateOperator(create_operator) => create_operator.fmt(f), Statement::CreateOperatorFamily(create_operator_family) => { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8267c71798..da2db8fb5c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -32,18 +32,18 @@ use super::{ AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, - ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, - CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, - ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, - FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, - HavingBound, IfStatement, IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, - LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, - MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, - PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, RaiseStatement, - RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, + CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, + ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, + Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, + FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, InTarget, + IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, + JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, + MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, + NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, + OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, + Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, + RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, @@ -487,6 +487,7 @@ impl Spanned for Statement { Statement::Unload { .. } => Span::empty(), Statement::OptimizeTable { .. } => Span::empty(), Statement::CreatePolicy { .. } => Span::empty(), + Statement::CreateRule(rule) => rule.span(), Statement::AlterPolicy { .. } => Span::empty(), Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), @@ -548,6 +549,27 @@ impl Spanned for Use { } } +impl Spanned for CreateRule { + fn span(&self) -> Span { + let action_span = match &self.action { + CreateRuleAction::Nothing => Span::empty(), + CreateRuleAction::Statement(statement) => statement.span(), + CreateRuleAction::Statements(statements) => { + union_spans(statements.iter().map(Spanned::span)) + } + }; + union_spans( + [ + self.name.span, + self.table_name.span(), + self.condition.as_ref().map_or(Span::empty(), Spanned::span), + action_span, + ] + .into_iter(), + ) + } +} + impl Spanned for CreateTable { fn span(&self) -> Span { let CreateTable { diff --git a/src/keywords.rs b/src/keywords.rs index 7e93822416..9651d2d60d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -115,6 +115,7 @@ define_keywords!( ALL, ALLOCATE, ALLOWOVERWRITE, + ALSO, ALTER, ALWAYS, ANALYZE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 276b252ed2..9af96b658e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5417,6 +5417,8 @@ impl<'a> Parser<'a> { .map(Into::into) } else if self.parse_keyword(Keyword::POLICY) { self.parse_create_policy().map(Into::into) + } else if self.parse_keyword(Keyword::RULE) { + self.parse_create_rule(or_replace).map(Into::into) } else if self.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(or_replace).map(Into::into) } else if self.parse_keyword(Keyword::FUNCTION) { @@ -7416,6 +7418,91 @@ impl<'a> Parser<'a> { }) } + /// ```sql + /// CREATE [ OR REPLACE ] RULE name AS ON event + /// TO table_name [ WHERE condition ] + /// DO [ ALSO | INSTEAD ] { NOTHING | command | ( command ; command ... ) } + /// ``` + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createrule.html) + pub fn parse_create_rule(&mut self, or_replace: bool) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::AS)?; + self.expect_keyword_is(Keyword::ON)?; + let event = match &self.peek_token_ref().token { + Token::Word(Word { + keyword: Keyword::SELECT, + .. + }) => CreateRuleEvent::Select, + Token::Word(Word { + keyword: Keyword::INSERT, + .. + }) => CreateRuleEvent::Insert, + Token::Word(Word { + keyword: Keyword::UPDATE, + .. + }) => CreateRuleEvent::Update, + Token::Word(Word { + keyword: Keyword::DELETE, + .. + }) => CreateRuleEvent::Delete, + _ => { + return self.expected_ref( + "one of SELECT or INSERT or UPDATE or DELETE", + self.peek_token_ref(), + ) + } + }; + self.advance_token(); + self.expect_keyword_is(Keyword::TO)?; + let table_name = self.parse_object_name(false)?; + + let condition = if self.parse_keyword(Keyword::WHERE) { + Some(self.parse_expr()?) + } else { + None + }; + + self.expect_keyword_is(Keyword::DO)?; + let do_kind = if self.parse_keyword(Keyword::ALSO) { + Some(CreateRuleDoKind::Also) + } else if self.parse_keyword(Keyword::INSTEAD) { + Some(CreateRuleDoKind::Instead) + } else { + None + }; + + let action = if self.parse_keyword(Keyword::NOTHING) { + CreateRuleAction::Nothing + } else if self.consume_token(&Token::LParen) { + let mut statements = Vec::new(); + loop { + while self.consume_token(&Token::SemiColon) {} + if self.consume_token(&Token::RParen) { + break; + } + statements.push(self.parse_statement()?); + if !self.consume_token(&Token::SemiColon) { + self.expect_token(&Token::RParen)?; + break; + } + } + CreateRuleAction::Statements(statements) + } else { + CreateRuleAction::Statement(Box::new(self.parse_statement()?)) + }; + + Ok(CreateRule { + or_replace, + name, + event, + table_name, + condition, + do_kind, + action, + }) + } + /// ```sql /// CREATE CONNECTOR [IF NOT EXISTS] connector_name /// [TYPE datasource_type] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 87fb61dc17..e62f8d0c9c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20496,6 +20496,74 @@ fn parse_stage_table_factor() { ); } +#[test] +fn parse_create_rule() { + let sql = "CREATE RULE r AS ON DELETE TO docs DO INSTEAD NOTHING"; + match all_dialects().verified_stmt(sql) { + Statement::CreateRule(CreateRule { + or_replace, + name, + event, + table_name, + condition, + do_kind, + action, + }) => { + assert!(!or_replace); + assert_eq!(name.to_string(), "r"); + assert_eq!(event, CreateRuleEvent::Delete); + assert_eq!(table_name.to_string(), "docs"); + assert!(condition.is_none()); + assert_eq!(do_kind, Some(CreateRuleDoKind::Instead)); + assert_eq!(action, CreateRuleAction::Nothing); + } + _ => unreachable!("Expected CREATE RULE"), + } + + all_dialects().verified_stmt("CREATE OR REPLACE RULE r AS ON SELECT TO t DO ALSO NOTHING"); + all_dialects().verified_stmt("CREATE RULE r AS ON INSERT TO s1.t DO NOTHING"); + all_dialects().verified_stmt( + "CREATE RULE r AS ON UPDATE TO t WHERE quantity > 100 DO ALSO INSERT INTO audit (id) VALUES (1)", + ); + all_dialects().verified_stmt( + "CREATE RULE r AS ON DELETE TO t DO INSTEAD (UPDATE t2 SET deleted = 1 WHERE id = 1; DELETE FROM t3)", + ); + + // A trailing semicolon inside a parenthesized action list is accepted and + // normalized away. + all_dialects().one_statement_parses_to( + "CREATE RULE r AS ON DELETE TO t DO INSTEAD (DELETE FROM t2;)", + "CREATE RULE r AS ON DELETE TO t DO INSTEAD (DELETE FROM t2)", + ); + + // invalid event + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE RULE r AS ON TRUNCATE TO t DO NOTHING") + .unwrap_err() + .to_string(), + "sql parser error: Expected: one of SELECT or INSERT or UPDATE or DELETE, found: TRUNCATE" + ); + + // missing DO clause + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE RULE r AS ON DELETE TO t") + .unwrap_err() + .to_string(), + "sql parser error: Expected: DO, found: EOF" + ); + + // missing table name + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE RULE r AS ON DELETE DO NOTHING") + .unwrap_err() + .to_string(), + "sql parser error: Expected: TO, found: DO" + ); +} + #[test] fn parse_alter_table_column_position() { let dialects = all_dialects_where(|d| d.supports_alter_column_position()); @@ -21022,3 +21090,157 @@ fn parse_window_spec_keyword_base_window_name() { "SELECT count(*) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t", ); } + +#[test] +fn parse_create_rule_action_boundaries() { + let dialects = all_dialects(); + let Statement::CreateRule(CreateRule { + action: CreateRuleAction::Statements(commands), + .. + }) = dialects.verified_stmt("CREATE RULE r AS ON INSERT TO t DO ()") + else { + panic!("expected a rule with a command list"); + }; + assert!(commands.is_empty()); + + let statements = dialects + .parse_sql_statements( + "CREATE RULE r AS ON INSERT TO t DO (;; INSERT INTO audit VALUES (1);; DELETE FROM pending;;); SELECT 9", + ) + .unwrap(); + assert_eq!( + statements.iter().map(ToString::to_string).collect::>(), + [ + "CREATE RULE r AS ON INSERT TO t DO (INSERT INTO audit VALUES (1); DELETE FROM pending)", + "SELECT 9", + ] + ); +} + +#[test] +fn parse_create_rule_invalid_components() { + let dialects = all_dialects(); + for (sql, expected) in [ + ("CREATE RULE", "Expected: identifier, found: EOF"), + ( + "CREATE RULE r ON UPDATE TO t DO NOTHING", + "Expected: AS, found: ON", + ), + ( + "CREATE RULE r AS UPDATE TO t DO NOTHING", + "Expected: ON, found: UPDATE", + ), + ( + "CREATE RULE r AS ON 1 TO t DO NOTHING", + "Expected: one of SELECT or INSERT or UPDATE or DELETE, found: 1", + ), + ( + "CREATE RULE r AS ON INSERT TO", + "Expected: identifier, found: EOF", + ), + ( + "CREATE RULE r AS ON INSERT TO t WHERE (", + "Expected: an expression, found: EOF", + ), + ( + "CREATE RULE r AS ON INSERT TO t DO", + "Expected: an SQL statement, found: EOF", + ), + ( + "CREATE RULE r AS ON INSERT TO t DO (BOGUS)", + "Expected: an SQL statement, found: BOGUS", + ), + ( + "CREATE RULE r AS ON INSERT TO t DO (SELECT 1 SELECT 2)", + "Expected: ), found: SELECT", + ), + ( + "CREATE RULE r AS ON INSERT TO t DO (SELECT 1", + "Expected: ), found: EOF", + ), + ] { + assert_eq!( + dialects.parse_sql_statements(sql), + Err(ParserError::ParserError(expected.to_string())), + "{sql}" + ); + } +} + +#[test] +fn parse_create_rule_spans() { + let dialects = all_dialects(); + for (sql, end) in [ + ( + "CREATE RULE r AS ON INSERT TO s.t DO NOTHING", + Location::new(1, 34), + ), + ( + "CREATE RULE r AS ON INSERT TO s.t WHERE new.id > 0 DO NOTHING", + Location::new(1, 51), + ), + ( + "CREATE RULE r AS ON INSERT TO s.t DO SELECT 1", + Location::new(1, 46), + ), + ( + "CREATE RULE r AS ON INSERT TO s.t DO ()", + Location::new(1, 34), + ), + ( + r#"CREATE RULE r AS ON UPDATE TO s.t +WHERE old.id = new.id +DO ALSO ( + UPDATE audit SET id = new.id; + DELETE FROM archive WHERE id = old.id +)"#, + Location::new(5, 40), + ), + ] { + for dialect in &dialects.dialects { + let statements = Parser::parse_sql(dialect.as_ref(), sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, 13), end), + "{dialect:?} {sql}" + ); + } + } +} + +#[test] +fn display_create_rule_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter<'a>(&'a str); + + impl Write for RejectingWriter<'_> { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let dialects = all_dialects(); + let statement = dialects.verified_stmt( + "CREATE OR REPLACE RULE r AS ON UPDATE TO t WHERE id > 0 DO ALSO (DELETE FROM audit)", + ); + for rejected in ["CREATE ", "r", "UPDATE", "t", ">", " DO", "ALSO", "(", ")"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); + } + for (sql, rejected) in [ + ("CREATE RULE r AS ON INSERT TO t DO NOTHING", "NOTHING"), + ( + "CREATE RULE r AS ON DELETE TO t DO DELETE FROM audit", + "audit", + ), + ] { + let statement = dialects.verified_stmt(sql); + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); + } +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0a3866d29f..9ad5b5a4b3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10265,3 +10265,14 @@ fn parse_geometric_typed_literal_error_propagates() { ParserError::ParserError("Expected: a value, found: (".to_string()) ); } + +#[test] +fn parse_create_rule() { + // Example from the PostgreSQL documentation. NOTIFY parsing is limited to + // dialects with `supports_listen_notify`. + pg().verified_stmt("CREATE RULE notify_me AS ON UPDATE TO mytable DO ALSO NOTIFY mytable"); + // OLD and NEW parse as plain identifiers inside rule conditions and actions. + pg_and_generic().verified_stmt( + "CREATE RULE r AS ON UPDATE TO t WHERE old.balance <> new.balance DO INSTEAD UPDATE shadow SET balance = new.balance WHERE id = old.id", + ); +}