Skip to main content

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}