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}