Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,27 @@ 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<bool>`. 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.

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,
Expand Down
13 changes: 13 additions & 0 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2237,6 +2237,12 @@ pub struct ConstraintCharacteristics {
pub initially: Option<DeferrableInitial>,
/// `[ ENFORCED | NOT ENFORCED ]`
pub enforced: Option<bool>,
/// `[ 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<bool>,
}

/// Initial setting for deferrable constraints (`INITIALLY IMMEDIATE` or `INITIALLY DEFERRED`).
Expand Down Expand Up @@ -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(())
}
}

Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ impl Spanned for ConstraintCharacteristics {
deferrable: _, // bool
initially: _, // enum
enforced: _, // bool
rely: _, // bool
} = self;

Span::empty()
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ impl Dialect for GenericDialect {
true
}

fn supports_constraint_reliance(&self) -> bool {
true
}

fn supports_key_column_option(&self) -> bool {
true
}
Expand Down
6 changes: 6 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ define_keywords!(
NOLOGIN,
NONE,
NOORDER,
NORELY,
NOREPLICATION,
NORMALIZE,
NORMALIZED,
Expand Down Expand Up @@ -867,6 +868,7 @@ define_keywords!(
RELAY,
RELEASE,
RELEASES,
RELY,
REMAINDER,
REMOTE,
REMOVE,
Expand Down
18 changes: 16 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<ConstraintCharacteristics>, ParserError> {
Expand All @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -4115,6 +4116,7 @@ fn parse_create_table_with_constraint_characteristics() {
deferrable: Some(true),
initially: Some(DeferrableInitial::Immediate),
enforced: None,
rely: None,
}),
}
.into(),
Expand All @@ -4131,6 +4133,7 @@ fn parse_create_table_with_constraint_characteristics() {
deferrable: Some(false),
initially: Some(DeferrableInitial::Deferred),
enforced: Some(false),
rely: None,
}),
}
.into(),
Expand All @@ -4147,6 +4150,7 @@ fn parse_create_table_with_constraint_characteristics() {
deferrable: Some(false),
initially: Some(DeferrableInitial::Immediate),
enforced: Some(true),
rely: None,
}),
}
.into(),
Expand Down Expand Up @@ -4213,6 +4217,7 @@ fn parse_create_table_column_constraint_characteristics() {
deferrable,
initially,
enforced,
rely: None,
})
} else {
None
Expand Down
1 change: 1 addition & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6433,6 +6433,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() {
deferrable: Some(true),
initially: Some(DeferrableInitial::Deferred),
enforced: None,
rely: None,
}),
});

Expand Down
86 changes: 86 additions & 0 deletions tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7068,6 +7068,92 @@ fn snowflake_create_schema_options_serde_round_trip_and_default() {
assert_eq!(serde_json::from_value::<Statement>(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());
}

#[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::<ConstraintCharacteristics>(serialized).unwrap(),
with_reliance
);
}
}

#[cfg(feature = "visitor")]
#[test]
fn snowflake_create_schema_options_expose_literal_expressions_to_visitors() {
Expand Down
Loading