squonk_ast/dialect/snowflake.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The Snowflake dialect preset (ANSI-derived, deliberately conservative).
5//!
6//! Snowflake diverges widely across its type, function, and statement surface, and —
7//! unlike the five shipped oracle-compared presets — this workspace has **no Snowflake
8//! oracle**, so over-acceptance cannot be measured. Conservatism is therefore the honesty
9//! bar: this preset derives from [`FeatureSet::ANSI`], the strict standard baseline, and
10//! enables only the Snowflake surface that already has a modelled, tested parser gate and
11//! clear documentary evidence. Every other axis keeps its ANSI value; a reader can predict
12//! from this module exactly what Snowflake accepts beyond the standard, and unsupported
13//! Snowflake syntax is a clean reject routed to a focused follow-up ticket, never a silent
14//! over-accept.
15//!
16//! # What this preset adds over ANSI
17//!
18//! Four grammar gates, each documented as Snowflake surface:
19//!
20//! - [`semi_structured_access`](ExpressionSyntax::semi_structured_access) — the
21//! `base:key[0].field` path syntax over `VARIANT`/`OBJECT`/`ARRAY` columns, Snowflake's
22//! signature semi-structured accessor. This is the flag this preset exists to expose (it
23//! shipped staged and dark, exercised only by a test dialect until now).
24//! - [`qualify`](SelectSyntax::qualify) — the `QUALIFY <predicate>` post-window filter.
25//! Snowflake ships `QUALIFY` and lists it in its reserved-keyword set, so the clause is
26//! admitted *and* `QUALIFY` is reserved in every identifier position (see
27//! [`SNOWFLAKE_QUALIFY_RESERVATION`]); the reservation is what lets `FROM t QUALIFY …`
28//! read the clause rather than a table alias named `qualify`.
29//! - [`group_by_all`](GroupingSyntax::group_by_all) — the `GROUP BY ALL` clause mode.
30//! Snowflake ships `GROUP BY ALL`; it does **not** ship `ORDER BY ALL`, so
31//! [`order_by_all`](GroupingSyntax::order_by_all) stays off (the two are separate flags for
32//! exactly this reason — the field doc names Snowflake as the engine that adopts one
33//! without the other).
34//! - [`connect_by_clause`](SelectSyntax::connect_by_clause) — the Oracle-style
35//! `START WITH … CONNECT BY [PRIOR] col = [PRIOR] col` hierarchical query clause
36//! (Snowflake `CONNECT BY` reference, the citable public grammar). Modelled as the
37//! Oracle superset — either clause order, the after-`WHERE` position, and `NOCYCLE`
38//! (which Snowflake's docs omit) — a documented conservative-direction over-acceptance;
39//! the `PRIOR` operator is scoped to the `CONNECT BY` condition alone.
40//!
41//! Snowflake's identifier lexis needs no delta: it folds unquoted identifiers to
42//! **upper**case ([`Casing::Upper`], already ANSI's value) and quotes with the standard
43//! `"…"` ([`STANDARD_IDENTIFIER_QUOTES`], likewise ANSI). The `semi_structured_access`
44//! path rides the `:` trigger, which contends with
45//! [`ParameterSyntax::named_colon`](super::ParameterSyntax::named_colon); Snowflake spells
46//! bind variables with `?`/`:name` *outside* SQL text and does not enable colon parameters
47//! in-grammar, so `named_colon` stays off (ANSI's value) and the `:` trigger has a single
48//! claimant — the lexical-consistency `const` assert below enforces it.
49
50use super::{
51 AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
52 ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
53 DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
54 IdentifierSyntax, IndexAlterSyntax, JoinSyntax, Keyword, KeywordOperators, KeywordSet,
55 MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
56 ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, RESERVED_BARE_ALIAS,
57 RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME, STANDARD_BYTE_CLASSES,
58 STANDARD_IDENTIFIER_QUOTES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
59 StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
60 TypeNameSyntax, UtilitySyntax,
61};
62use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
63
64/// `QUALIFY`, reserved by Snowflake (its documented reserved-keyword list rejects the word
65/// as any unquoted identifier). Unioned into all four per-position reject sets below,
66/// mirroring the engine-probed DuckDB profile: the bare-alias reservation is load-bearing
67/// for the grammar — it is what lets `FROM t QUALIFY …` read the clause instead of a table
68/// alias named `qualify`, and the column/function/type reservations match Snowflake's
69/// "reserved everywhere" status. `AS`-label position stays open (`SELECT 1 AS qualify`),
70/// keeping `reserved_as_label` empty like every ANSI-derived preset.
71pub const SNOWFLAKE_QUALIFY_RESERVATION: KeywordSet =
72 KeywordSet::from_keywords(&[Keyword::Qualify]);
73
74/// `PIVOT`, `UNPIVOT`, and `MATCH_RECOGNIZE` — Snowflake's FROM-clause table operators.
75/// Unlike [`SNOWFLAKE_QUALIFY_RESERVATION`], none of these is a Snowflake *reserved*
76/// keyword: all three are absent from Snowflake's reserved-keyword list and stay usable
77/// as ordinary unquoted identifiers
78/// (<https://docs.snowflake.com/en/sql-reference/reserved-keywords>). Each is instead
79/// *position-reserved* — recognized as an operator immediately after a table reference —
80/// so a bare factor must not swallow the keyword as a correlation alias:
81/// `FROM t PIVOT (…)` / `FROM t UNPIVOT (…)`
82/// (<https://docs.snowflake.com/en/sql-reference/constructs/pivot>,
83/// <https://docs.snowflake.com/en/sql-reference/constructs/unpivot>) and
84/// `FROM t MATCH_RECOGNIZE (…)`
85/// (<https://docs.snowflake.com/en/sql-reference/constructs/match_recognize>) all attach
86/// the operator directly to the `FROM` object.
87///
88/// As with BigQuery's `BIGQUERY_PIVOT_RESERVATION`, the
89/// reservation is confined to the `ColId` axis ([`SNOWFLAKE_RESERVED_COLUMN_NAME`]) — the
90/// load-bearing set for bare-alias reachability — and deliberately *not* the function,
91/// type, or projection bare-label axes, which Snowflake keeps open (`pivot(1)`,
92/// `CAST(1 AS pivot)`, `SELECT 1 pivot` still parse). This is the minimal deviation from
93/// the DuckDB `DUCKDB_PIVOT_RESERVATION` (all
94/// four positions, because DuckDB's engine genuinely reserves the words); QUALIFY, a real
95/// Snowflake reserved keyword, still rides all four via
96/// [`SNOWFLAKE_QUALIFY_RESERVATION`]. `MATCH_RECOGNIZE` rides Snowflake alone — BigQuery
97/// has no such operator. The reachability cost mirrors BigQuery's: under this preset an
98/// unquoted `pivot`/`unpivot`/`match_recognize` is not admitted as a column/table name or
99/// table alias — quote it (`"pivot"`) to use it as an identifier there.
100pub const SNOWFLAKE_TABLE_OPERATOR_RESERVATION: KeywordSet =
101 KeywordSet::from_keywords(&[Keyword::Pivot, Keyword::Unpivot, Keyword::MatchRecognize]);
102
103/// The ANSI column-name reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`] and the
104/// [`SNOWFLAKE_TABLE_OPERATOR_RESERVATION`] `ColId`-axis reservation.
105pub const SNOWFLAKE_RESERVED_COLUMN_NAME: KeywordSet = RESERVED_COLUMN_NAME
106 .union(SNOWFLAKE_QUALIFY_RESERVATION)
107 .union(SNOWFLAKE_TABLE_OPERATOR_RESERVATION);
108
109/// The ANSI function-name reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`].
110pub const SNOWFLAKE_RESERVED_FUNCTION_NAME: KeywordSet =
111 RESERVED_FUNCTION_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION);
112
113/// The ANSI type-name reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`].
114pub const SNOWFLAKE_RESERVED_TYPE_NAME: KeywordSet =
115 RESERVED_TYPE_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION);
116
117/// The ANSI bare-alias reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`].
118pub const SNOWFLAKE_RESERVED_BARE_ALIAS: KeywordSet =
119 RESERVED_BARE_ALIAS.union(SNOWFLAKE_QUALIFY_RESERVATION);
120
121impl SelectSyntax {
122 /// Snowflake SELECT surface: the ANSI baseline plus the documented Snowflake
123 /// clauses — the `QUALIFY <predicate>` post-window filter, the `GROUP BY ALL` clause
124 /// mode, and the Oracle-style `START WITH`/`CONNECT BY` hierarchical query clause.
125 /// `ORDER BY ALL` is deliberately *not* enabled: Snowflake ships `GROUP BY ALL`
126 /// without `ORDER BY ALL` (see [`order_by_all`](GroupingSyntax::order_by_all)).
127 /// Every other SELECT knob is conservatively ANSI.
128 pub const SNOWFLAKE: Self = Self {
129 qualify: true,
130 // Snowflake's `START WITH … CONNECT BY [PRIOR] col = [PRIOR] col` hierarchical
131 // query clause (Snowflake `CONNECT BY` reference — the citable public grammar,
132 // there being no Oracle preset). Modelled as the Oracle superset (either clause
133 // order, the after-`WHERE` position, `NOCYCLE`); see
134 // [`connect_by_clause`](SelectSyntax::connect_by_clause) for the acceptance bound.
135 connect_by_clause: true,
136 ..SelectSyntax::ANSI
137 };
138}
139
140impl QueryTailSyntax {
141 /// The `SNOWFLAKE` preset for query tail syntax.
142 pub const SNOWFLAKE: Self = Self {
143 ..QueryTailSyntax::ANSI
144 };
145}
146
147impl TableFactorSyntax {
148 /// Snowflake table-factor surface: the ANSI baseline plus the standard PIVOT table
149 /// factor (`FROM t PIVOT(<agg> FOR <col> IN (<vals> | ANY [ORDER BY …] | <subquery>)
150 /// [DEFAULT ON NULL (<expr>)]))`). Snowflake has no differential oracle here, so the
151 /// gate follows the documented grammar on this conservative preset (the `qualify`
152 /// precedent). The DuckDB [`pivot`](TableFactorSyntax::pivot) flag stays off — Snowflake has no
153 /// leading-keyword `PIVOT` statement, `IN <enum>`, or multi-`FOR`-column form.
154 pub const SNOWFLAKE: Self = Self {
155 pivot_value_sources: true,
156 // The SQL:2016 MATCH_RECOGNIZE row-pattern table factor (documented; no
157 // differential oracle here, so the gate follows the grammar on this
158 // conservative preset — the `qualify`/`pivot_value_sources` precedent).
159 match_recognize: true,
160 ..TableFactorSyntax::ANSI
161 };
162}
163
164impl GroupingSyntax {
165 /// The `SNOWFLAKE` preset for grouping syntax.
166 pub const SNOWFLAKE: Self = Self {
167 group_by_all: true,
168 ..GroupingSyntax::ANSI
169 };
170}
171
172impl UtilitySyntax {
173 /// Snowflake utility surface: the ANSI baseline plus the `COPY INTO` bulk
174 /// load/unload statement. Every other utility knob is conservatively ANSI — the
175 /// PostgreSQL/DuckDB `COPY`, `COMMENT ON`, the SQLite/MySQL statements, and the
176 /// prepared-statement lifecycle all stay off.
177 pub const SNOWFLAKE: Self = Self {
178 copy_into: true,
179 stage_references: true,
180 ..UtilitySyntax::ANSI
181 };
182}
183
184impl ExpressionSyntax {
185 /// Snowflake expression surface: the ANSI baseline plus semi-structured path access
186 /// (`base:key[0].field`), Snowflake's `VARIANT`/`OBJECT`/`ARRAY` accessor. Every other
187 /// expression knob is conservatively ANSI.
188 pub const SNOWFLAKE: Self = Self {
189 semi_structured_access: true,
190 ..ExpressionSyntax::ANSI
191 };
192}
193
194impl FeatureSet {
195 /// Snowflake as ANSI-derived dialect data (see the module docs for the full derivation
196 /// rationale and the conservatism bar).
197 pub const SNOWFLAKE: Self = Self {
198 // Snowflake folds unquoted identifiers to uppercase — the classic upper-folding
199 // dialect, which is already ANSI's value (kept explicit as the documented fact).
200 identifier_casing: Casing::Upper,
201 // Standard `"…"` identifier quoting; Snowflake adds no second delimiter.
202 identifier_quotes: STANDARD_IDENTIFIER_QUOTES,
203 default_null_ordering: NullOrdering::NullsLast,
204 // The reserved-set delta over ANSI: `QUALIFY` is reserved in every identifier
205 // position (Snowflake's documented reserved-keyword status), which the `QUALIFY`
206 // clause gate depends on to disambiguate `FROM t QUALIFY …`; and the FROM-clause
207 // table operators `PIVOT`/`UNPIVOT`/`MATCH_RECOGNIZE` are position-reserved on the
208 // `ColId` axis only (see [`SNOWFLAKE_TABLE_OPERATOR_RESERVATION`]) so a bare
209 // `FROM t PIVOT (…)` / `FROM t MATCH_RECOGNIZE (…)` reaches the operator instead of
210 // aliasing the source. The operators are not Snowflake reserved keywords, so — unlike
211 // QUALIFY — they stay out of the function/type/bare-label reject sets below.
212 reserved_column_name: SNOWFLAKE_RESERVED_COLUMN_NAME,
213 reserved_function_name: SNOWFLAKE_RESERVED_FUNCTION_NAME,
214 reserved_type_name: SNOWFLAKE_RESERVED_TYPE_NAME,
215 reserved_bare_alias: SNOWFLAKE_RESERVED_BARE_ALIAS,
216 reserved_as_label: KeywordSet::EMPTY,
217 catalog_qualified_names: true,
218 byte_classes: STANDARD_BYTE_CLASSES,
219 binding_powers: STANDARD_BINDING_POWERS,
220 set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
221 // Conservative ANSI string/number/parameter surface: Snowflake's own forms
222 // (`$$…$$` dollar-quoting, backslash escapes, `{name:Type}`-free bind syntax) have
223 // no modelled gate here and are deferred rather than guessed at without an oracle.
224 // Crucially `named_colon` stays off so the `:` trigger belongs solely to
225 // `semi_structured_access` (the lexical-consistency assert below enforces it).
226 string_literals: StringLiteralSyntax::ANSI,
227 numeric_literals: NumericLiteralSyntax::ANSI,
228 parameters: ParameterSyntax::ANSI,
229 session_variables: SessionVariableSyntax::ANSI,
230 identifier_syntax: IdentifierSyntax::ANSI,
231 // ANSI table-expression surface plus the PartiQL / SUPER table-position JSON path
232 // (`FROM src[0].a`), sqlparser-rs's `supports_partiql`.
233 table_expressions: TableExpressionSyntax {
234 table_json_path: true,
235 ..TableExpressionSyntax::ANSI
236 },
237 join_syntax: JoinSyntax::ANSI,
238 table_factor_syntax: TableFactorSyntax::SNOWFLAKE,
239 // The semi-structured path accessor — the capstone this preset exposes.
240 expression_syntax: ExpressionSyntax::SNOWFLAKE,
241 operator_syntax: OperatorSyntax::ANSI,
242 call_syntax: CallSyntax::ANSI,
243 string_func_forms: StringFuncForms::ANSI,
244 aggregate_call_syntax: AggregateCallSyntax::ANSI,
245 predicate_syntax: PredicateSyntax::ANSI,
246 pipe_operator: PipeOperator::StringConcat,
247 double_ampersand: DoubleAmpersand::Unsupported,
248 keyword_operators: KeywordOperators::Unsupported,
249 caret_operator: CaretOperator::Unsupported,
250 hash_bitwise_xor: false,
251 comment_syntax: CommentSyntax::ANSI,
252 mutation_syntax: MutationSyntax::ANSI,
253 statement_ddl_gates: StatementDdlGates::ANSI,
254 create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
255 column_definition_syntax: ColumnDefinitionSyntax::ANSI,
256 constraint_syntax: ConstraintSyntax::ANSI,
257 index_alter_syntax: IndexAlterSyntax::ANSI,
258 existence_guards: ExistenceGuards::ANSI,
259 // The `QUALIFY` and `GROUP BY ALL` clauses.
260 select_syntax: SelectSyntax::SNOWFLAKE,
261 query_tail_syntax: QueryTailSyntax::SNOWFLAKE,
262 grouping_syntax: GroupingSyntax::SNOWFLAKE,
263 // The `COPY INTO` bulk load/unload statement.
264 utility_syntax: UtilitySyntax::SNOWFLAKE,
265 show_syntax: ShowSyntax::ANSI,
266 maintenance_syntax: MaintenanceSyntax::ANSI,
267 access_control_syntax: AccessControlSyntax::ANSI,
268 type_name_syntax: TypeNameSyntax::ANSI,
269 // No Snowflake-specific Tier-1 output spelling yet; render the portable ANSI
270 // canonical type names (a `TargetSpelling::Snowflake` is render work a later
271 // ticket owns).
272 target_spelling: TargetSpelling::Ansi,
273 };
274}
275
276/// Prefer [`FeatureSet::SNOWFLAKE`] for struct update.
277pub const SNOWFLAKE: FeatureSet = FeatureSet::SNOWFLAKE;
278
279// Compile-time proof the Snowflake preset claims no shared tokenizer trigger twice. Its
280// one contended trigger is `:` — claimed by `semi_structured_access` here and by
281// `named_colon` when on — and the preset keeps `named_colon` off (ANSI's value), so `:`
282// has a single claimant. Every other delta is a contextual grammar gate or a keyword
283// reservation with no tokenizer trigger. Kept as a ratchet so a future Snowflake delta
284// that *does* add a contending trigger (e.g. enabling colon parameters) fails the build
285// here.
286const _: () = assert!(FeatureSet::SNOWFLAKE.is_lexically_consistent());
287// The two sibling self-consistency registries are ratcheted the same way, so the
288// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
289// flag rides an unset base, and no two features contend for one parser-position head.
290const _: () = assert!(FeatureSet::SNOWFLAKE.has_satisfied_feature_dependencies());
291const _: () = assert!(FeatureSet::SNOWFLAKE.has_no_grammar_conflict());
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn snowflake_is_ansi_plus_the_four_gates_and_the_qualify_reservation() {
299 // The preset is ANSI with a documented, closed set of divergent axes: the grammar
300 // gates (over two sub-presets), the `COPY INTO` utility gate, the `QUALIFY`
301 // reservation, and the `PIVOT`/`UNPIVOT`/`MATCH_RECOGNIZE` table-operator `ColId`
302 // reservation. Asserting the whole rest equals ANSI keeps the "ANSI-derived, every
303 // delta documented" claim honest against a future stray edit. Bind to locals so the
304 // const reads are not flagged by clippy's `assertions_on_constants`.
305 let ansi = FeatureSet::ANSI;
306 let sf = FeatureSet::SNOWFLAKE;
307
308 // The two divergent sub-presets.
309 assert_eq!(sf.select_syntax, SelectSyntax::SNOWFLAKE);
310 assert_ne!(sf.select_syntax, ansi.select_syntax);
311 assert_eq!(sf.expression_syntax, ExpressionSyntax::SNOWFLAKE);
312 assert_ne!(sf.expression_syntax, ansi.expression_syntax);
313
314 // The reserved-set delta: QUALIFY in every identifier position (a real Snowflake
315 // reserved keyword) plus the `PIVOT`/`UNPIVOT`/`MATCH_RECOGNIZE` table operators on
316 // the `ColId` axis only (position-reserved, not reserved keywords).
317 assert_eq!(sf.reserved_column_name, SNOWFLAKE_RESERVED_COLUMN_NAME);
318 assert_ne!(sf.reserved_column_name, ansi.reserved_column_name);
319 assert_eq!(sf.reserved_function_name, SNOWFLAKE_RESERVED_FUNCTION_NAME);
320 assert_ne!(sf.reserved_function_name, ansi.reserved_function_name);
321 assert_eq!(sf.reserved_type_name, SNOWFLAKE_RESERVED_TYPE_NAME);
322 assert_ne!(sf.reserved_type_name, ansi.reserved_type_name);
323 assert_eq!(sf.reserved_bare_alias, SNOWFLAKE_RESERVED_BARE_ALIAS);
324 assert_ne!(sf.reserved_bare_alias, ansi.reserved_bare_alias);
325 // Dropping QUALIFY *and* the table operators recovers the ANSI column set verbatim.
326 assert_eq!(
327 sf.reserved_column_name
328 .difference(SNOWFLAKE_QUALIFY_RESERVATION)
329 .difference(SNOWFLAKE_TABLE_OPERATOR_RESERVATION),
330 ansi.reserved_column_name,
331 );
332 assert!(sf.reserved_column_name.contains(Keyword::Qualify));
333 assert!(sf.reserved_bare_alias.contains(Keyword::Qualify));
334 // The table operators are `ColId`-only: reserved as a column/table name and table
335 // alias (bare-alias reachability) but not as a function name, type name, or projection
336 // bare label (Snowflake's non-reserved status — `pivot(1)`, `SELECT 1 pivot` parse).
337 for kw in [Keyword::Pivot, Keyword::Unpivot, Keyword::MatchRecognize] {
338 assert!(sf.reserved_column_name.contains(kw));
339 assert!(!sf.reserved_function_name.contains(kw));
340 assert!(!sf.reserved_type_name.contains(kw));
341 assert!(!sf.reserved_bare_alias.contains(kw));
342 }
343 // The function/type/bare-label sets carry exactly the QUALIFY delta — no table
344 // operator leaks in.
345 assert_eq!(
346 sf.reserved_function_name,
347 RESERVED_FUNCTION_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION),
348 );
349 assert_eq!(
350 sf.reserved_type_name,
351 RESERVED_TYPE_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION),
352 );
353 assert_eq!(
354 sf.reserved_bare_alias,
355 RESERVED_BARE_ALIAS.union(SNOWFLAKE_QUALIFY_RESERVATION),
356 );
357 // `AS`-label position stays open (`SELECT 1 AS qualify`).
358 assert_eq!(sf.reserved_as_label, KeywordSet::EMPTY);
359
360 // Snowflake's identifier lexis needs no delta — both facts equal ANSI's.
361 assert_eq!(sf.identifier_casing, Casing::Upper);
362 assert_eq!(sf.identifier_casing, ansi.identifier_casing);
363 assert_eq!(sf.identifier_quotes, ansi.identifier_quotes);
364 // `named_colon` off is the interplay the semi-structured accessor depends on.
365 assert!(!sf.parameters.named_colon);
366
367 // Everything else is inherited verbatim from ANSI.
368 assert_eq!(sf.string_literals, ansi.string_literals);
369 assert_eq!(sf.numeric_literals, ansi.numeric_literals);
370 assert_eq!(sf.parameters, ansi.parameters);
371 assert_eq!(sf.session_variables, ansi.session_variables);
372 assert_eq!(sf.identifier_syntax, ansi.identifier_syntax);
373 // Snowflake diverges from ANSI on exactly the PartiQL / SUPER table-position path.
374 assert_eq!(
375 TableExpressionSyntax {
376 table_json_path: false,
377 ..sf.table_expressions
378 },
379 ansi.table_expressions,
380 );
381 assert!(sf.table_expressions.table_json_path);
382 assert_eq!(sf.operator_syntax, ansi.operator_syntax);
383 assert_eq!(sf.call_syntax, ansi.call_syntax);
384 assert_eq!(sf.predicate_syntax, ansi.predicate_syntax);
385 assert_eq!(sf.mutation_syntax, ansi.mutation_syntax);
386 assert_eq!(sf.statement_ddl_gates, ansi.statement_ddl_gates);
387 assert_eq!(
388 sf.create_table_clause_syntax,
389 ansi.create_table_clause_syntax
390 );
391 assert_eq!(sf.column_definition_syntax, ansi.column_definition_syntax);
392 assert_eq!(sf.constraint_syntax, ansi.constraint_syntax);
393 assert_eq!(sf.index_alter_syntax, ansi.index_alter_syntax);
394 assert_eq!(sf.existence_guards, ansi.existence_guards);
395 // The utility surface diverges from ANSI by the `COPY INTO` gate and stage
396 // references (`@stage` / `@~` / `@%table`).
397 assert_eq!(sf.utility_syntax, UtilitySyntax::SNOWFLAKE);
398 assert_ne!(sf.utility_syntax, ansi.utility_syntax);
399 assert!(sf.utility_syntax.copy_into);
400 assert!(sf.utility_syntax.stage_references);
401 assert_eq!(
402 UtilitySyntax {
403 copy_into: false,
404 stage_references: false,
405 ..sf.utility_syntax
406 },
407 ansi.utility_syntax,
408 );
409 assert_eq!(sf.type_name_syntax, ansi.type_name_syntax);
410 assert_eq!(sf.byte_classes, ansi.byte_classes);
411 assert_eq!(sf.binding_powers, ansi.binding_powers);
412 assert_eq!(sf.target_spelling, ansi.target_spelling);
413 }
414
415 #[test]
416 fn snowflake_enables_exactly_the_four_staged_gates() {
417 // The capstone: semi-structured access, QUALIFY, GROUP BY ALL, and the Oracle-style
418 // CONNECT BY hierarchical query clause are on, and each
419 // is off in the ANSI base it derives from — while ORDER BY ALL stays off (Snowflake
420 // ships GROUP BY ALL without it). Forcing the four back off recovers the ANSI
421 // sub-presets verbatim.
422 let ansi = FeatureSet::ANSI;
423 let sf = FeatureSet::SNOWFLAKE;
424
425 assert!(sf.expression_syntax.semi_structured_access);
426 assert!(!ansi.expression_syntax.semi_structured_access);
427 assert!(sf.select_syntax.qualify && !ansi.select_syntax.qualify);
428 // The Oracle-style hierarchical query clause; on for Snowflake, off in ANSI.
429 assert!(sf.select_syntax.connect_by_clause && !ansi.select_syntax.connect_by_clause);
430 assert!(sf.grouping_syntax.group_by_all && !ansi.grouping_syntax.group_by_all);
431 // Snowflake has no ORDER BY ALL — the flag the field doc names as the split.
432 assert!(!sf.grouping_syntax.order_by_all);
433 assert_eq!(
434 sf.grouping_syntax.order_by_all,
435 ansi.grouping_syntax.order_by_all
436 );
437
438 assert_eq!(
439 SelectSyntax {
440 qualify: false,
441 connect_by_clause: false,
442 ..sf.select_syntax
443 },
444 ansi.select_syntax,
445 );
446 assert_eq!(
447 GroupingSyntax {
448 group_by_all: false,
449 ..sf.grouping_syntax
450 },
451 ansi.grouping_syntax,
452 );
453 assert_eq!(
454 ExpressionSyntax {
455 semi_structured_access: false,
456 ..sf.expression_syntax
457 },
458 ansi.expression_syntax,
459 );
460 }
461
462 #[test]
463 fn snowflake_is_lexically_consistent_and_dependency_clean() {
464 // Both self-consistency registries must be clean: the semi-structured `:` trigger
465 // has a single claimant (colon parameters stay off), and none of the four
466 // contextual gates rides an unset base flag.
467 let sf = FeatureSet::SNOWFLAKE;
468 assert_eq!(sf.lexical_conflict(), None);
469 assert!(sf.is_lexically_consistent());
470 assert_eq!(sf.feature_dependencies(), None);
471 assert!(sf.has_satisfied_feature_dependencies());
472 assert_eq!(sf.grammar_conflict(), None);
473 assert!(sf.has_no_grammar_conflict());
474 }
475}