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//! Five grammar gates and the `COPY INTO` utility delta are enabled as data deltas:
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//! - [`table_json_path`](TableExpressionSyntax::table_json_path) — PartiQL-style `@path`
41//!   lookups in `FROM` table-positioned JSON/VARIANT expressions.
42//! - [`copy_into`](UtilitySyntax::copy_into) and [`stage_references`](UtilitySyntax::stage_references)
43//!   for bulk load/unload statements with staged locations.
44//!
45//! Snowflake's identifier lexis needs no delta: it folds unquoted identifiers to
46//! **upper**case ([`Casing::Upper`], already ANSI's value) and quotes with the standard
47//! `"…"` ([`STANDARD_IDENTIFIER_QUOTES`], likewise ANSI). The `semi_structured_access`
48//! path rides the `:` trigger, which contends with
49//! [`ParameterSyntax::named_colon`](super::ParameterSyntax::named_colon); Snowflake speaks
50//! bind variables with `?`/`:name` *outside* SQL text and does not enable colon parameters
51//! in-grammar, so `named_colon` stays off (ANSI's value) and the `:` trigger has a single
52//! claimant — the lexical-consistency `const` assert below enforces it.
53
54use super::{
55    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
56    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
57    DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
58    IdentifierSyntax, IndexAlterSyntax, JoinSyntax, Keyword, KeywordOperators, KeywordSet,
59    MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
60    ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, RESERVED_BARE_ALIAS,
61    RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME, STANDARD_BYTE_CLASSES,
62    STANDARD_IDENTIFIER_QUOTES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
63    StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
64    TransactionSyntax, TypeNameSyntax, UtilitySyntax, ViewSequenceClauseSyntax,
65};
66use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
67
68/// `QUALIFY`, reserved by Snowflake (its documented reserved-keyword list rejects the word
69/// as any unquoted identifier). Unioned into all four per-position reject sets below,
70/// mirroring the engine-probed DuckDB profile: the bare-alias reservation is load-bearing
71/// for the grammar — it is what lets `FROM t QUALIFY …` read the clause instead of a table
72/// alias named `qualify`, and the column/function/type reservations match Snowflake's
73/// "reserved everywhere" status. `AS`-label position stays open (`SELECT 1 AS qualify`),
74/// keeping `reserved_as_label` empty like every ANSI-derived preset.
75pub const SNOWFLAKE_QUALIFY_RESERVATION: KeywordSet =
76    KeywordSet::from_keywords(&[Keyword::Qualify]);
77
78/// `PIVOT`, `UNPIVOT`, and `MATCH_RECOGNIZE` — Snowflake's FROM-clause table operators.
79/// Unlike [`SNOWFLAKE_QUALIFY_RESERVATION`], none of these is a Snowflake *reserved*
80/// keyword: all three are absent from Snowflake's reserved-keyword list and stay usable
81/// as ordinary unquoted identifiers
82/// (<https://docs.snowflake.com/en/sql-reference/reserved-keywords>). Each is instead
83/// *position-reserved* — recognized as an operator immediately after a table reference —
84/// so a bare factor must not swallow the keyword as a correlation alias:
85/// `FROM t PIVOT (…)` / `FROM t UNPIVOT (…)`
86/// (<https://docs.snowflake.com/en/sql-reference/constructs/pivot>,
87/// <https://docs.snowflake.com/en/sql-reference/constructs/unpivot>) and
88/// `FROM t MATCH_RECOGNIZE (…)`
89/// (<https://docs.snowflake.com/en/sql-reference/constructs/match_recognize>) all attach
90/// the operator directly to the `FROM` object.
91///
92/// As with BigQuery's `BIGQUERY_PIVOT_RESERVATION`, the
93/// reservation is confined to the `ColId` axis ([`SNOWFLAKE_RESERVED_COLUMN_NAME`]) — the
94/// load-bearing set for bare-alias reachability — and deliberately *not* the function,
95/// type, or projection bare-label axes, which Snowflake keeps open (`pivot(1)`,
96/// `CAST(1 AS pivot)`, `SELECT 1 pivot` still parse). This is the minimal deviation from
97/// the DuckDB `DUCKDB_PIVOT_RESERVATION` (all
98/// four positions, because DuckDB's engine genuinely reserves the words); QUALIFY, a real
99/// Snowflake reserved keyword, still rides all four via
100/// [`SNOWFLAKE_QUALIFY_RESERVATION`]. `MATCH_RECOGNIZE` rides Snowflake alone — BigQuery
101/// has no such operator. The reachability cost mirrors BigQuery's: under this preset an
102/// unquoted `pivot`/`unpivot`/`match_recognize` is not admitted as a column/table name or
103/// table alias — quote it (`"pivot"`) to use it as an identifier there.
104pub const SNOWFLAKE_TABLE_OPERATOR_RESERVATION: KeywordSet =
105    KeywordSet::from_keywords(&[Keyword::Pivot, Keyword::Unpivot, Keyword::MatchRecognize]);
106
107/// The ANSI column-name reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`] and the
108/// [`SNOWFLAKE_TABLE_OPERATOR_RESERVATION`] `ColId`-axis reservation.
109pub const SNOWFLAKE_RESERVED_COLUMN_NAME: KeywordSet = RESERVED_COLUMN_NAME
110    .union(SNOWFLAKE_QUALIFY_RESERVATION)
111    .union(SNOWFLAKE_TABLE_OPERATOR_RESERVATION);
112
113/// The ANSI function-name reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`].
114pub const SNOWFLAKE_RESERVED_FUNCTION_NAME: KeywordSet =
115    RESERVED_FUNCTION_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION);
116
117/// The ANSI type-name reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`].
118pub const SNOWFLAKE_RESERVED_TYPE_NAME: KeywordSet =
119    RESERVED_TYPE_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION);
120
121/// The ANSI bare-alias reject set plus [`SNOWFLAKE_QUALIFY_RESERVATION`].
122pub const SNOWFLAKE_RESERVED_BARE_ALIAS: KeywordSet =
123    RESERVED_BARE_ALIAS.union(SNOWFLAKE_QUALIFY_RESERVATION);
124
125impl SelectSyntax {
126    /// Snowflake SELECT surface: the ANSI baseline plus the documented Snowflake
127    /// clauses — the `QUALIFY <predicate>` post-window filter, the `GROUP BY ALL` clause
128    /// mode, and the Oracle-style `START WITH`/`CONNECT BY` hierarchical query clause.
129    /// `ORDER BY ALL` is deliberately *not* enabled: Snowflake ships `GROUP BY ALL`
130    /// without `ORDER BY ALL` (see [`order_by_all`](GroupingSyntax::order_by_all)).
131    /// Every other SELECT knob is conservatively ANSI.
132    pub const SNOWFLAKE: Self = Self {
133        qualify: true,
134        // Snowflake's `START WITH … CONNECT BY [PRIOR] col = [PRIOR] col` hierarchical
135        // query clause (Snowflake `CONNECT BY` reference — the citable public grammar,
136        // there being no Oracle preset). Modelled as the Oracle superset (either clause
137        // order, the after-`WHERE` position, `NOCYCLE`); see
138        // [`connect_by_clause`](SelectSyntax::connect_by_clause) for the acceptance bound.
139        connect_by_clause: true,
140        distinct_on: false,
141        select_into: false,
142        empty_target_list: false,
143        alias_string_literals: false,
144        bare_alias_string_literals: false,
145        union_by_name: false,
146        wildcard_modifiers: false,
147        wildcard_replace: false,
148        intersect_all: true,
149        except_all: true,
150        qualified_wildcard_alias: false,
151        from_first: false,
152        explicit_table: true,
153        parenthesized_query_operands: true,
154        values_rows_require_equal_arity: false,
155        values_row_constructor: true,
156        as_alias_rejects_reserved: false,
157        trailing_comma: false,
158        prefix_colon_alias: false,
159        lateral_view_clause: false,
160    };
161}
162
163impl QueryTailSyntax {
164    /// The `SNOWFLAKE` preset for query tail syntax.
165    pub const SNOWFLAKE: Self = Self {
166        fetch_first: true,
167        limit_offset_comma: false,
168        locking_clauses: false,
169        key_lock_strengths: false,
170        stacked_locking_clauses: false,
171        using_sample: false,
172        leading_offset: true,
173        limit_expressions: true,
174        limit_percent: false,
175        with_ties_requires_order_by: false,
176        pipe_syntax: false,
177        limit_by_clause: false,
178        settings_clause: false,
179        format_clause: false,
180        for_xml_json_clause: false,
181    };
182}
183
184impl TableFactorSyntax {
185    /// Snowflake table-factor surface: the ANSI baseline plus the standard PIVOT table
186    /// factor (`FROM t PIVOT(<agg> FOR <col> IN (<vals> | ANY [ORDER BY …] | <subquery>)
187    /// [DEFAULT ON NULL (<expr>)]))`). Snowflake has no differential oracle here, so the
188    /// gate follows the documented grammar on this conservative preset (the `qualify`
189    /// precedent). The DuckDB [`pivot`](TableFactorSyntax::pivot) flag stays off — Snowflake has no
190    /// leading-keyword `PIVOT` statement, `IN <enum>`, or multi-`FOR`-column form.
191    pub const SNOWFLAKE: Self = Self {
192        pivot_value_sources: true,
193        // The SQL:2016 MATCH_RECOGNIZE row-pattern table factor (documented; no
194        // differential oracle here, so the gate follows the grammar on this
195        // conservative preset — the `qualify`/`pivot_value_sources` precedent).
196        match_recognize: true,
197        lateral: false,
198        table_functions: false,
199        rows_from: false,
200        unnest: false,
201        unnest_with_offset: false,
202        table_function_ordinality: false,
203        special_function_table_source: true,
204        pivot: false,
205        unpivot: false,
206        show_ref: false,
207        from_values: false,
208        json_table: false,
209        xml_table: false,
210        table_expr_factor: false,
211        open_json: false,
212    };
213}
214
215impl GroupingSyntax {
216    /// The `SNOWFLAKE` preset for grouping syntax.
217    pub const SNOWFLAKE: Self = Self {
218        group_by_all: true,
219        grouping_sets: true,
220        with_rollup: false,
221        order_by_using: false,
222        group_by_set_quantifier: false,
223        order_by_all: false,
224    };
225}
226
227impl UtilitySyntax {
228    /// Snowflake utility surface: the ANSI baseline plus the `COPY INTO` bulk
229    /// load/unload statement. Every other utility knob is conservatively ANSI — the
230    /// PostgreSQL/DuckDB `COPY`, `COMMENT ON`, the SQLite/MySQL statements, and the
231    /// prepared-statement lifecycle all stay off.
232    pub const SNOWFLAKE: Self = Self {
233        copy_into: true,
234        stage_references: true,
235        copy: false,
236        comment_on: false,
237        comment_if_exists: false,
238        pragma: false,
239        attach: false,
240        kill: false,
241        handler_statements: false,
242        plugin_component_statements: false,
243        shutdown: false,
244        restart: false,
245        clone: false,
246        import_table: false,
247        help_statement: false,
248        binlog: false,
249        key_cache_statements: false,
250        use_statement: false,
251        use_qualified_name: false,
252        use_string_literal_name: false,
253        prepared_statements: false,
254        prepare_typed_parameters: false,
255        prepared_statements_from: false,
256        call: false,
257        call_bare_name: false,
258        load_extension: false,
259        load_bare_name: false,
260        load_data: false,
261        reset_scope: false,
262        detach_if_exists: false,
263        do_statement: false,
264        do_expression_list: false,
265        lock_tables: false,
266        lock_instance: false,
267        rename_statement: false,
268        signal_diagnostics: false,
269        export_import_database: false,
270        update_extensions: false,
271        flush: false,
272        purge_binary_logs: false,
273        replication_statements: false,
274    };
275}
276impl TransactionSyntax {
277    /// Transaction-control surface for the `SNOWFLAKE` preset (split from UtilitySyntax).
278    pub const SNOWFLAKE: Self = Self {
279        start_transaction: true,
280        start_transaction_block_optional: false,
281        transaction_work_keyword: true,
282        begin_transaction_keyword: true,
283        commit_transaction_keyword: true,
284        rollback_transaction_keyword: true,
285        transaction_name: false,
286        begin_transaction_modes: true,
287        transaction_savepoints: true,
288        set_transaction: true,
289        transaction_isolation_mode: true,
290        transaction_access_mode: true,
291        transaction_deferrable_mode: true,
292        start_transaction_isolation_mode: true,
293        start_transaction_deferrable_mode: true,
294        start_transaction_consistent_snapshot: false,
295        transaction_multiple_modes: true,
296        transaction_modes_require_commas: false,
297        transaction_modes_reject_duplicates: false,
298        abort_transaction_alias: false,
299        end_transaction_alias: false,
300        transaction_release: false,
301        transaction_chain: true,
302        release_savepoint_keyword_optional: true,
303        begin_transaction_mode: false,
304        xa_transactions: false,
305    };
306}
307
308impl ExpressionSyntax {
309    /// Snowflake expression surface: the ANSI baseline plus semi-structured path access
310    /// (`base:key[0].field`), Snowflake's `VARIANT`/`OBJECT`/`ARRAY` accessor. Every other
311    /// expression knob is conservatively ANSI.
312    pub const SNOWFLAKE: Self = Self {
313        semi_structured_access: true,
314        typecast_operator: false,
315        subscript: false,
316        slice_step: false,
317        collate: false,
318        at_time_zone: false,
319        array_constructor: false,
320        multidim_array_literals: false,
321        collection_literals: false,
322        row_constructor: false,
323        struct_constructor: false,
324        field_selection: false,
325        field_wildcard: false,
326        typed_string_literals: true,
327        typed_interval_literal: true,
328        relaxed_interval_syntax: false,
329        mysql_interval_operator: false,
330        positional_column: false,
331        lambda_keyword: false,
332    };
333}
334
335impl FeatureSet {
336    /// Snowflake as ANSI-derived dialect data (see the module docs for the full derivation
337    /// rationale and the conservatism bar).
338    pub const SNOWFLAKE: Self = Self {
339        // Snowflake folds unquoted identifiers to uppercase — the classic upper-folding
340        // dialect, which is already ANSI's value (kept explicit as the documented fact).
341        identifier_casing: Casing::Upper,
342        // Standard `"…"` identifier quoting; Snowflake adds no second delimiter.
343        identifier_quotes: STANDARD_IDENTIFIER_QUOTES,
344        default_null_ordering: NullOrdering::NullsLast,
345        // The reserved-set delta over ANSI: `QUALIFY` is reserved in every identifier
346        // position (Snowflake's documented reserved-keyword status), which the `QUALIFY`
347        // clause gate depends on to disambiguate `FROM t QUALIFY …`; and the FROM-clause
348        // table operators `PIVOT`/`UNPIVOT`/`MATCH_RECOGNIZE` are position-reserved on the
349        // `ColId` axis only (see [`SNOWFLAKE_TABLE_OPERATOR_RESERVATION`]) so a bare
350        // `FROM t PIVOT (…)` / `FROM t MATCH_RECOGNIZE (…)` reaches the operator instead of
351        // aliasing the source. The operators are not Snowflake reserved keywords, so — unlike
352        // QUALIFY — they stay out of the function/type/bare-label reject sets below.
353        reserved_column_name: SNOWFLAKE_RESERVED_COLUMN_NAME,
354        reserved_function_name: SNOWFLAKE_RESERVED_FUNCTION_NAME,
355        reserved_type_name: SNOWFLAKE_RESERVED_TYPE_NAME,
356        reserved_bare_alias: SNOWFLAKE_RESERVED_BARE_ALIAS,
357        reserved_as_label: KeywordSet::EMPTY,
358        catalog_qualified_names: true,
359        byte_classes: STANDARD_BYTE_CLASSES,
360        binding_powers: STANDARD_BINDING_POWERS,
361        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
362        // Conservative ANSI string/number/parameter surface: Snowflake's own forms
363        // (`$$…$$` dollar-quoting, backslash escapes, `{name:Type}`-free bind syntax) have
364        // no modelled gate here and are deferred rather than guessed at without an oracle.
365        // Crucially `named_colon` stays off so the `:` trigger belongs solely to
366        // `semi_structured_access` (the lexical-consistency assert below enforces it).
367        string_literals: StringLiteralSyntax::ANSI,
368        numeric_literals: NumericLiteralSyntax::ANSI,
369        parameters: ParameterSyntax::ANSI,
370        session_variables: SessionVariableSyntax::ANSI,
371        identifier_syntax: IdentifierSyntax::ANSI,
372        // ANSI table-expression surface plus the PartiQL / SUPER table-position JSON path
373        // (`FROM src[0].a`), sqlparser-rs's `supports_partiql`.
374        table_expressions: TableExpressionSyntax {
375            table_json_path: true,
376            only: false,
377            table_sample: false,
378            parenthesized_joins: true,
379            table_alias_column_lists: true,
380            join_using_alias: false,
381            index_hints: false,
382            table_hints: false,
383            partition_selection: false,
384            base_table_alias_column_lists: true,
385            string_literal_aliases: false,
386            aliased_parenthesized_join: true,
387            bare_table_alias_is_bare_label: false,
388            table_version: false,
389            indexed_by: false,
390            prefix_colon_alias: false,
391        },
392        join_syntax: JoinSyntax::ANSI,
393        table_factor_syntax: TableFactorSyntax::SNOWFLAKE,
394        // The semi-structured path accessor — the capstone this preset exposes.
395        expression_syntax: ExpressionSyntax::SNOWFLAKE,
396        operator_syntax: OperatorSyntax::ANSI,
397        call_syntax: CallSyntax::ANSI,
398        string_func_forms: StringFuncForms::ANSI,
399        aggregate_call_syntax: AggregateCallSyntax::ANSI,
400        predicate_syntax: PredicateSyntax::ANSI,
401        pipe_operator: PipeOperator::StringConcat,
402        double_ampersand: DoubleAmpersand::Unsupported,
403        keyword_operators: KeywordOperators::Unsupported,
404        caret_operator: CaretOperator::Unsupported,
405        hash_bitwise_xor: false,
406        comment_syntax: CommentSyntax::ANSI,
407        mutation_syntax: MutationSyntax::ANSI,
408        statement_ddl_gates: StatementDdlGates::ANSI,
409        view_sequence_clause_syntax: ViewSequenceClauseSyntax::ANSI,
410        create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
411        column_definition_syntax: ColumnDefinitionSyntax::ANSI,
412        constraint_syntax: ConstraintSyntax::ANSI,
413        index_alter_syntax: IndexAlterSyntax::ANSI,
414        existence_guards: ExistenceGuards::ANSI,
415        // The `QUALIFY` and `GROUP BY ALL` clauses.
416        select_syntax: SelectSyntax::SNOWFLAKE,
417        query_tail_syntax: QueryTailSyntax::SNOWFLAKE,
418        grouping_syntax: GroupingSyntax::SNOWFLAKE,
419        // The `COPY INTO` bulk load/unload statement.
420        utility_syntax: UtilitySyntax::SNOWFLAKE,
421        transaction_syntax: TransactionSyntax::SNOWFLAKE,
422        show_syntax: ShowSyntax::ANSI,
423        maintenance_syntax: MaintenanceSyntax::ANSI,
424        access_control_syntax: AccessControlSyntax::ANSI,
425        type_name_syntax: TypeNameSyntax::ANSI,
426        // No Snowflake-specific Tier-1 output spelling yet; render the portable ANSI
427        // canonical type names (a `TargetSpelling::Snowflake` is render work a later
428        // ticket owns).
429        target_spelling: TargetSpelling::Ansi,
430    };
431}
432
433/// Prefer [`FeatureSet::SNOWFLAKE`] for struct update.
434pub const SNOWFLAKE: FeatureSet = FeatureSet::SNOWFLAKE;
435
436// Compile-time proof the Snowflake preset claims no shared tokenizer trigger twice. Its
437// one contended trigger is `:` — claimed by `semi_structured_access` here and by
438// `named_colon` when on — and the preset keeps `named_colon` off (ANSI's value), so `:`
439// has a single claimant. Every other delta is a contextual grammar gate or a keyword
440// reservation with no tokenizer trigger. Kept as a ratchet so a future Snowflake delta
441// that *does* add a contending trigger (e.g. enabling colon parameters) fails the build
442// here.
443const _: () = assert!(FeatureSet::SNOWFLAKE.is_lexically_consistent());
444// The two sibling self-consistency registries are ratcheted the same way, so the
445// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
446// flag rides an unset base, and no two features contend for one parser-position head.
447const _: () = assert!(FeatureSet::SNOWFLAKE.has_satisfied_feature_dependencies());
448const _: () = assert!(FeatureSet::SNOWFLAKE.has_no_grammar_conflict());
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn snowflake_is_ansi_plus_the_five_gates_and_the_qualify_reservation() {
456        // The preset is ANSI with a documented, closed set of divergent axes: five grammar
457        // gates (including `table_json_path`), the `COPY INTO` utility gate, the `QUALIFY`
458        // reservation, and the `PIVOT`/`UNPIVOT`/`MATCH_RECOGNIZE` table-operator `ColId`
459        // reservation. Asserting the whole rest equals ANSI keeps the "ANSI-derived, every
460        // delta documented" claim honest against a future stray edit. Bind to locals so the
461        // const reads are not flagged by clippy's `assertions_on_constants`.
462        let ansi = FeatureSet::ANSI;
463        let sf = FeatureSet::SNOWFLAKE;
464
465        // The four divergent sub-presets.
466        assert_eq!(sf.select_syntax, SelectSyntax::SNOWFLAKE);
467        assert_ne!(sf.select_syntax, ansi.select_syntax);
468        assert_eq!(sf.expression_syntax, ExpressionSyntax::SNOWFLAKE);
469        assert_ne!(sf.expression_syntax, ansi.expression_syntax);
470
471        // The reserved-set delta: QUALIFY in every identifier position (a real Snowflake
472        // reserved keyword) plus the `PIVOT`/`UNPIVOT`/`MATCH_RECOGNIZE` table operators on
473        // the `ColId` axis only (position-reserved, not reserved keywords).
474        assert_eq!(sf.reserved_column_name, SNOWFLAKE_RESERVED_COLUMN_NAME);
475        assert_ne!(sf.reserved_column_name, ansi.reserved_column_name);
476        assert_eq!(sf.reserved_function_name, SNOWFLAKE_RESERVED_FUNCTION_NAME);
477        assert_ne!(sf.reserved_function_name, ansi.reserved_function_name);
478        assert_eq!(sf.reserved_type_name, SNOWFLAKE_RESERVED_TYPE_NAME);
479        assert_ne!(sf.reserved_type_name, ansi.reserved_type_name);
480        assert_eq!(sf.reserved_bare_alias, SNOWFLAKE_RESERVED_BARE_ALIAS);
481        assert_ne!(sf.reserved_bare_alias, ansi.reserved_bare_alias);
482        // Dropping QUALIFY *and* the table operators recovers the ANSI column set verbatim.
483        assert_eq!(
484            sf.reserved_column_name
485                .difference(SNOWFLAKE_QUALIFY_RESERVATION)
486                .difference(SNOWFLAKE_TABLE_OPERATOR_RESERVATION),
487            ansi.reserved_column_name,
488        );
489        assert!(sf.reserved_column_name.contains(Keyword::Qualify));
490        assert!(sf.reserved_bare_alias.contains(Keyword::Qualify));
491        // The table operators are `ColId`-only: reserved as a column/table name and table
492        // alias (bare-alias reachability) but not as a function name, type name, or projection
493        // bare label (Snowflake's non-reserved status — `pivot(1)`, `SELECT 1 pivot` parse).
494        for kw in [Keyword::Pivot, Keyword::Unpivot, Keyword::MatchRecognize] {
495            assert!(sf.reserved_column_name.contains(kw));
496            assert!(!sf.reserved_function_name.contains(kw));
497            assert!(!sf.reserved_type_name.contains(kw));
498            assert!(!sf.reserved_bare_alias.contains(kw));
499        }
500        // The function/type/bare-label sets carry exactly the QUALIFY delta — no table
501        // operator leaks in.
502        assert_eq!(
503            sf.reserved_function_name,
504            RESERVED_FUNCTION_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION),
505        );
506        assert_eq!(
507            sf.reserved_type_name,
508            RESERVED_TYPE_NAME.union(SNOWFLAKE_QUALIFY_RESERVATION),
509        );
510        assert_eq!(
511            sf.reserved_bare_alias,
512            RESERVED_BARE_ALIAS.union(SNOWFLAKE_QUALIFY_RESERVATION),
513        );
514        // `AS`-label position stays open (`SELECT 1 AS qualify`).
515        assert_eq!(sf.reserved_as_label, KeywordSet::EMPTY);
516
517        // Snowflake's identifier lexis needs no delta — both facts equal ANSI's.
518        assert_eq!(sf.identifier_casing, Casing::Upper);
519        assert_eq!(sf.identifier_casing, ansi.identifier_casing);
520        assert_eq!(sf.identifier_quotes, ansi.identifier_quotes);
521        // `named_colon` off is the interplay the semi-structured accessor depends on.
522        assert!(!sf.parameters.named_colon);
523
524        // Everything else is inherited verbatim from ANSI.
525        assert_eq!(sf.string_literals, ansi.string_literals);
526        assert_eq!(sf.numeric_literals, ansi.numeric_literals);
527        assert_eq!(sf.parameters, ansi.parameters);
528        assert_eq!(sf.session_variables, ansi.session_variables);
529        assert_eq!(sf.identifier_syntax, ansi.identifier_syntax);
530        // Snowflake diverges from ANSI on exactly the PartiQL / SUPER table-position path.
531        assert_eq!(
532            TableExpressionSyntax {
533                table_json_path: false,
534                ..sf.table_expressions
535            },
536            ansi.table_expressions,
537        );
538        assert!(sf.table_expressions.table_json_path);
539        // Table-factor surface: pivot_value_sources + match_recognize (not ANSI).
540        assert_eq!(sf.table_factor_syntax, TableFactorSyntax::SNOWFLAKE);
541        assert_ne!(sf.table_factor_syntax, ansi.table_factor_syntax);
542        assert_eq!(sf.operator_syntax, ansi.operator_syntax);
543        assert_eq!(
544            sf.view_sequence_clause_syntax,
545            ansi.view_sequence_clause_syntax
546        );
547        assert_eq!(sf.transaction_syntax, ansi.transaction_syntax);
548        assert_eq!(sf.call_syntax, ansi.call_syntax);
549        assert_eq!(sf.predicate_syntax, ansi.predicate_syntax);
550        assert_eq!(sf.mutation_syntax, ansi.mutation_syntax);
551        assert_eq!(sf.statement_ddl_gates, ansi.statement_ddl_gates);
552        assert_eq!(
553            sf.create_table_clause_syntax,
554            ansi.create_table_clause_syntax
555        );
556        assert_eq!(sf.column_definition_syntax, ansi.column_definition_syntax);
557        assert_eq!(sf.constraint_syntax, ansi.constraint_syntax);
558        assert_eq!(sf.index_alter_syntax, ansi.index_alter_syntax);
559        assert_eq!(sf.existence_guards, ansi.existence_guards);
560        // The utility surface diverges from ANSI by the `COPY INTO` gate and stage
561        // references (`@stage` / `@~` / `@%table`).
562        assert_eq!(sf.utility_syntax, UtilitySyntax::SNOWFLAKE);
563        assert_ne!(sf.utility_syntax, ansi.utility_syntax);
564        assert!(sf.utility_syntax.copy_into);
565        assert!(sf.utility_syntax.stage_references);
566        assert_eq!(
567            UtilitySyntax {
568                copy_into: false,
569                stage_references: false,
570                ..sf.utility_syntax
571            },
572            ansi.utility_syntax,
573        );
574        assert_eq!(sf.type_name_syntax, ansi.type_name_syntax);
575        assert_eq!(sf.byte_classes, ansi.byte_classes);
576        assert_eq!(sf.binding_powers, ansi.binding_powers);
577        assert_eq!(sf.target_spelling, ansi.target_spelling);
578    }
579
580    #[test]
581    fn snowflake_enables_exactly_the_five_staged_gates() {
582        // The capstone: semi-structured access, QUALIFY, GROUP BY ALL, table-JSON path,
583        // and the Oracle-style CONNECT BY hierarchical query clause are on, and each
584        // is off in the ANSI base it derives from — while ORDER BY ALL stays off (Snowflake
585        // ships GROUP BY ALL without it). Forcing the five back off recovers the ANSI
586        // sub-presets verbatim.
587        let ansi = FeatureSet::ANSI;
588        let sf = FeatureSet::SNOWFLAKE;
589
590        assert!(sf.expression_syntax.semi_structured_access);
591        assert!(!ansi.expression_syntax.semi_structured_access);
592        assert!(sf.select_syntax.qualify && !ansi.select_syntax.qualify);
593        // The Oracle-style hierarchical query clause; on for Snowflake, off in ANSI.
594        assert!(sf.select_syntax.connect_by_clause && !ansi.select_syntax.connect_by_clause);
595        assert!(sf.grouping_syntax.group_by_all && !ansi.grouping_syntax.group_by_all);
596        assert!(sf.table_expressions.table_json_path);
597        assert!(!ansi.table_expressions.table_json_path);
598        // Snowflake has no ORDER BY ALL — the flag the field doc names as the split.
599        assert!(!sf.grouping_syntax.order_by_all);
600        assert_eq!(
601            sf.grouping_syntax.order_by_all,
602            ansi.grouping_syntax.order_by_all
603        );
604
605        assert_eq!(
606            SelectSyntax {
607                qualify: false,
608                connect_by_clause: false,
609                ..sf.select_syntax
610            },
611            ansi.select_syntax,
612        );
613        assert_eq!(
614            GroupingSyntax {
615                group_by_all: false,
616                ..sf.grouping_syntax
617            },
618            ansi.grouping_syntax,
619        );
620        assert_eq!(
621            ExpressionSyntax {
622                semi_structured_access: false,
623                ..sf.expression_syntax
624            },
625            ansi.expression_syntax,
626        );
627        assert_eq!(
628            TableExpressionSyntax {
629                table_json_path: false,
630                ..sf.table_expressions
631            },
632            ansi.table_expressions,
633        );
634    }
635
636    #[test]
637    fn snowflake_is_lexically_consistent_and_dependency_clean() {
638        // Both self-consistency registries must be clean: the semi-structured `:` trigger
639        // has a single claimant (colon parameters stay off), and none of the five contextual
640        // gates rides an unset base flag.
641        let sf = FeatureSet::SNOWFLAKE;
642        assert_eq!(sf.lexical_conflict(), None);
643        assert!(sf.is_lexically_consistent());
644        assert_eq!(sf.feature_dependencies(), None);
645        assert!(sf.has_satisfied_feature_dependencies());
646        assert_eq!(sf.grammar_conflict(), None);
647        assert!(sf.has_no_grammar_conflict());
648    }
649    #[test]
650    fn snowflake_closed_delta_axes_match_documented_set() {
651        crate::dialect::closed_delta::assert_closed_delta(
652            &FeatureSet::ANSI,
653            &FeatureSet::SNOWFLAKE,
654            &[
655                "reserved_column_name",
656                "reserved_function_name",
657                "reserved_type_name",
658                "reserved_bare_alias",
659                "table_expressions",
660                "table_factor_syntax",
661                "expression_syntax",
662                "select_syntax",
663                "grouping_syntax",
664                "utility_syntax",
665            ],
666        );
667    }
668}