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//! Two table-expression gates carry the headline 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//! One expression gate:
36//!
37//! - [`struct_constructor`](ExpressionSyntax::struct_constructor) — the `STRUCT(...)`
38//! value constructor (`STRUCT(1, 2)`, `STRUCT(x AS a)`, `STRUCT<a INT64>(1)`), the
39//! documented GoogleSQL tuple builder. Its own flag doc names BigQuery as the motivating
40//! dialect; the `(`/`<` lookahead keeps a bare `struct` an ordinary name, so the gate is
41//! additive over ANSI.
42//!
43//! # The two lexical facts over ANSI
44//!
45//! - **Backtick identifier quoting.** BigQuery quotes identifiers with the backtick
46//! `` `name` `` alone — its `"…"` and `'…'` are *both* string literals. So
47//! [`BIGQUERY_IDENTIFIER_QUOTES`] lists only the backtick (unlike the SQLite/Databricks/MSSQL
48//! bracket-or-double-quote sets, and unlike ANSI's `"…"`), and
49//! [`double_quoted_strings`](StringLiteralSyntax::double_quoted_strings) is correspondingly
50//! **on** so `"x"` lexes as a string, never an identifier — exactly the MySQL default
51//! (`ANSI_QUOTES` off) lexis. The two facts are coupled: `"` has a single claimant (the
52//! string scanner) precisely because it is absent from the quote set, the
53//! [`DoubleQuoteStringVersusIdentifier`](super::LexicalConflict) hazard the `const` assert
54//! below rules out. The backtick likewise has a single claimant — no enabled expression
55//! grammar lexes a backtick.
56//! - **Case folding.** BigQuery column and alias references resolve case-insensitively (table
57//! and dataset names are case-sensitive), so [`identifier_casing`](FeatureSet::identifier_casing)
58//! is [`Casing::Lower`] — the closest single fit per the [`Casing`] doc's
59//! *known-limitation* paragraph, which names exactly this "case-insensitive column beside a
60//! case-sensitive table" shape (shared with MySQL/T-SQL) as one no single fold can express;
61//! `Casing::Lower` is the value it prescribes. The interned text still renders exactly as
62//! written; the fold is identity-only and never affects acceptance. (The
63//! per-identifier-kind table-vs-column sensitivity split is that documented `Casing`
64//! limitation and a deliberate future extension, not modelled here.)
65//!
66//! # Deliberately deferred (conservative reject)
67//!
68//! [`pipe_syntax`](QueryTailSyntax::pipe_syntax) — BigQuery/ZetaSQL query pipe syntax
69//! (`FROM t |> WHERE x |> SELECT a`) — stays **off**, and this is a considered judgment, not an
70//! oversight. Its own flag doc names the BigQuery preset as the eventual home, but reads
71//! the honesty bar the other way for now: the framework ships only the reference `|> WHERE`
72//! operator, so enabling the gate today would accept `|> WHERE` while rejecting every other
73//! pipe operator — a fragment a reader of this module could not predict, and with no BigQuery
74//! oracle the boundary cannot be measured either. The flag doc's own argument (that even the
75//! permissive `LENIENT` must wait until the `planner-parity-pipe-*` tickets make the
76//! pipe-operator surface coherent) cuts identically for BigQuery. Leaving it off with that
77//! reasoning cited is the correct call; flipping it on merely because the doc names BigQuery
78//! would ship an incoherent half-surface. The flip is deferred to the pipe-surface tickets.
79//!
80//! Everything else BigQuery (the `STRUCT<…>`/`ARRAY<…>` *type-position* surface —
81//! `CAST(x AS STRUCT<…>)` and column types, distinct from the value constructor above —
82//! `SELECT AS STRUCT/VALUE`,
83//! `EXCEPT`/`REPLACE` in `SELECT *`, `QUALIFY`, table-name backtick paths with dots,
84//! parameterised `@name` / `?` binds, the `SAFE.` function prefix, …) has no modelled gate and
85//! is a clean reject routed to follow-up tickets, never a silent over-accept.
86
87use super::{
88 AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
89 ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
90 DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
91 IdentifierQuote, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, Keyword, KeywordOperators,
92 KeywordSet, MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax,
93 OperatorSyntax, ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax,
94 RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME,
95 STANDARD_BYTE_CLASSES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
96 StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
97 TypeNameSyntax, UtilitySyntax,
98};
99use crate::precedence::{STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS};
100
101/// BigQuery identifier quoting: the backtick `` `…` `` alone. BigQuery spells a quoted
102/// identifier `` `a` ``; its `"a"` and `'a'` are *both* string constants, so `"` is
103/// deliberately absent here (and [`StringLiteralSyntax::BIGQUERY`] turns
104/// `double_quoted_strings` on so `"` unambiguously lexes a string) — the same backtick-only
105/// lexis MySQL uses under its default `ANSI_QUOTES`-off mode.
106pub const BIGQUERY_IDENTIFIER_QUOTES: &[IdentifierQuote] = &[IdentifierQuote::Symmetric('`')];
107
108/// `PIVOT` and `UNPIVOT`, GoogleSQL's row/column rotation operators. Neither is a
109/// BigQuery *reserved* keyword — both are absent from the GoogleSQL reserved-keyword
110/// list and stay usable as ordinary unquoted identifiers (GoogleSQL lexical-structure
111/// reference, "Reserved keywords":
112/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#reserved_keywords>).
113/// They are instead *position-reserved*: the `pivot_operator` / `unpivot_operator`
114/// grammar attaches directly to a `from_item`, so `FROM t PIVOT (…)` must read the
115/// operator, not a correlation alias named `pivot` (GoogleSQL query-syntax reference,
116/// "Pivot operator" / "Unpivot operator":
117/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#pivot_operator>).
118///
119/// Modelling that reachability in this parser's shared-`ColId` alias grammar means
120/// reserving both on the `ColId` axis only — [`RESERVED_COLUMN_NAME`], the set a bare
121/// *and* `AS`-introduced table alias, a column name, and a table name all draw from —
122/// and *not* the function-name, type-name, or projection bare-label axes, which stay
123/// open (`pivot(1)`, `CAST(1 AS pivot)`, `SELECT 1 pivot` still parse), matching
124/// BigQuery's non-reserved status. This is the deliberate minimal deviation from the
125/// DuckDB `DUCKDB_PIVOT_RESERVATION`, which unions into all four positions because
126/// DuckDB's engine genuinely classes the words `reserved`. The unavoidable reachability
127/// cost: under this preset an unquoted `pivot`/`unpivot` is not admitted as a column/table
128/// name or a table alias — quote it (`` `pivot` ``) to use it as an identifier there.
129pub const BIGQUERY_PIVOT_RESERVATION: KeywordSet =
130 KeywordSet::from_keywords(&[Keyword::Pivot, Keyword::Unpivot]);
131
132/// The ANSI `ColId` reject set plus [`BIGQUERY_PIVOT_RESERVATION`]; see that const for
133/// why the reservation is confined to this one axis. The function-name, type-name, and
134/// bare-label reject sets take no delta over ANSI, so BigQuery keeps the shared consts
135/// for those three positions.
136pub const BIGQUERY_RESERVED_COLUMN_NAME: KeywordSet =
137 RESERVED_COLUMN_NAME.union(BIGQUERY_PIVOT_RESERVATION);
138
139impl StringLiteralSyntax {
140 /// BigQuery string surface: the ANSI baseline plus `"…"` double-quoted string constants
141 /// (BigQuery quotes strings with both `'…'` and `"…"`, reserving the backtick for
142 /// identifiers). Enabling this is what lets [`BIGQUERY_IDENTIFIER_QUOTES`] drop `"` from the
143 /// quote set without stranding the byte. Every other string knob is conservatively ANSI —
144 /// BigQuery's backslash escape sequences have no BigQuery-citing flag doc or oracle here and
145 /// are deferred rather than guessed at (`backslash_escapes` stays off).
146 pub const BIGQUERY: Self = Self {
147 double_quoted_strings: true,
148 ..StringLiteralSyntax::ANSI
149 };
150}
151
152impl TableExpressionSyntax {
153 /// BigQuery table-expression surface: the ANSI baseline plus the `FOR SYSTEM_TIME AS OF`
154 /// time-travel modifier. The first-class `UNNEST(…)` factor and its `WITH OFFSET` tail
155 /// ride [`TableFactorSyntax`]; every other table knob is conservatively ANSI.
156 pub const BIGQUERY: Self = Self {
157 // `FROM t FOR SYSTEM_TIME AS OF <ts>` — BigQuery's sole time-travel spelling.
158 table_version: true,
159 ..TableExpressionSyntax::ANSI
160 };
161}
162
163impl JoinSyntax {
164 /// The `BIGQUERY` preset for join syntax.
165 pub const BIGQUERY: Self = Self { ..JoinSyntax::ANSI };
166}
167
168impl ExpressionSyntax {
169 /// BigQuery expression surface: the ANSI baseline plus the `STRUCT(...)` value
170 /// constructor (`STRUCT(1, 2)`, `STRUCT(x AS a)`, `STRUCT<a INT64>(1)`), a documented
171 /// GoogleSQL form with no differential oracle here. Every other expression knob stays
172 /// conservatively ANSI.
173 pub const BIGQUERY: Self = Self {
174 struct_constructor: true,
175 ..ExpressionSyntax::ANSI
176 };
177}
178
179impl TableFactorSyntax {
180 /// The `BIGQUERY` preset for table factor syntax.
181 pub const BIGQUERY: Self = Self {
182 unnest: true,
183 unnest_with_offset: true,
184 // GoogleSQL's `FROM t PIVOT(<agg> FOR <col> IN (<vals>))` table factor. No
185 // BigQuery oracle ships here, so the standard-PIVOT gate is enabled on the
186 // conservative preset per the documented grammar (the `unnest_with_offset`
187 // precedent). BigQuery uses only the explicit value list, but the shared gate
188 // also admits `ANY`/subquery and `DEFAULT ON NULL` — over-acceptance that is
189 // unmeasurable without an oracle.
190 pivot_value_sources: true,
191 ..TableFactorSyntax::ANSI
192 };
193}
194
195impl TypeNameSyntax {
196 /// BigQuery type names: ANSI baseline plus angle-bracket `STRUCT<>`/`ARRAY<>`.
197 pub const BIGQUERY: Self = Self {
198 angle_bracket_types: true,
199 ..TypeNameSyntax::ANSI
200 };
201}
202
203impl FeatureSet {
204 /// BigQuery / ZetaSQL as ANSI-derived dialect data (see the module docs for the full
205 /// derivation rationale and the conservatism bar).
206 pub const BIGQUERY: Self = Self {
207 // BigQuery column/alias resolution is case-insensitive (table/dataset names are
208 // case-sensitive); `Casing::Lower` is the closest single fit (the `Casing` known-
209 // limitation paragraph names this shape). Identity only — the interned text still
210 // renders exactly as written, so this never affects acceptance.
211 identifier_casing: Casing::Lower,
212 // The lexical delta over ANSI: backtick-only identifier quoting (with `"` handed to the
213 // string scanner via `double_quoted_strings` below).
214 identifier_quotes: BIGQUERY_IDENTIFIER_QUOTES,
215 default_null_ordering: NullOrdering::NullsLast,
216 // The one reserved-set delta over ANSI: `PIVOT`/`UNPIVOT` are position-reserved on
217 // the `ColId` axis so a bare `FROM t PIVOT (…)` reaches the operator instead of
218 // aliasing `t` as `pivot` (see [`BIGQUERY_PIVOT_RESERVATION`]). Confined to
219 // `reserved_column_name`; the function/type/bare-label positions stay ANSI, matching
220 // BigQuery's non-reserved status for the words. (BigQuery's `QUALIFY`/`EXCEPT`-in-star
221 // reservations ride gates not modelled here.)
222 reserved_column_name: BIGQUERY_RESERVED_COLUMN_NAME,
223 reserved_function_name: RESERVED_FUNCTION_NAME,
224 reserved_type_name: RESERVED_TYPE_NAME,
225 reserved_bare_alias: RESERVED_BARE_ALIAS,
226 reserved_as_label: KeywordSet::EMPTY,
227 catalog_qualified_names: true,
228 byte_classes: STANDARD_BYTE_CLASSES,
229 binding_powers: STANDARD_BINDING_POWERS,
230 set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
231 // `"…"` double-quoted strings (the coupled half of the backtick-only identifier lexis).
232 string_literals: StringLiteralSyntax::BIGQUERY,
233 numeric_literals: NumericLiteralSyntax::ANSI,
234 parameters: ParameterSyntax::ANSI,
235 session_variables: SessionVariableSyntax::ANSI,
236 identifier_syntax: IdentifierSyntax::ANSI,
237 // `FROM UNNEST(…)` and its `WITH OFFSET` tail — the capstone this preset exposes.
238 table_expressions: TableExpressionSyntax::BIGQUERY,
239 join_syntax: JoinSyntax::BIGQUERY,
240 table_factor_syntax: TableFactorSyntax::BIGQUERY,
241 expression_syntax: ExpressionSyntax::BIGQUERY,
242 operator_syntax: OperatorSyntax::ANSI,
243 call_syntax: CallSyntax::ANSI,
244 string_func_forms: StringFuncForms::ANSI,
245 aggregate_call_syntax: AggregateCallSyntax::ANSI,
246 predicate_syntax: PredicateSyntax::ANSI,
247 pipe_operator: PipeOperator::StringConcat,
248 double_ampersand: DoubleAmpersand::Unsupported,
249 keyword_operators: KeywordOperators::Unsupported,
250 caret_operator: CaretOperator::Unsupported,
251 hash_bitwise_xor: false,
252 comment_syntax: CommentSyntax::ANSI,
253 mutation_syntax: MutationSyntax::ANSI,
254 statement_ddl_gates: StatementDdlGates::ANSI,
255 create_table_clause_syntax: CreateTableClauseSyntax::ANSI,
256 column_definition_syntax: ColumnDefinitionSyntax::ANSI,
257 constraint_syntax: ConstraintSyntax::ANSI,
258 index_alter_syntax: IndexAlterSyntax::ANSI,
259 existence_guards: ExistenceGuards::ANSI,
260 // `pipe_syntax` stays off (deferred judgment — see the module docs); every other SELECT
261 // knob is conservatively ANSI.
262 select_syntax: SelectSyntax::ANSI,
263 query_tail_syntax: QueryTailSyntax::ANSI,
264 grouping_syntax: GroupingSyntax::ANSI,
265 utility_syntax: UtilitySyntax::ANSI,
266 show_syntax: ShowSyntax::ANSI,
267 maintenance_syntax: MaintenanceSyntax::ANSI,
268 access_control_syntax: AccessControlSyntax::ANSI,
269 type_name_syntax: TypeNameSyntax::BIGQUERY,
270 // No BigQuery-specific Tier-1 output spelling yet; render the portable ANSI canonical
271 // type names (a `TargetSpelling::BigQuery` is render work a later ticket owns).
272 target_spelling: TargetSpelling::Ansi,
273 };
274}
275
276/// Prefer [`FeatureSet::BIGQUERY`] for struct update.
277pub const BIGQUERY: FeatureSet = FeatureSet::BIGQUERY;
278
279// Compile-time proof the BigQuery preset claims no shared tokenizer trigger twice. Beyond ANSI
280// it adds one lexical trigger — the backtick identifier opener — with a single claimant (no
281// enabled expression grammar lexes a backtick), and it hands `"` to the string scanner
282// (`double_quoted_strings` on) *while dropping `"` from the identifier quote set*, so `"` also
283// keeps a single claimant. The two `UNNEST` gates are contextual keyword grammar with no
284// tokenizer trigger. Kept as a ratchet so a future BigQuery delta that *does* add a contending
285// trigger (e.g. re-listing `"` as an identifier quote) fails the build here.
286const _: () = assert!(FeatureSet::BIGQUERY.is_lexically_consistent());
287// The two sibling self-consistency registries are ratcheted the same way, so the
288// parse-entry `debug_assert!` folds all three to dead code for this preset: the
289// `unnest_with_offset` tail rides the enabled `unnest` base, and no two features contend
290// for one parser-position head.
291const _: () = assert!(FeatureSet::BIGQUERY.has_satisfied_feature_dependencies());
292const _: () = assert!(FeatureSet::BIGQUERY.has_no_grammar_conflict());
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn bigquery_is_ansi_plus_the_gates_and_two_lexical_facts() {
300 // The preset is ANSI with a documented, closed set of divergent axes: the two lexical
301 // facts (case-folding, backtick-only quoting coupled with double-quoted strings), the
302 // enabled table-expression/factor gates, and the `PIVOT`/`UNPIVOT` `ColId` reservation.
303 // Asserting the whole rest equals ANSI keeps the "ANSI-derived, every delta documented"
304 // claim honest against a future stray edit.
305 // Bind to locals so the const reads are not flagged by clippy's
306 // `assertions_on_constants`.
307 let ansi = FeatureSet::ANSI;
308 let bq = FeatureSet::BIGQUERY;
309
310 // The two lexical facts.
311 assert_eq!(bq.identifier_casing, Casing::Lower);
312 assert_ne!(bq.identifier_casing, ansi.identifier_casing);
313 assert_eq!(bq.identifier_quotes, BIGQUERY_IDENTIFIER_QUOTES);
314 assert_ne!(bq.identifier_quotes, ansi.identifier_quotes);
315 // The coupling: `"` is dropped from the quote set and handed to the string scanner.
316 assert!(bq.string_literals.double_quoted_strings);
317 assert!(!bq.identifier_quotes.iter().any(|quote| quote.open() == '"'));
318 // Backtick is the sole identifier quote; no bracket (unlike SQLite/MSSQL) and no `"`.
319 assert!(bq.identifier_quotes.iter().any(|quote| quote.open() == '`'));
320 assert_eq!(bq.identifier_quotes.len(), 1);
321
322 // The one divergent string sub-preset (the double-quote coupling).
323 assert_eq!(bq.string_literals, StringLiteralSyntax::BIGQUERY);
324 assert_ne!(bq.string_literals, ansi.string_literals);
325 // The divergent table-expression sub-preset.
326 assert_eq!(bq.table_expressions, TableExpressionSyntax::BIGQUERY);
327 assert_eq!(bq.table_factor_syntax, TableFactorSyntax::BIGQUERY);
328 assert_ne!(bq.table_factor_syntax, ansi.table_factor_syntax);
329
330 // The reserved-set delta: `PIVOT`/`UNPIVOT` are added on the `ColId` axis only, so a
331 // bare `FROM t PIVOT (…)` reaches the operator. Dropping them recovers the ANSI set
332 // verbatim; the other three positions take no delta over ANSI.
333 assert_eq!(bq.reserved_column_name, BIGQUERY_RESERVED_COLUMN_NAME);
334 assert_ne!(bq.reserved_column_name, ansi.reserved_column_name);
335 assert_eq!(
336 bq.reserved_column_name
337 .difference(BIGQUERY_PIVOT_RESERVATION),
338 ansi.reserved_column_name,
339 );
340 assert!(bq.reserved_column_name.contains(Keyword::Pivot));
341 assert!(bq.reserved_column_name.contains(Keyword::Unpivot));
342 // Confined to `ColId`: function/type/bare-label positions stay ANSI, so `pivot(1)`,
343 // `CAST(1 AS pivot)`, and `SELECT 1 pivot` keep parsing (BigQuery's non-reserved
344 // status for the words).
345 assert_eq!(bq.reserved_function_name, ansi.reserved_function_name);
346 assert_eq!(bq.reserved_type_name, ansi.reserved_type_name);
347 assert_eq!(bq.reserved_bare_alias, ansi.reserved_bare_alias);
348 assert!(!bq.reserved_function_name.contains(Keyword::Pivot));
349 assert!(!bq.reserved_type_name.contains(Keyword::Pivot));
350 assert!(!bq.reserved_bare_alias.contains(Keyword::Pivot));
351 assert_eq!(bq.reserved_as_label, KeywordSet::EMPTY);
352
353 // Everything else is inherited verbatim from ANSI — including SELECT (pipe_syntax is
354 // deferred, so it stays off) and numeric/parameter surfaces.
355 assert_eq!(bq.select_syntax, ansi.select_syntax);
356 assert!(!bq.query_tail_syntax.pipe_syntax);
357 assert_eq!(bq.numeric_literals, ansi.numeric_literals);
358 assert_eq!(bq.parameters, ansi.parameters);
359 // BigQuery adds the `STRUCT(...)` value constructor; every other expression knob
360 // is inherited verbatim from ANSI.
361 assert_eq!(bq.expression_syntax, ExpressionSyntax::BIGQUERY);
362 assert!(bq.expression_syntax.struct_constructor);
363 assert!(!ansi.expression_syntax.struct_constructor);
364 assert_eq!(
365 ExpressionSyntax {
366 struct_constructor: false,
367 ..bq.expression_syntax
368 },
369 ansi.expression_syntax
370 );
371 assert_eq!(bq.session_variables, ansi.session_variables);
372 assert_eq!(bq.identifier_syntax, ansi.identifier_syntax);
373 assert_eq!(bq.operator_syntax, ansi.operator_syntax);
374 assert_eq!(bq.call_syntax, ansi.call_syntax);
375 assert_eq!(bq.predicate_syntax, ansi.predicate_syntax);
376 assert_eq!(bq.mutation_syntax, ansi.mutation_syntax);
377 assert_eq!(bq.statement_ddl_gates, ansi.statement_ddl_gates);
378 assert_eq!(
379 bq.create_table_clause_syntax,
380 ansi.create_table_clause_syntax
381 );
382 assert_eq!(bq.column_definition_syntax, ansi.column_definition_syntax);
383 assert_eq!(bq.constraint_syntax, ansi.constraint_syntax);
384 assert_eq!(bq.index_alter_syntax, ansi.index_alter_syntax);
385 assert_eq!(bq.existence_guards, ansi.existence_guards);
386 assert_eq!(bq.utility_syntax, ansi.utility_syntax);
387 assert!(bq.type_name_syntax.angle_bracket_types);
388 assert!(!ansi.type_name_syntax.angle_bracket_types);
389 assert_eq!(
390 TypeNameSyntax {
391 angle_bracket_types: false,
392 ..bq.type_name_syntax
393 },
394 ansi.type_name_syntax,
395 );
396 assert_eq!(bq.byte_classes, ansi.byte_classes);
397 assert_eq!(bq.binding_powers, ansi.binding_powers);
398 assert_eq!(bq.target_spelling, ansi.target_spelling);
399 assert_eq!(bq.default_null_ordering, ansi.default_null_ordering);
400 }
401
402 #[test]
403 fn bigquery_enables_exactly_the_unnest_gates_and_double_quote_string() {
404 // The capstone: the first-class UNNEST factor, its BigQuery WITH OFFSET tail, and
405 // double-quoted strings are on, and each is off in the ANSI base it derives from.
406 // Forcing the flags back off recovers the ANSI sub-presets verbatim.
407 let ansi = FeatureSet::ANSI;
408 let bq = FeatureSet::BIGQUERY;
409
410 assert!(bq.table_factor_syntax.unnest && !ansi.table_factor_syntax.unnest);
411 assert!(
412 bq.table_factor_syntax.unnest_with_offset
413 && !ansi.table_factor_syntax.unnest_with_offset
414 );
415 assert!(
416 bq.string_literals.double_quoted_strings && !ansi.string_literals.double_quoted_strings
417 );
418 // The backtick opener is the lexical gate (an identifier-quote delta, not a bool).
419 assert!(bq.identifier_quotes.iter().any(|quote| quote.open() == '`'));
420 assert!(
421 !ansi
422 .identifier_quotes
423 .iter()
424 .any(|quote| quote.open() == '`')
425 );
426
427 assert_eq!(
428 TableFactorSyntax {
429 unnest: false,
430 unnest_with_offset: false,
431 // BigQuery adds the standard PIVOT gate over the ANSI baseline.
432 pivot_value_sources: false,
433 ..bq.table_factor_syntax
434 },
435 ansi.table_factor_syntax,
436 );
437 assert_eq!(
438 StringLiteralSyntax {
439 double_quoted_strings: false,
440 ..bq.string_literals
441 },
442 ansi.string_literals,
443 );
444 }
445
446 #[test]
447 fn bigquery_is_lexically_consistent_and_dependency_clean() {
448 // Both self-consistency registries must be clean: the backtick quote has a single
449 // claimant, `"` is handed to the string scanner (dropped from the quote set), and — the
450 // fact this preset is the first to prove positively — the `unnest_with_offset` tail
451 // rides the enabled `unnest` base, so the feature-dependency registry is satisfied.
452 let bq = FeatureSet::BIGQUERY;
453 assert_eq!(bq.lexical_conflict(), None);
454 assert!(bq.is_lexically_consistent());
455 assert_eq!(bq.feature_dependencies(), None);
456 assert!(bq.has_satisfied_feature_dependencies());
457 assert_eq!(bq.grammar_conflict(), None);
458 assert!(bq.has_no_grammar_conflict());
459 // Guard the dependency direction explicitly: dropping the `unnest` base while keeping
460 // the `WITH OFFSET` tail must trip the registry — this preset is the first whose flip
461 // exercises `UnnestWithOffsetWithoutUnnest`.
462 let broken = FeatureSet::BIGQUERY.with(
463 super::super::FeatureDelta::EMPTY.table_factor_syntax(TableFactorSyntax {
464 unnest: false,
465 ..bq.table_factor_syntax
466 }),
467 );
468 assert_eq!(
469 broken.feature_dependencies(),
470 Some(super::super::FeatureDependencyViolation::UnnestWithOffsetWithoutUnnest),
471 );
472 }
473}