From 868ce5156af56023d7d2a6028f7f32afac9a3250 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 7 Aug 2026 08:16:53 +0200 Subject: [PATCH 1/2] Add support for PostgreSQL CREATE RULE --- src/ast/ddl.rs | 146 +++++++++++++++++++++++++++++++++++- src/ast/mod.rs | 25 +++--- src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 78 +++++++++++++++++++ tests/sqlparser_common.rs | 68 +++++++++++++++++ tests/sqlparser_postgres.rs | 11 +++ 7 files changed, 318 insertions(+), 12 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a0e69ad8a..753f7a8e3 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -48,9 +48,9 @@ use crate::ast::{ 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, + 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; @@ -5994,3 +5994,143 @@ impl From for crate::ast::Statement { crate::ast::Statement::AlterPolicy(v) } } + +/// CREATE RULE statement. +/// +/// ```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) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8a9a67a74..0e9a4e5f6 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, 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, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, + 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, + TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, @@ -3775,6 +3776,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) @@ -5630,6 +5636,7 @@ impl fmt::Display for Statement { 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 a34fe66d9..a307b15cd 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -486,6 +486,7 @@ impl Spanned for Statement { Statement::Unload { .. } => Span::empty(), Statement::OptimizeTable { .. } => Span::empty(), Statement::CreatePolicy { .. } => Span::empty(), + Statement::CreateRule { .. } => Span::empty(), Statement::AlterPolicy { .. } => Span::empty(), Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index 0c50703c3..4614df826 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -112,6 +112,7 @@ define_keywords!( ALL, ALLOCATE, ALLOWOVERWRITE, + ALSO, ALTER, ALWAYS, ANALYZE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2b3f42bb..1aaa79130 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5266,6 +5266,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) { @@ -7244,6 +7246,82 @@ 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.expect_one_of_keywords(&[ + Keyword::SELECT, + Keyword::INSERT, + Keyword::UPDATE, + Keyword::DELETE, + ])? { + Keyword::SELECT => CreateRuleEvent::Select, + Keyword::INSERT => CreateRuleEvent::Insert, + Keyword::UPDATE => CreateRuleEvent::Update, + Keyword::DELETE => CreateRuleEvent::Delete, + unexpected_keyword => { + return Err(ParserError::ParserError(format!( + "Internal parser error: unexpected keyword `{unexpected_keyword}` in rule event" + ))) + } + }; + 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 0800bc41f..373dcbe98 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -19679,3 +19679,71 @@ fn parse_function_arg_call_chain_no_exponential_blowup() { rx.recv_timeout(Duration::from_secs(5)) .expect("parser should reject this quickly, not loop exponentially"); } + +#[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" + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a7128eafd..d228d91f2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9663,3 +9663,14 @@ fn parse_right_deep_join_chain() { // NATURAL JOIN followed by a constrained join must stay left-associative. pg().verified_stmt("SELECT * FROM t0 NATURAL JOIN t1 INNER JOIN t2 ON true"); } + +#[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", + ); +} From 4d83b17e62e1c5b862beaaa9737086b169dd331a Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 7 Aug 2026 08:28:04 +0200 Subject: [PATCH 2/2] Remove redundant explicit doc link target on Statement --- src/ast/ddl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 753f7a8e3..2edc16bbe 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"))]