From 571aba96b558523ae0eb3cba094ec9c109e28a0e Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Sun, 15 Nov 2026 05:00:29 +0200 Subject: [PATCH 1/3] test: define Snowflake constraint RELY and NORELY syntax --- tests/sqlparser_snowflake.rs | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c1f8285ca7..ebe5fd4aba 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -7068,6 +7068,50 @@ fn snowflake_create_schema_options_serde_round_trip_and_default() { assert_eq!(serde_json::from_value::(old).unwrap(), plain); } +#[test] +fn snowflake_constraint_reliance_round_trips() { + for sql in [ + "CREATE TABLE NE (ID INT PRIMARY KEY NOT ENFORCED RELY)", + "CREATE TABLE t (id INT PRIMARY KEY NORELY)", + "CREATE TABLE t (id INT UNIQUE RELY)", + "CREATE TABLE t (id INT REFERENCES parent (id) NOT ENFORCED RELY)", + "CREATE TABLE t (id INT, PRIMARY KEY (id) RELY)", + "CREATE TABLE t (id INT, CONSTRAINT uk UNIQUE (id) NOT ENFORCED NORELY)", + "CREATE TABLE t (id INT, FOREIGN KEY (id) REFERENCES parent (id) RELY)", + ] { + snowflake_and_generic().verified_stmt(sql); + } +} + +#[test] +fn snowflake_constraint_reliance_preserves_statement_boundaries() { + let statements = sqlparser::parser::Parser::parse_sql( + &SnowflakeDialect {}, + "CREATE TABLE t (id INT PRIMARY KEY NOT ENFORCED RELY); SELECT 1", + ) + .unwrap(); + assert_eq!(statements.len(), 2); + assert!(matches!(statements[1], Statement::Query(_))); + for sql in [ + "CREATE TABLE t (id INT PRIMARY KEY RELY RELY)", + "CREATE TABLE t (id INT PRIMARY KEY RELY NORELY)", + "CREATE TABLE t (id INT PRIMARY KEY NORELY RELY)", + "CREATE TABLE t (id INT PRIMARY KEY NOT RELY)", + ] { + assert!(sqlparser::parser::Parser::parse_sql(&SnowflakeDialect {}, sql).is_err()); + } +} + +#[test] +fn snowflake_constraint_reliance_does_not_change_quoted_identifiers_or_other_dialects() { + snowflake_and_generic().verified_stmt(r#"CREATE TABLE "RELY" ("NORELY" INT PRIMARY KEY)"#); + assert!(sqlparser::parser::Parser::parse_sql( + &sqlparser::dialect::PostgreSqlDialect {}, + "CREATE TABLE t (id INT PRIMARY KEY RELY)", + ) + .is_err()); +} + #[cfg(feature = "visitor")] #[test] fn snowflake_create_schema_options_expose_literal_expressions_to_visitors() { From 05e0803bfade9fc0f3dda495feac2e21fbbe8ab0 Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Sun, 15 Nov 2026 05:38:52 +0200 Subject: [PATCH 2/3] feat(snowflake): preserve typed constraint RELY and NORELY --- AGENTS.md | 6 +++++ README.md | 13 +++++++++++ src/ast/ddl.rs | 13 +++++++++++ src/ast/spans.rs | 1 + src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 6 +++++ src/dialect/snowflake.rs | 4 ++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 18 +++++++++++++-- tests/sqlparser_common.rs | 7 +++++- tests/sqlparser_postgres.rs | 1 + tests/sqlparser_snowflake.rs | 44 +++++++++++++++++++++++++++++++++++- 12 files changed, 115 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5bc24790bb..68081ddc9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,12 @@ perform storage IO, evaluate options, or enforce an engine's transfer policy. You can try to simplify the SQL statement to identify the root cause of the parsing issue. This may involve removing certain clauses or components of the SQL statement to see if it can be parsed successfully. Additionally, you can compare the problematic SQL statement with similar statements that are parsed correctly to identify any differences that may be causing the issue. ## Pre Commit Checks + +Snowflake key reliance has its own optional `ConstraintCharacteristics::rely` field +and `Dialect::supports_constraint_reliance` capability. Keep RELY/NORELY distinct +from enforcement and deferrability, preserve absent-field serde compatibility and +statement boundaries, and never infer optimizer or execution semantics from syntax. + Run the following commands before you commit to ensure the change will pass the CI process: ```bash cargo test --all-features diff --git a/README.md b/README.md index 235c63d3d9..4738274762 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,19 @@ complete Snowflake workflow. In particular, path traversal/backslash admission, remain semantic responsibilities. These additions require owning qualification; source tests alone do not establish runtime or reference parity. +## Snowflake constraint reliance + +Snowflake and generic dialects retain `RELY`/`NORELY` on informational key +declarations as `ConstraintCharacteristics::rely: Option`. For example, +`CREATE TABLE NE (ID INT PRIMARY KEY NOT ENFORCED RELY)` keeps enforcement and +reliance as separate fields and round-trips both clauses. Duplicate or conflicting +reliance clauses are syntax errors. Other dialects retain their existing grammar. + +This is an additive public AST field: consumers constructing characteristics must +initialize it. Old serialized characteristics omit it and deserialize as `None`; +serializing `None` preserves the old shape. The parser does not infer constraint +enforcement, optimizer trust, privileges or engine execution support. + ## Preserves Syntax Round Trip This crate allows users to recover the original SQL text (with comments removed, diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a6f73e90cc..2a4288f608 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2237,6 +2237,12 @@ pub struct ConstraintCharacteristics { pub initially: Option, /// `[ ENFORCED | NOT ENFORCED ]` pub enforced: Option, + /// `[ RELY | NORELY ]`. Syntax only; the consumer decides whether to trust it. + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Option::is_none") + )] + pub rely: Option, } /// Initial setting for deferrable constraints (`INITIALLY IMMEDIATE` or `INITIALLY DEFERRED`). @@ -2299,7 +2305,14 @@ impl fmt::Display for ConstraintCharacteristics { (Some(deferrable), Some(initial), Some(enforced)) => { write!(f, "{deferrable} {initial} {enforced}") } + }?; + if let Some(rely) = self.rely { + if deferrable.is_some() || initially_immediate.is_some() || enforced.is_some() { + write!(f, " ")?; + } + write!(f, "{}", if rely { "RELY" } else { "NORELY" })?; } + Ok(()) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index c0fe578ce8..f950dd7255 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -889,6 +889,7 @@ impl Spanned for ConstraintCharacteristics { deferrable: _, // bool initially: _, // enum enforced: _, // bool + rely: _, // bool } = self; Span::empty() diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 1ed4584db7..7680843ed5 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -297,6 +297,10 @@ impl Dialect for GenericDialect { true } + fn supports_constraint_reliance(&self) -> bool { + true + } + fn supports_key_column_option(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 94c960d943..24014af8d4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1323,6 +1323,12 @@ pub trait Dialect: Debug + Any { false } + /// Supports the `RELY` and `NORELY` constraint characteristics. + /// These record syntax without inferring enforcement or optimizer behavior. + fn supports_constraint_reliance(&self) -> bool { + false + } + /// Returns true if the dialect supports the `KEY` keyword as part of /// column-level constraints in a `CREATE TABLE` statement. /// diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 27cf67994a..65a8b652e7 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -234,6 +234,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_constraint_reliance(&self) -> bool { + true + } + /// See [doc](https://docs.snowflake.com/en/sql-reference/functions/extract) fn supports_extract_comma_syntax(&self) -> bool { true diff --git a/src/keywords.rs b/src/keywords.rs index 8a9cb0a6a8..8fbcb1cf30 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -698,6 +698,7 @@ define_keywords!( NOLOGIN, NONE, NOORDER, + NORELY, NOREPLICATION, NORMALIZE, NORMALIZED, @@ -867,6 +868,7 @@ define_keywords!( RELAY, RELEASE, RELEASES, + RELY, REMAINDER, REMOTE, REMOVE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8d917c4a31..788aca08fd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9742,7 +9742,7 @@ impl<'a> Parser<'a> { }) } - /// Parse optional constraint characteristics such as `DEFERRABLE`, `INITIALLY` and `ENFORCED`. + /// Parse optional constraint characteristics, including dialect-supported `RELY`/`NORELY`. pub fn parse_constraint_characteristics( &mut self, ) -> Result, ParserError> { @@ -9768,12 +9768,26 @@ impl<'a> Parser<'a> { && self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED]) { cc.enforced = Some(false); + } else if cc.rely.is_none() + && self.dialect.supports_constraint_reliance() + && self.parse_keyword(Keyword::RELY) + { + cc.rely = Some(true); + } else if cc.rely.is_none() + && self.dialect.supports_constraint_reliance() + && self.parse_keyword(Keyword::NORELY) + { + cc.rely = Some(false); } else { break; } } - if cc.deferrable.is_some() || cc.initially.is_some() || cc.enforced.is_some() { + if cc.deferrable.is_some() + || cc.initially.is_some() + || cc.enforced.is_some() + || cc.rely.is_some() + { Ok(Some(cc)) } else { Ok(None) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f714f4b782..6794277ee8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4098,7 +4098,8 @@ fn parse_create_table_with_constraint_characteristics() { characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), - enforced: None + enforced: None, + rely: None, }), } .into(), @@ -4115,6 +4116,7 @@ fn parse_create_table_with_constraint_characteristics() { deferrable: Some(true), initially: Some(DeferrableInitial::Immediate), enforced: None, + rely: None, }), } .into(), @@ -4131,6 +4133,7 @@ fn parse_create_table_with_constraint_characteristics() { deferrable: Some(false), initially: Some(DeferrableInitial::Deferred), enforced: Some(false), + rely: None, }), } .into(), @@ -4147,6 +4150,7 @@ fn parse_create_table_with_constraint_characteristics() { deferrable: Some(false), initially: Some(DeferrableInitial::Immediate), enforced: Some(true), + rely: None, }), } .into(), @@ -4213,6 +4217,7 @@ fn parse_create_table_column_constraint_characteristics() { deferrable, initially, enforced, + rely: None, }) } else { None diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a020b10a8d..0317698531 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6433,6 +6433,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), enforced: None, + rely: None, }), }); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ebe5fd4aba..883b700792 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -7077,7 +7077,7 @@ fn snowflake_constraint_reliance_round_trips() { "CREATE TABLE t (id INT REFERENCES parent (id) NOT ENFORCED RELY)", "CREATE TABLE t (id INT, PRIMARY KEY (id) RELY)", "CREATE TABLE t (id INT, CONSTRAINT uk UNIQUE (id) NOT ENFORCED NORELY)", - "CREATE TABLE t (id INT, FOREIGN KEY (id) REFERENCES parent (id) RELY)", + "CREATE TABLE t (id INT, FOREIGN KEY (id) REFERENCES parent(id) RELY)", ] { snowflake_and_generic().verified_stmt(sql); } @@ -7112,6 +7112,48 @@ fn snowflake_constraint_reliance_does_not_change_quoted_identifiers_or_other_dia .is_err()); } +#[test] +fn snowflake_constraint_reliance_has_its_own_typed_characteristic() { + for (suffix, expected) in [("RELY", true), ("NORELY", false)] { + let sql = format!("CREATE TABLE t (id INT PRIMARY KEY NOT ENFORCED {suffix})"); + let Statement::CreateTable(create) = snowflake_and_generic().verified_stmt(&sql) else { + panic!("expected CREATE TABLE"); + }; + let ColumnOption::PrimaryKey(key) = &create.columns[0].options[0].option else { + panic!("expected inline primary key"); + }; + assert_eq!( + key.characteristics, + Some(ConstraintCharacteristics { + enforced: Some(false), + rely: Some(expected), + ..Default::default() + }) + ); + } +} + +#[cfg(feature = "serde")] +#[test] +fn snowflake_constraint_reliance_preserves_old_serde_shape() { + let old = serde_json::json!({"deferrable": null, "initially": null, "enforced": false}); + let characteristics: ConstraintCharacteristics = serde_json::from_value(old.clone()).unwrap(); + assert_eq!(characteristics.rely, None); + assert_eq!(serde_json::to_value(characteristics).unwrap(), old); + for rely in [true, false] { + let with_reliance = ConstraintCharacteristics { + rely: Some(rely), + ..characteristics + }; + let serialized = serde_json::to_value(with_reliance).unwrap(); + assert_eq!(serialized["rely"], rely); + assert_eq!( + serde_json::from_value::(serialized).unwrap(), + with_reliance + ); + } +} + #[cfg(feature = "visitor")] #[test] fn snowflake_create_schema_options_expose_literal_expressions_to_visitors() { From a718545ef0690683243a3430a060722626fe251c Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Sun, 15 Nov 2026 05:48:55 +0200 Subject: [PATCH 3/3] docs: record source-bound constraint reliance parser qualification --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 4738274762..1e4227f4ee 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,14 @@ initialize it. Old serialized characteristics omit it and deserialize as `None`; serializing `None` preserves the old shape. The parser does not infer constraint enforcement, optimizer trust, privileges or engine execution support. +The selected runtime source `05e0803b` has local all-feature qualification: +1,630 tests pass, zero fail and seven existing tests remain ignored, including +47 documentation tests. Configured Rust 1.89 all-target/all-feature Clippy and +format checks pass. The test-first baseline has two reliance failures and one +unchanged control pass; a later foreign-key test corrects only its existing +canonical whitespace expectation. Rustice still needs its consumer/pin and owning +runtime qualification before this grammar can close an engine compatibility gap. + ## Preserves Syntax Round Trip This crate allows users to recover the original SQL text (with comments removed,