Skip to content

Commit 0e5e26e

Browse files
committed
Snowflake: parse informational constraint properties (ENABLE/VALIDATE/RELY)
Extend ConstraintCharacteristics with the remaining three Snowflake constraint properties — { ENABLE | DISABLE }, { VALIDATE | NOVALIDATE } and { RELY | NORELY } — so they can be given in any order alongside DEFERRABLE / INITIALLY / ENFORCED on inline column constraints, out-of-line table constraints and ALTER TABLE ADD CONSTRAINT. Parsing is gated on a new Dialect::supports_informational_constraint_properties hook (Snowflake only), since ENABLE, DISABLE and VALIDATE are keywords used elsewhere. Display round-trips the full set. Also add TableConstraint::clear_characteristics(), for consumers whose target dialect has no grammar for the characteristics Snowflake accepts.
1 parent 0cb6820 commit 0e5e26e

10 files changed

Lines changed: 199 additions & 23 deletions

File tree

src/ast/ddl.rs

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2310,9 +2310,13 @@ pub(crate) fn display_option_spaced<T: fmt::Display>(option: &Option<T>) -> impl
23102310
display_option(" ", "", option)
23112311
}
23122312

2313-
/// `<constraint_characteristics> = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]`
2313+
/// `<constraint_characteristics> = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ] [ ENABLE | DISABLE ] [ VALIDATE | NOVALIDATE ] [ RELY | NORELY ]`
23142314
///
23152315
/// Used in UNIQUE and foreign key constraints. The individual settings may occur in any order.
2316+
///
2317+
/// `ENABLE`/`DISABLE`, `VALIDATE`/`NOVALIDATE` and `RELY`/`NORELY` are only
2318+
/// parsed for dialects returning true from
2319+
/// [`Dialect::supports_informational_constraint_properties`].
23162320
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Default, Eq, Ord, Hash)]
23172321
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23182322
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
@@ -2323,6 +2327,12 @@ pub struct ConstraintCharacteristics {
23232327
pub initially: Option<DeferrableInitial>,
23242328
/// `[ ENFORCED | NOT ENFORCED ]`
23252329
pub enforced: Option<bool>,
2330+
/// `[ ENABLE | DISABLE ]`
2331+
pub enabled: Option<bool>,
2332+
/// `[ VALIDATE | NOVALIDATE ]`
2333+
pub validated: Option<bool>,
2334+
/// `[ RELY | NORELY ]`
2335+
pub rely: Option<bool>,
23262336
}
23272337

23282338
/// Initial setting for deferrable constraints (`INITIALLY IMMEDIATE` or `INITIALLY DEFERRED`).
@@ -2366,26 +2376,35 @@ impl ConstraintCharacteristics {
23662376
},
23672377
)
23682378
}
2379+
2380+
fn enabled_text(&self) -> Option<&'static str> {
2381+
self.enabled
2382+
.map(|enabled| if enabled { "ENABLE" } else { "DISABLE" })
2383+
}
2384+
2385+
fn validated_text(&self) -> Option<&'static str> {
2386+
self.validated
2387+
.map(|validated| if validated { "VALIDATE" } else { "NOVALIDATE" })
2388+
}
2389+
2390+
fn rely_text(&self) -> Option<&'static str> {
2391+
self.rely.map(|rely| if rely { "RELY" } else { "NORELY" })
2392+
}
23692393
}
23702394

23712395
impl fmt::Display for ConstraintCharacteristics {
23722396
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2373-
let deferrable = self.deferrable_text();
2374-
let initially_immediate = self.initially_immediate_text();
2375-
let enforced = self.enforced_text();
2376-
2377-
match (deferrable, initially_immediate, enforced) {
2378-
(None, None, None) => Ok(()),
2379-
(None, None, Some(enforced)) => write!(f, "{enforced}"),
2380-
(None, Some(initial), None) => write!(f, "{initial}"),
2381-
(None, Some(initial), Some(enforced)) => write!(f, "{initial} {enforced}"),
2382-
(Some(deferrable), None, None) => write!(f, "{deferrable}"),
2383-
(Some(deferrable), None, Some(enforced)) => write!(f, "{deferrable} {enforced}"),
2384-
(Some(deferrable), Some(initial), None) => write!(f, "{deferrable} {initial}"),
2385-
(Some(deferrable), Some(initial), Some(enforced)) => {
2386-
write!(f, "{deferrable} {initial} {enforced}")
2387-
}
2388-
}
2397+
let properties = [
2398+
self.deferrable_text(),
2399+
self.initially_immediate_text(),
2400+
self.enforced_text(),
2401+
self.enabled_text(),
2402+
self.validated_text(),
2403+
self.rely_text(),
2404+
];
2405+
2406+
let set: Vec<&str> = properties.into_iter().flatten().collect();
2407+
write!(f, "{}", display_separated(&set, " "))
23892408
}
23902409
}
23912410

src/ast/spans.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,9 @@ impl Spanned for ConstraintCharacteristics {
978978
deferrable: _, // bool
979979
initially: _, // enum
980980
enforced: _, // bool
981+
enabled: _, // bool
982+
validated: _, // bool
983+
rely: _, // bool
981984
} = self;
982985

983986
Span::empty()

src/ast/table_constraints.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ impl From<FullTextOrSpatialConstraint> for TableConstraint {
155155
}
156156
}
157157

158+
impl TableConstraint {
159+
/// Drop any [`ConstraintCharacteristics`] the constraint carries, so it
160+
/// renders as the bare constraint. Useful for dialects whose grammar has no
161+
/// counterpart for the characteristics the source dialect accepts.
162+
pub fn clear_characteristics(&mut self) {
163+
match self {
164+
TableConstraint::Unique(constraint) => constraint.characteristics = None,
165+
TableConstraint::PrimaryKey(constraint) => constraint.characteristics = None,
166+
TableConstraint::ForeignKey(constraint) => constraint.characteristics = None,
167+
TableConstraint::PrimaryKeyUsingIndex(constraint)
168+
| TableConstraint::UniqueUsingIndex(constraint) => constraint.characteristics = None,
169+
TableConstraint::Check(_)
170+
| TableConstraint::Index(_)
171+
| TableConstraint::FulltextOrSpatial(_) => {}
172+
}
173+
}
174+
}
175+
158176
impl fmt::Display for TableConstraint {
159177
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160178
match self {

src/dialect/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,21 @@ pub trait Dialect: Debug + Any {
12651265
false
12661266
}
12671267

1268+
/// Returns true if the dialect accepts the informational constraint properties
1269+
/// `{ ENABLE | DISABLE }`, `{ VALIDATE | NOVALIDATE }` and `{ RELY | NORELY }`
1270+
/// alongside `DEFERRABLE` / `INITIALLY` / `ENFORCED` in constraint
1271+
/// characteristics.
1272+
///
1273+
/// Example:
1274+
/// ```sql
1275+
/// CREATE TABLE t (a INT, CONSTRAINT pk PRIMARY KEY (a) NOT ENFORCED RELY)
1276+
/// ```
1277+
///
1278+
/// <https://docs.snowflake.com/en/sql-reference/constraints-properties>
1279+
fn supports_informational_constraint_properties(&self) -> bool {
1280+
false
1281+
}
1282+
12681283
/// Returns true if the dialect supports the `CONSTRAINT` keyword without a name
12691284
/// in table constraint definitions.
12701285
///

src/dialect/snowflake.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,11 @@ impl Dialect for SnowflakeDialect {
915915
true
916916
}
917917

918+
/// See: <https://docs.snowflake.com/en/sql-reference/constraints-properties>
919+
fn supports_informational_constraint_properties(&self) -> bool {
920+
true
921+
}
922+
918923
/// See: <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>
919924
fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
920925
&RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR

src/keywords.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,7 @@ define_keywords!(
733733
NOLOGIN,
734734
NONE,
735735
NOORDER,
736+
NORELY,
736737
NOREPLICATION,
737738
NORMALIZE,
738739
NORMALIZED,
@@ -742,6 +743,7 @@ define_keywords!(
742743
NOTHING,
743744
NOTIFY,
744745
NOTNULL,
746+
NOVALIDATE,
745747
NOWAIT,
746748
NO_WRITE_TO_BINLOG,
747749
NTH_VALUE,
@@ -909,6 +911,7 @@ define_keywords!(
909911
RELAY,
910912
RELEASE,
911913
RELEASES,
914+
RELY,
912915
REMAINDER,
913916
REMOTE,
914917
REMOVE,

src/parser/mod.rs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10403,16 +10403,62 @@ impl<'a> Parser<'a> {
1040310403
&& self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED])
1040410404
{
1040510405
cc.enforced = Some(false);
10406-
} else {
10406+
} else if !self.parse_informational_constraint_property(&mut cc) {
1040710407
break;
1040810408
}
1040910409
}
1041010410

10411-
if cc.deferrable.is_some() || cc.initially.is_some() || cc.enforced.is_some() {
10412-
Ok(Some(cc))
10413-
} else {
10411+
if cc == ConstraintCharacteristics::default() {
1041410412
Ok(None)
10413+
} else {
10414+
Ok(Some(cc))
10415+
}
10416+
}
10417+
10418+
/// Parse one of the informational constraint properties `{ ENABLE | DISABLE }`,
10419+
/// `{ VALIDATE | NOVALIDATE }` or `{ RELY | NORELY }`, returning whether one was
10420+
/// consumed. Only dialects opting in via
10421+
/// [`Dialect::supports_informational_constraint_properties`] accept them, as
10422+
/// `ENABLE`, `DISABLE` and `VALIDATE` are keywords used elsewhere.
10423+
fn parse_informational_constraint_property(
10424+
&mut self,
10425+
cc: &mut ConstraintCharacteristics,
10426+
) -> bool {
10427+
if !self.dialect.supports_informational_constraint_properties() {
10428+
return false;
10429+
}
10430+
10431+
if cc.enabled.is_none() {
10432+
if self.parse_keyword(Keyword::ENABLE) {
10433+
cc.enabled = Some(true);
10434+
return true;
10435+
}
10436+
if self.parse_keyword(Keyword::DISABLE) {
10437+
cc.enabled = Some(false);
10438+
return true;
10439+
}
10440+
}
10441+
if cc.validated.is_none() {
10442+
if self.parse_keyword(Keyword::VALIDATE) {
10443+
cc.validated = Some(true);
10444+
return true;
10445+
}
10446+
if self.parse_keyword(Keyword::NOVALIDATE) {
10447+
cc.validated = Some(false);
10448+
return true;
10449+
}
10450+
}
10451+
if cc.rely.is_none() {
10452+
if self.parse_keyword(Keyword::RELY) {
10453+
cc.rely = Some(true);
10454+
return true;
10455+
}
10456+
if self.parse_keyword(Keyword::NORELY) {
10457+
cc.rely = Some(false);
10458+
return true;
10459+
}
1041510460
}
10461+
false
1041610462
}
1041710463

1041810464
/// Parse an optional table constraint (e.g. `PRIMARY KEY`, `UNIQUE`, `FOREIGN KEY`, `CHECK`).

tests/sqlparser_common.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4111,7 +4111,8 @@ fn parse_create_table_with_constraint_characteristics() {
41114111
characteristics: Some(ConstraintCharacteristics {
41124112
deferrable: Some(true),
41134113
initially: Some(DeferrableInitial::Deferred),
4114-
enforced: None
4114+
enforced: None,
4115+
..Default::default()
41154116
}),
41164117
}
41174118
.into(),
@@ -4128,6 +4129,7 @@ fn parse_create_table_with_constraint_characteristics() {
41284129
deferrable: Some(true),
41294130
initially: Some(DeferrableInitial::Immediate),
41304131
enforced: None,
4132+
..Default::default()
41314133
}),
41324134
}
41334135
.into(),
@@ -4144,6 +4146,7 @@ fn parse_create_table_with_constraint_characteristics() {
41444146
deferrable: Some(false),
41454147
initially: Some(DeferrableInitial::Deferred),
41464148
enforced: Some(false),
4149+
..Default::default()
41474150
}),
41484151
}
41494152
.into(),
@@ -4160,6 +4163,7 @@ fn parse_create_table_with_constraint_characteristics() {
41604163
deferrable: Some(false),
41614164
initially: Some(DeferrableInitial::Immediate),
41624165
enforced: Some(true),
4166+
..Default::default()
41634167
}),
41644168
}
41654169
.into(),
@@ -4226,6 +4230,7 @@ fn parse_create_table_column_constraint_characteristics() {
42264230
deferrable,
42274231
initially,
42284232
enforced,
4233+
..Default::default()
42294234
})
42304235
} else {
42314236
None

tests/sqlparser_postgres.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6444,6 +6444,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() {
64446444
deferrable: Some(true),
64456445
initially: Some(DeferrableInitial::Deferred),
64466446
enforced: None,
6447+
..Default::default()
64476448
}),
64486449
});
64496450

tests/sqlparser_snowflake.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use sqlparser::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionK
2323
use sqlparser::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageLoadSelectItemKind};
2424
use sqlparser::ast::*;
2525
use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect};
26-
use sqlparser::parser::{ParserError, ParserOptions};
26+
use sqlparser::parser::{Parser, ParserError, ParserOptions};
2727
use sqlparser::tokenizer::*;
2828
use test_utils::*;
2929

@@ -103,6 +103,67 @@ fn parse_sf_create_stream_on_table_and_view() {
103103
}
104104
}
105105

106+
#[test]
107+
fn parse_sf_informational_constraint_properties() {
108+
let canonical = "CREATE TABLE t (id INT, CONSTRAINT pk PRIMARY KEY (id) NOT ENFORCED DISABLE NOVALIDATE RELY)";
109+
match snowflake().verified_stmt(canonical) {
110+
Statement::CreateTable(CreateTable { constraints, .. }) => match &constraints[0] {
111+
TableConstraint::PrimaryKey(pk) => assert_eq!(
112+
pk.characteristics,
113+
Some(ConstraintCharacteristics {
114+
deferrable: None,
115+
initially: None,
116+
enforced: Some(false),
117+
enabled: Some(false),
118+
validated: Some(false),
119+
rely: Some(true),
120+
})
121+
),
122+
other => panic!("unexpected constraint: {other}"),
123+
},
124+
other => panic!("unexpected statement: {other}"),
125+
}
126+
127+
// The properties are interchangeable in order.
128+
snowflake().one_statement_parses_to(
129+
"CREATE TABLE t (id INT, CONSTRAINT pk PRIMARY KEY (id) RELY DISABLE NOVALIDATE NOT ENFORCED)",
130+
canonical,
131+
);
132+
133+
snowflake().verified_stmt("CREATE TABLE t (id INT PRIMARY KEY NOT ENFORCED RELY)");
134+
snowflake().verified_stmt("CREATE TABLE t (id INT UNIQUE ENABLE VALIDATE NORELY)");
135+
snowflake()
136+
.verified_stmt("CREATE TABLE t (id INT, CONSTRAINT u UNIQUE (id) NOT ENFORCED NORELY)");
137+
snowflake().verified_stmt(
138+
"CREATE TABLE t (id INT, CONSTRAINT fk FOREIGN KEY (id) REFERENCES p(id) NOT ENFORCED RELY)",
139+
);
140+
snowflake().verified_stmt("ALTER TABLE t ADD CONSTRAINT u UNIQUE (id) NOT ENFORCED RELY");
141+
}
142+
143+
#[test]
144+
fn parse_sf_clear_constraint_characteristics() {
145+
let sql = "CREATE TABLE t (id INT, CONSTRAINT u UNIQUE (id) NOT ENFORCED RELY)";
146+
match snowflake().verified_stmt(sql) {
147+
Statement::CreateTable(CreateTable {
148+
mut constraints, ..
149+
}) => {
150+
constraints[0].clear_characteristics();
151+
assert_eq!(constraints[0].to_string(), "CONSTRAINT u UNIQUE (id)");
152+
}
153+
other => panic!("unexpected statement: {other}"),
154+
}
155+
}
156+
157+
#[test]
158+
fn parse_informational_constraint_properties_are_dialect_gated() {
159+
let sql = "CREATE TABLE t (id INT, CONSTRAINT pk PRIMARY KEY (id) RELY)";
160+
let err = Parser::parse_sql(&GenericDialect {}, sql).unwrap_err();
161+
assert_eq!(
162+
err.to_string(),
163+
"sql parser error: Expected: \',\' or \')\' after column definition, found: RELY at Line: 1, Column: 56"
164+
);
165+
}
166+
106167
#[test]
107168
fn test_snowflake_create_or_replace_table() {
108169
let sql = "CREATE OR REPLACE TABLE my_table (a number)";

0 commit comments

Comments
 (0)