squonk_ast/dialect/bigquery.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The BigQuery / ZetaSQL dialect preset (ANSI-derived, deliberately conservative).
5//!
6//! Google BigQuery's GoogleSQL (ZetaSQL) diverges widely across its type, function, and
7//! statement surface, and — unlike the five shipped oracle-compared presets — this workspace
8//! has **no BigQuery oracle**, so over-acceptance cannot be measured. Conservatism is
9//! therefore the honesty bar: this preset derives from [`FeatureSet::ANSI`], the strict
10//! standard baseline, and enables only the BigQuery surface that already has a modelled,
11//! tested parser gate and clear documentary evidence — in the headline case the flag's *own*
12//! doc names BigQuery as the motivating dialect. Every other axis keeps its ANSI value; a
13//! reader can predict from this module exactly what BigQuery accepts beyond the standard, and
14//! unsupported BigQuery syntax is a clean reject routed to a focused follow-up ticket, never a
15//! silent over-accept.
16//!
17//! # What this preset adds over ANSI
18//!
19//! Three headline gates carry the BigQuery surface:
20//!
21//! - [`unnest`](TableFactorSyntax::unnest) — the first-class `UNNEST(<expr>)` table
22//! factor (`FROM UNNEST(…)`), BigQuery's array-to-relation expansion. It is not
23//! BigQuery-exclusive (PostgreSQL/DuckDB/Lenient enable it too), but it is genuine BigQuery
24//! `FROM` surface *and* the base the next gate rides.
25//! - [`unnest_with_offset`](TableFactorSyntax::unnest_with_offset) — the
26//! `WITH OFFSET [AS <alias>]` tail on an `UNNEST` factor, a 0-based ordinal column and the
27//! BigQuery counterpart of PostgreSQL's `WITH ORDINALITY`. Its own flag doc records this
28//! surface as on for the BigQuery preset alone — this preset is that home, the first to
29//! enable the flag positively. The gate rides `unnest`
30//! ([`FeatureDependencyViolation::UnnestWithOffsetWithoutUnnest`](super::FeatureDependencyViolation)):
31//! `unnest` is therefore on above, and this is the first shipped preset to exercise that
32//! dependency in the satisfied direction. PostgreSQL and DuckDB both parse-*reject*
33//! `WITH OFFSET` (engine-probed), so the tail is a clean cross-preset reject.
34//!
35//! - [`angle_bracket_types`](TypeNameSyntax::angle_bracket_types) — type-position support
36//! for BigQuery-style `ARRAY<...>` / `STRUCT<...>` and array-of-struct declarations.
37//!
38//! One expression gate:
39//!
40//! - [`struct_constructor`](ExpressionSyntax::struct_constructor) — the `STRUCT(...)`
41//! value constructor (`STRUCT(1, 2)`, `STRUCT(x AS a)`, `STRUCT<a INT64>(1)`), the
42//! documented GoogleSQL tuple builder. Its own flag doc names BigQuery as the motivating
43//! dialect; the `(`/`<` lookahead keeps a bare `struct` an ordinary name, so the gate is
44//! additive over ANSI.
45//!
46//! # The two lexical facts over ANSI
47//!
48//! - **Backtick identifier quoting.** BigQuery quotes identifiers with the backtick
49//! `` `name` `` alone — its `"…"` and `'…'` are *both* string literals. So
50//! [`BIGQUERY_IDENTIFIER_QUOTES`] lists only the backtick (unlike the SQLite/Databricks/MSSQL
51//! bracket-or-double-quote sets, and unlike ANSI's `"…"`), and
52//! [`double_quoted_strings`](StringLiteralSyntax::double_quoted_strings) is correspondingly
53//! **on** so `"x"` lexes as a string, never an identifier — exactly the MySQL default
54//! (`ANSI_QUOTES` off) lexis. The two facts are coupled: `"` has a single claimant (the
55//! string scanner) precisely because it is absent from the quote set, the
56//! [`DoubleQuoteStringVersusIdentifier`](super::LexicalConflict) hazard the `const` assert
57//! below rules out. The backtick likewise has a single claimant — no enabled expression
58//! grammar lexes a backtick.
59//! - **Case folding.** BigQuery column and alias references resolve case-insensitively (table
60//! and dataset names are case-sensitive), so [`identifier_casing`](FeatureSet::identifier_casing)
61//! is [`Casing::Lower`] — the closest single fit per the [`Casing`] doc's
62//! *known-limitation* paragraph, which names exactly this "case-insensitive column beside a
63//! case-sensitive table" shape (shared with MySQL/T-SQL) as one no single fold can express;
64//! `Casing::Lower` is the value it prescribes. The interned text still renders exactly as
65//! written; the fold is identity-only and never affects acceptance. (The
66//! per-identifier-kind table-vs-column sensitivity split is that documented `Casing`
67//! limitation and a deliberate future extension, not modelled here.)
68//!
69//! # Deliberately deferred (conservative reject)
70//!
71//! [`pipe_syntax`](QueryTailSyntax::pipe_syntax) — BigQuery/ZetaSQL query pipe syntax
72//! (`FROM t |> WHERE x |> SELECT a`) — stays **off**, and this is a considered judgment, not an
73//! oversight. Its own flag doc names the BigQuery preset as the eventual home, but reads
74//! the honesty bar the other way for now: the framework ships only the reference `|> WHERE`
75//! operator, so enabling the gate today would accept `|> WHERE` while rejecting every other
76//! pipe operator — a fragment a reader of this module could not predict, and with no BigQuery
77//! oracle the boundary cannot be measured either. The flag doc's own argument (that even the
78//! permissive `LENIENT` must wait until the `planner-parity-pipe-*` tickets make the
79//! pipe-operator surface coherent) cuts identically for BigQuery. Leaving it off with that
80//! reasoning cited is the correct call; flipping it on merely because the doc names BigQuery
81//! would ship an incoherent half-surface. The flip is deferred to the pipe-surface tickets.
82//!
83//! Everything else BigQuery (the `STRUCT<…>`/`ARRAY<…>` *type-position* surface —
84//! `CAST(x AS STRUCT<…>)` and column types, distinct from the value constructor above —
85//! `SELECT AS STRUCT/VALUE`,
86//! `EXCEPT`/`REPLACE` in `SELECT *`, `QUALIFY`, table-name backtick paths with dots,
87//! parameterised `@name` / `?` binds, the `SAFE.` function prefix, …) has no modelled gate and
88//! is a clean reject routed to follow-up tickets, never a silent over-accept.
89
90use super::{
91 AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
92 ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
93 DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
94 IdentifierQuote, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, Keyword, KeywordOperators,
95 KeywordSet, MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax,
96 OperatorSyntax, ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax,
97 RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME,
98 STANDARD_BYTE_CLASSES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
99 StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
100 TransactionSyntax, TypeNameSyntax, UtilitySyntax, ViewSequenceClauseSyntax,
101};
102use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
103
104/// BigQuery identifier quoting: the backtick `` `…` `` alone. BigQuery spells a quoted
105/// identifier `` `a` ``; its `"a"` and `'a'` are *both* string constants, so `"` is
106/// deliberately absent here (and [`StringLiteralSyntax::BIGQUERY`] turns
107/// `double_quoted_strings` on so `"` unambiguously lexes a string) — the same backtick-only
108/// lexis MySQL uses under its default `ANSI_QUOTES`-off mode.
109pub const BIGQUERY_IDENTIFIER_QUOTES: &[IdentifierQuote] = &[IdentifierQuote::Symmetric('`')];
110
111/// `PIVOT` and `UNPIVOT`, GoogleSQL's row/column rotation operators. Neither is a
112/// BigQuery *reserved* keyword — both are absent from the GoogleSQL reserved-keyword
113/// list and stay usable as ordinary unquoted identifiers (GoogleSQL lexical-structure
114/// reference, "Reserved keywords":
115/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#reserved_keywords>).
116/// They are instead *position-reserved*: the `pivot_operator` / `unpivot_operator`
117/// grammar attaches directly to a `from_item`, so `FROM t PIVOT (…)` must read the
118/// operator, not a correlation alias named `pivot` (GoogleSQL query-syntax reference,
119/// "Pivot operator" / "Unpivot operator":
120/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#pivot_operator>).
121///
122/// Modelling that reachability in this parser's shared-`ColId` alias grammar means
123/// reserving both on the `ColId` axis only — [`RESERVED_COLUMN_NAME`], the set a bare
124/// *and* `AS`-introduced table alias, a column name, and a table name all draw from —
125/// and *not* the function-name, type-name, or projection bare-label axes, which stay
126/// open (`pivot(1)`, `CAST(1 AS pivot)`, `SELECT 1 pivot` still parse), matching
127/// BigQuery's non-reserved status. This is the deliberate minimal deviation from the
128/// DuckDB `DUCKDB_PIVOT_RESERVATION`, which unions into all four positions because
129/// DuckDB's engine genuinely classes the words `reserved`. The unavoidable reachability
130/// cost: under this preset an unquoted `pivot`/`unpivot` is not admitted as a column/table
131/// name or a table alias — quote it (`` `pivot` ``) to use it as an identifier there.
132pub const BIGQUERY_PIVOT_RESERVATION: KeywordSet =
133 KeywordSet::from_keywords(&[Keyword::Pivot, Keyword::Unpivot]);
134
135/// The ANSI `ColId` reject set plus [`BIGQUERY_PIVOT_RESERVATION`]; see that const for
136/// why the reservation is confined to this one axis. The function-name, type-name, and
137/// bare-label reject sets take no delta over ANSI, so BigQuery keeps the shared consts
138/// for those three positions.
139pub const BIGQUERY_RESERVED_COLUMN_NAME: KeywordSet =
140 RESERVED_COLUMN_NAME.union(BIGQUERY_PIVOT_RESERVATION);
141
142impl StringLiteralSyntax {
143 /// BigQuery string surface: the ANSI baseline plus `"…"` double-quoted string constants
144 /// (BigQuery quotes strings with both `'…'` and `"…"`, reserving the backtick for
145 /// identifiers). Enabling this is what lets [`BIGQUERY_IDENTIFIER_QUOTES`] drop `"` from the
146 /// quote set without stranding the byte. Every other string knob is conservatively ANSI —
147 /// BigQuery's backslash escape sequences have no BigQuery-citing flag doc or oracle here and
148 /// are deferred rather than guessed at (`backslash_escapes` stays off).
149 pub const BIGQUERY: Self = Self {
150 double_quoted_strings: true,
151 escape_strings: false,
152 dollar_quoted_strings: false,
153 national_strings: false,
154 backslash_escapes: false,
155 unicode_strings: false,
156 bit_string_literals: false,
157 blob_literals: false,
158 charset_introducers: false,
159 same_line_adjacent_concat: false,
160 };
161}
162
163impl TableExpressionSyntax {
164 /// BigQuery table-expression surface: the ANSI baseline plus the `FOR SYSTEM_TIME AS OF`
165 /// time-travel modifier. The first-class `UNNEST(…)` factor and its `WITH OFFSET` tail
166 /// ride [`TableFactorSyntax`]; every other table knob is conservatively ANSI.
167 pub const BIGQUERY: Self = Self {
168 // `FROM t FOR SYSTEM_TIME AS OF <ts>` — BigQuery's sole time-travel spelling.
169 table_version: true,
170 only: false,
171 table_sample: false,
172 parenthesized_joins: true,
173 table_alias_column_lists: true,
174 join_using_alias: false,
175 index_hints: false,
176 table_hints: false,
177 partition_selection: false,
178 base_table_alias_column_lists: true,
179 string_literal_aliases: false,
180 aliased_parenthesized_join: true,
181 bare_table_alias_is_bare_label: false,
182 table_json_path: false,
183 indexed_by: false,
184 prefix_colon_alias: false,
185 };
186}
187
188impl JoinSyntax {
189 /// The `BIGQUERY` preset for join syntax.
190 pub const BIGQUERY: Self = Self {
191 stacked_join_qualifiers: true,
192 full_outer_join: true,
193 natural_cross_join: false,
194 straight_join: false,
195 asof_join: false,
196 positional_join: false,
197 semi_anti_join: false,
198 sided_semi_anti_join: false,
199 apply_join: false,
200 recursive_search_cycle: false,
201 recursive_union_rejects_order_limit: false,
202 recursive_using_key: false,
203 };
204}
205
206impl ExpressionSyntax {
207 /// BigQuery expression surface: the ANSI baseline plus the `STRUCT(...)` value
208 /// constructor (`STRUCT(1, 2)`, `STRUCT(x AS a)`, `STRUCT<a INT64>(1)`), a documented
209 /// GoogleSQL form with no differential oracle here. Every other expression knob stays
210 /// conservatively ANSI.
211 pub const BIGQUERY: Self = Self {
212 struct_constructor: true,
213 typecast_operator: false,
214 subscript: false,
215 slice_step: false,
216 collate: false,
217 at_time_zone: false,
218 semi_structured_access: false,
219 array_constructor: false,
220 multidim_array_literals: false,
221 collection_literals: false,
222 row_constructor: false,
223 field_selection: false,
224 field_wildcard: false,
225 typed_string_literals: true,
226 typed_interval_literal: true,
227 relaxed_interval_syntax: false,
228 mysql_interval_operator: false,
229 positional_column: false,
230 lambda_keyword: false,
231 };
232}
233
234impl TableFactorSyntax {
235 /// The `BIGQUERY` preset for table factor syntax.
236 pub const BIGQUERY: Self = Self {
237 unnest: true,
238 unnest_with_offset: true,
239 // GoogleSQL's `FROM t PIVOT(<agg> FOR <col> IN (<vals>))` table factor. No
240 // BigQuery oracle ships here, so the standard-PIVOT gate is enabled on the
241 // conservative preset per the documented grammar (the `unnest_with_offset`
242 // precedent). BigQuery uses only the explicit value list, but the shared gate
243 // also admits `ANY`/subquery and `DEFAULT ON NULL` — over-acceptance that is
244 // unmeasurable without an oracle.
245 pivot_value_sources: true,
246 lateral: false,
247 table_functions: false,
248 rows_from: false,
249 table_function_ordinality: false,
250 special_function_table_source: true,
251 pivot: false,
252 unpivot: false,
253 show_ref: false,
254 from_values: false,
255 json_table: false,
256 xml_table: false,
257 table_expr_factor: false,
258 match_recognize: false,
259 open_json: false,
260 };
261}
262
263impl TypeNameSyntax {
264 /// BigQuery type names: ANSI baseline plus angle-bracket `STRUCT<>`/`ARRAY<>`.
265 pub const BIGQUERY: Self = Self {
266 angle_bracket_types: true,
267 extended_scalar_type_names: false,
268 enum_type: false,
269 set_type: false,
270 numeric_modifiers: false,
271 integer_display_width: false,
272 composite_types: false,
273 varchar_requires_length: false,
274 zoned_temporal_types: true,
275 empty_type_parens: false,
276 character_set_annotation: false,
277 signed_type_modifier: false,
278 nullable_type: false,
279 low_cardinality_type: false,
280 fixed_string_type: false,
281 datetime64_type: false,
282 nested_type: false,
283 bit_width_integer_names: false,
284 liberal_type_names: false,
285 string_type_modifiers: false,
286 };
287}
288
289impl FeatureSet {
290 /// BigQuery / ZetaSQL as ANSI-derived dialect data (see the module docs for the full
291 /// derivation rationale and the conservatism bar).
292 pub const BIGQUERY: Self = Self {
293 // BigQuery column/alias resolution is case-insensitive (table/dataset names are
294 // case-sensitive); `Casing::Lower` is the closest single fit (the `Casing` known-
295 // limitation paragraph names this shape). Identity only — the interned text still
296 // renders exactly as written, so this never affects acceptance.
297 identifier_casing: Casing::Lower,
298 // The lexical delta over ANSI: backtick-only identifier quoting (with `"` handed to the
299 // string scanner via `double_quoted_strings` below).
300 identifier_quotes: BIGQUERY_IDENTIFIER_QUOTES,
301 default_null_ordering: NullOrdering::NullsLast,
302 // The one reserved-set delta over ANSI: `PIVOT`/`UNPIVOT` are position-reserved on
303 // the `ColId` axis so a bare `FROM t PIVOT (…)` reaches the operator instead of
304 // aliasing `t` as `pivot` (see [`BIGQUERY_PIVOT_RESERVATION`]). Confined to
305 // `reserved_column_name`; the function/type/bare-label positions stay ANSI, matching
306 // BigQuery's non-reserved status for the words. (BigQuery's `QUALIFY`/`EXCEPT`-in-star
307 // reservations ride gates not modelled here.)
308 reserved_column_name: BIGQUERY_RESERVED_COLUMN_NAME,
309 reserved_function_name: RESERVED_FUNCTION_NAME,
310 reserved_type_name: RESERVED_TYPE_NAME,
311 reserved_bare_alias: RESERVED_BARE_ALIAS,
312 reserved_as_label: KeywordSet::EMPTY,
313 catalog_qualified_names: true,
314 byte_classes: STANDARD_BYTE_CLASSES,
315 binding_powers: STANDARD_BINDING_POWERS,
316 set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
317 // `"…"` double-quoted strings (the coupled half of the backtick-only identifier lexis).
318 string_literals: StringLiteralSyntax::BIGQUERY,
319 numeric_literals: NumericLiteralSyntax::ANSI,
320 parameters: ParameterSyntax::ANSI,
321 session_variables: SessionVariableSyntax::ANSI,
322 identifier_syntax: IdentifierSyntax::ANSI,
323 // `FROM UNNEST(…)` and its `WITH OFFSET` tail — the capstone this preset exposes.
324 table_expressions: TableExpressionSyntax::BIGQUERY,
325 join_syntax: JoinSyntax::BIGQUERY,
326 table_factor_syntax: TableFactorSyntax::BIGQUERY,
327 expression_syntax: ExpressionSyntax::BIGQUERY,
328 operator_syntax: OperatorSyntax::ANSI,
329 call_syntax: CallSyntax::ANSI,
330 string_func_forms: StringFuncForms::ANSI,
331 aggregate_call_syntax: AggregateCallSyntax::ANSI,
332 predicate_syntax: PredicateSyntax::ANSI,
333 pipe_operator: PipeOperator::StringConcat,
334 double_ampersand: DoubleAmpersand::Unsupported,
335 keyword_operators: KeywordOperators::Unsupported,
336 caret_operator: CaretOperator::Unsupported,
337 hash_bitwise_xor: false,
338 comment_syntax: CommentSyntax::ANSI,
339 mutation_syntax: MutationSyntax::ANSI,
340 statement_ddl_gates: StatementDdlGates::ANSI,
341 view_sequence_clause_syntax: ViewSequenceClauseSyntax::ANSI,
342 create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
343 column_definition_syntax: ColumnDefinitionSyntax::ANSI,
344 constraint_syntax: ConstraintSyntax::ANSI,
345 index_alter_syntax: IndexAlterSyntax::ANSI,
346 existence_guards: ExistenceGuards::ANSI,
347 // `pipe_syntax` stays off (deferred judgment — see the module docs); every other SELECT
348 // knob is conservatively ANSI.
349 select_syntax: SelectSyntax::ANSI,
350 query_tail_syntax: QueryTailSyntax::ANSI,
351 grouping_syntax: GroupingSyntax::ANSI,
352 utility_syntax: UtilitySyntax::ANSI,
353 transaction_syntax: TransactionSyntax::ANSI,
354 show_syntax: ShowSyntax::ANSI,
355 maintenance_syntax: MaintenanceSyntax::ANSI,
356 access_control_syntax: AccessControlSyntax::ANSI,
357 type_name_syntax: TypeNameSyntax::BIGQUERY,
358 // No BigQuery-specific Tier-1 output spelling yet; render the portable ANSI canonical
359 // type names (a `TargetSpelling::BigQuery` is render work a later ticket owns).
360 target_spelling: TargetSpelling::Ansi,
361 };
362}
363
364/// Prefer [`FeatureSet::BIGQUERY`] for struct update.
365pub const BIGQUERY: FeatureSet = FeatureSet::BIGQUERY;
366
367// Compile-time proof the BigQuery preset claims no shared tokenizer trigger twice. Beyond ANSI
368// it adds one lexical trigger — the backtick identifier opener — with a single claimant (no
369// enabled expression grammar lexes a backtick), and it hands `"` to the string scanner
370// (`double_quoted_strings` on) *while dropping `"` from the identifier quote set*, so `"` also
371// keeps a single claimant. The two `UNNEST` gates are contextual keyword grammar with no
372// tokenizer trigger. Kept as a ratchet so a future BigQuery delta that *does* add a contending
373// trigger (e.g. re-listing `"` as an identifier quote) fails the build here.
374const _: () = assert!(FeatureSet::BIGQUERY.is_lexically_consistent());
375// The two sibling self-consistency registries are ratcheted the same way, so the
376// parse-entry `debug_assert!` folds all three to dead code for this preset: the
377// `unnest_with_offset` tail rides the enabled `unnest` base, and no two features contend
378// for one parser-position head.
379const _: () = assert!(FeatureSet::BIGQUERY.has_satisfied_feature_dependencies());
380const _: () = assert!(FeatureSet::BIGQUERY.has_no_grammar_conflict());
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn bigquery_is_ansi_plus_the_gates_and_two_lexical_facts() {
388 // The preset is ANSI with a documented, closed set of divergent axes: the two lexical
389 // facts (case-folding, backtick-only quoting coupled with double-quoted strings), the
390 // enabled table-expression/factor gates, the `STRUCT`/`ARRAY` angle-bracket type-position
391 // support, and the `PIVOT`/`UNPIVOT` `ColId` reservation.
392 // Asserting the whole rest equals ANSI keeps the "ANSI-derived, every delta documented"
393 // claim honest against a future stray edit.
394 // Bind to locals so the const reads are not flagged by clippy's
395 // `assertions_on_constants`.
396 let ansi = FeatureSet::ANSI;
397 let bq = FeatureSet::BIGQUERY;
398
399 // The two lexical facts.
400 assert_eq!(bq.identifier_casing, Casing::Lower);
401 assert_ne!(bq.identifier_casing, ansi.identifier_casing);
402 assert_eq!(bq.identifier_quotes, BIGQUERY_IDENTIFIER_QUOTES);
403 assert_ne!(bq.identifier_quotes, ansi.identifier_quotes);
404 // The coupling: `"` is dropped from the quote set and handed to the string scanner.
405 assert!(bq.string_literals.double_quoted_strings);
406 assert!(!bq.identifier_quotes.iter().any(|quote| quote.open() == '"'));
407 // Backtick is the sole identifier quote; no bracket (unlike SQLite/MSSQL) and no `"`.
408 assert!(bq.identifier_quotes.iter().any(|quote| quote.open() == '`'));
409 assert_eq!(bq.identifier_quotes.len(), 1);
410
411 // The one divergent string sub-preset (the double-quote coupling).
412 assert_eq!(bq.string_literals, StringLiteralSyntax::BIGQUERY);
413 assert_ne!(bq.string_literals, ansi.string_literals);
414 // The divergent table-expression sub-preset.
415 assert_eq!(bq.table_expressions, TableExpressionSyntax::BIGQUERY);
416 assert_eq!(bq.table_factor_syntax, TableFactorSyntax::BIGQUERY);
417 assert_ne!(bq.table_factor_syntax, ansi.table_factor_syntax);
418
419 // The reserved-set delta: `PIVOT`/`UNPIVOT` are added on the `ColId` axis only, so a
420 // bare `FROM t PIVOT (…)` reaches the operator. Dropping them recovers the ANSI set
421 // verbatim; the other three positions take no delta over ANSI.
422 assert_eq!(bq.reserved_column_name, BIGQUERY_RESERVED_COLUMN_NAME);
423 assert_ne!(bq.reserved_column_name, ansi.reserved_column_name);
424 assert_eq!(
425 bq.reserved_column_name
426 .difference(BIGQUERY_PIVOT_RESERVATION),
427 ansi.reserved_column_name,
428 );
429 assert!(bq.reserved_column_name.contains(Keyword::Pivot));
430 assert!(bq.reserved_column_name.contains(Keyword::Unpivot));
431 // Confined to `ColId`: function/type/bare-label positions stay ANSI, so `pivot(1)`,
432 // `CAST(1 AS pivot)`, and `SELECT 1 pivot` keep parsing (BigQuery's non-reserved
433 // status for the words).
434 assert_eq!(bq.reserved_function_name, ansi.reserved_function_name);
435 assert_eq!(bq.reserved_type_name, ansi.reserved_type_name);
436 assert_eq!(bq.reserved_bare_alias, ansi.reserved_bare_alias);
437 assert!(!bq.reserved_function_name.contains(Keyword::Pivot));
438 assert!(!bq.reserved_type_name.contains(Keyword::Pivot));
439 assert!(!bq.reserved_bare_alias.contains(Keyword::Pivot));
440 assert_eq!(bq.reserved_as_label, KeywordSet::EMPTY);
441
442 // Everything else is inherited verbatim from ANSI — including SELECT (pipe_syntax is
443 // deferred, so it stays off) and numeric/parameter surfaces.
444 assert_eq!(bq.select_syntax, ansi.select_syntax);
445 assert!(!bq.query_tail_syntax.pipe_syntax);
446 assert_eq!(bq.numeric_literals, ansi.numeric_literals);
447 assert_eq!(bq.parameters, ansi.parameters);
448 // BigQuery adds the `STRUCT(...)` value constructor; every other expression knob
449 // is inherited verbatim from ANSI.
450 assert_eq!(bq.expression_syntax, ExpressionSyntax::BIGQUERY);
451 assert!(bq.expression_syntax.struct_constructor);
452 assert!(!ansi.expression_syntax.struct_constructor);
453 assert_eq!(
454 ExpressionSyntax {
455 struct_constructor: false,
456 ..bq.expression_syntax
457 },
458 ansi.expression_syntax
459 );
460 assert_eq!(bq.session_variables, ansi.session_variables);
461 assert_eq!(bq.identifier_syntax, ansi.identifier_syntax);
462 assert_eq!(bq.operator_syntax, ansi.operator_syntax);
463 assert_eq!(bq.call_syntax, ansi.call_syntax);
464 assert_eq!(bq.predicate_syntax, ansi.predicate_syntax);
465 assert_eq!(bq.mutation_syntax, ansi.mutation_syntax);
466 assert_eq!(bq.statement_ddl_gates, ansi.statement_ddl_gates);
467 assert_eq!(
468 bq.create_table_clause_syntax,
469 ansi.create_table_clause_syntax
470 );
471 assert_eq!(bq.column_definition_syntax, ansi.column_definition_syntax);
472 assert_eq!(bq.constraint_syntax, ansi.constraint_syntax);
473 assert_eq!(bq.index_alter_syntax, ansi.index_alter_syntax);
474 assert_eq!(bq.existence_guards, ansi.existence_guards);
475 assert_eq!(bq.utility_syntax, ansi.utility_syntax);
476 assert!(bq.type_name_syntax.angle_bracket_types);
477 assert!(!ansi.type_name_syntax.angle_bracket_types);
478 assert_eq!(
479 TypeNameSyntax {
480 angle_bracket_types: false,
481 ..bq.type_name_syntax
482 },
483 ansi.type_name_syntax,
484 );
485 assert_eq!(bq.byte_classes, ansi.byte_classes);
486 assert_eq!(bq.binding_powers, ansi.binding_powers);
487 assert_eq!(bq.target_spelling, ansi.target_spelling);
488 assert_eq!(bq.default_null_ordering, ansi.default_null_ordering);
489 }
490
491 #[test]
492 fn bigquery_enables_exactly_the_unnest_gates_and_double_quote_string() {
493 // The capstone: the first-class UNNEST factor, its BigQuery WITH OFFSET tail, and
494 // double-quoted strings are on, and each is off in the ANSI base it derives from.
495 // Forcing the flags back off recovers the ANSI sub-presets verbatim.
496 let ansi = FeatureSet::ANSI;
497 let bq = FeatureSet::BIGQUERY;
498
499 assert!(bq.table_factor_syntax.unnest && !ansi.table_factor_syntax.unnest);
500 assert!(
501 bq.table_factor_syntax.unnest_with_offset
502 && !ansi.table_factor_syntax.unnest_with_offset
503 );
504 assert!(
505 bq.string_literals.double_quoted_strings && !ansi.string_literals.double_quoted_strings
506 );
507 // The backtick opener is the lexical gate (an identifier-quote delta, not a bool).
508 assert!(bq.identifier_quotes.iter().any(|quote| quote.open() == '`'));
509 assert!(
510 !ansi
511 .identifier_quotes
512 .iter()
513 .any(|quote| quote.open() == '`')
514 );
515
516 assert_eq!(
517 TableFactorSyntax {
518 unnest: false,
519 unnest_with_offset: false,
520 // BigQuery adds the standard PIVOT gate over the ANSI baseline.
521 pivot_value_sources: false,
522 ..bq.table_factor_syntax
523 },
524 ansi.table_factor_syntax,
525 );
526 assert_eq!(
527 StringLiteralSyntax {
528 double_quoted_strings: false,
529 ..bq.string_literals
530 },
531 ansi.string_literals,
532 );
533 }
534
535 #[test]
536 fn bigquery_is_lexically_consistent_and_dependency_clean() {
537 // Both self-consistency registries must be clean: the backtick quote has a single
538 // claimant, `"` is handed to the string scanner (dropped from the quote set), and — the
539 // fact this preset is the first to prove positively — the `unnest_with_offset` tail
540 // rides the enabled `unnest` base, so the feature-dependency registry is satisfied.
541 let bq = FeatureSet::BIGQUERY;
542 assert_eq!(bq.lexical_conflict(), None);
543 assert!(bq.is_lexically_consistent());
544 assert_eq!(bq.feature_dependencies(), None);
545 assert!(bq.has_satisfied_feature_dependencies());
546 assert_eq!(bq.grammar_conflict(), None);
547 assert!(bq.has_no_grammar_conflict());
548 // Guard the dependency direction explicitly: dropping the `unnest` base while keeping
549 // the `WITH OFFSET` tail must trip the registry — this preset is the first whose flip
550 // exercises `UnnestWithOffsetWithoutUnnest`.
551 let broken = FeatureSet::BIGQUERY.with(
552 super::super::FeatureDelta::EMPTY.table_factor_syntax(TableFactorSyntax {
553 unnest: false,
554 ..bq.table_factor_syntax
555 }),
556 );
557 assert_eq!(
558 broken.feature_dependencies(),
559 Some(super::super::FeatureDependencyViolation::UnnestWithOffsetWithoutUnnest),
560 );
561 }
562 #[test]
563 fn bigquery_closed_delta_axes_match_documented_set() {
564 crate::dialect::closed_delta::assert_closed_delta(
565 &FeatureSet::ANSI,
566 &FeatureSet::BIGQUERY,
567 &[
568 "identifier_casing",
569 "identifier_quotes",
570 "reserved_column_name",
571 "string_literals",
572 "table_expressions",
573 "table_factor_syntax",
574 "expression_syntax",
575 "type_name_syntax",
576 ],
577 );
578 }
579}