squonk_ast/dialect/conflict.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! Feature-set self-consistency registries. Three sibling checkers surface the
5//! combinations the per-feature model cannot make independent:
6//!
7//! - [`FeatureSet::lexical_conflict`] — two features that both claim the *same*
8//! context-free tokenizer trigger (a soundness hazard: fixed precedence silently
9//! *mis-parses* one of them).
10//! - [`FeatureSet::feature_dependencies`] — a grammar flag set without the base flag it
11//! rides on (an *inertness* hazard: the dependent flag is simply unreachable, never a
12//! wrong parse).
13//! - [`FeatureSet::grammar_conflict`] — two features that both read the *same*
14//! parser-position head (a soundness hazard like a lexical conflict, but one layer up: a
15//! fixed parser branch order silently shadows one reading, with the tokenizer innocent).
16//!
17//! The three are MECE by construction: a lexical trigger never appears in the dependency or
18//! grammar registries, a pure grammar dependency never appears in the other two, and a
19//! parser-position contention has no tokenizer trigger of its own and rides no base flag.
20//! All are test/debug-time properties — every shipped preset and any custom [`FeatureDelta`]
21//! should be clean under all three.
22//!
23//! These registries record the contentions with *no* defined resolution. Their positive
24//! counterpart — the multi-claimant statement heads a preset *does* union with a documented,
25//! deterministic resolution (a lookahead split, a dispatch precedence, or a deliberate
26//! one-reading exclusion), which is why they earn no variant here — is the enumerable
27//! [`MULTI_CLAIMANT_STATEMENT_HEADS`] ledger in the `head_contention` module. A new
28//! head-level `LENIENT` exclusion is recorded there, not as a registry variant.
29
30use super::*;
31
32impl FeatureSet {
33 /// The first lexical conflict in this feature set, or `None` when it is
34 /// self-consistent.
35 ///
36 /// A dialect is modelled as independent feature data, but a few features
37 /// contend for the *same* tokenizer trigger, and the tokenizer is context-free:
38 /// each trigger resolves to exactly one token kind by a fixed precedence. Enabling
39 /// two claimants of one trigger is not an error today — the precedence silently
40 /// keeps one and shadows the other (e.g. `double_quoted_strings` on top of a `"`
41 /// identifier quote makes `"x"` a string, never the identifier). This predicate
42 /// turns that implicit precedence into an explicit, testable property: every
43 /// shipped preset — and any custom [`FeatureDelta`] — should return `None`.
44 ///
45 /// The contested triggers form a MECE partition: each [`LexicalConflict`] variant
46 /// governs one distinct trigger — `"`, `[`, `:`+identifier (the `a[x:y]` slice
47 /// bound / `{a: b}` collection separator), `$`+digit, `#` (whose claimants — a line
48 /// comment, the XOR operator, a `#`-led identifier byte class, and DuckDB's `#n`
49 /// positional reference — pair up as comment-vs-identifier, XOR-vs-comment,
50 /// positional-vs-XOR, and positional-vs-comment, none of which both fire in a shipped
51 /// preset), `@name`, `<@`, `?` (the `jsonb` key-existence operator vs the anonymous
52 /// placeholder), and `@@` (the `jsonb`/text-search match operator vs the MySQL
53 /// system-variable sigil) — with no overlap, plus the byte-class
54 /// hygiene variants that keep a feature-claimed sigil byte (`$`, `@`, `:`) out of a
55 /// custom table's identifier-start class. The set is exhaustive over the tokenizer's
56 /// *shared-trigger* hazards. The other either/or features are already single-valued
57 /// *by type* — [`PipeOperator`] for `||` and [`DoubleAmpersand`] for `&&` are enums,
58 /// not overlapping booleans, so an invalid combination is unrepresentable and needs
59 /// no runtime check. The remaining multi-meaning sigils are disambiguated by
60 /// lookahead, not by enabling rival features, so they are MECE by context: `::`
61 /// (typecast) and a lone `:` before a non-identifier byte stay disjoint from `:name`
62 /// by their follow byte — only a `:` before a bare identifier (the `a[x:y]` slice
63 /// bound / `{a: b}` collection separator) contends,
64 /// which is the tracked variant above; `@@` is disjoint from `@name` by its second
65 /// `@` (its only contention is the `jsonb`-vs-system-variable trigger above), and the
66 /// `@?` operator is disjoint from every `@` claimant by its second byte; the non-digit
67 /// `$` forms and the `_charset'…'` introducer split by their own
68 /// follow-sets; `/*!` (a versioned-comment region under
69 /// [`CommentSyntax::versioned_comments`]) is disjoint from a plain `/*` block
70 /// comment by its third byte, so the two never contend for the `/*` trigger; and the
71 /// two `U&`-prefixed [`StringLiteralSyntax::unicode_strings`] surfaces — the `U&'…'`
72 /// string constant and the `U&"…"` delimited identifier — are disjoint from each
73 /// other by their third byte (`'` vs `"`) and from a plain `U`-led identifier by the
74 /// three-byte `U&'`/`U&"` lead (`U` is an ordinary identifier-start letter, so a bare
75 /// `U&1` stays a `U` word, a `&` operator, and `1`), so neither steals the other nor a
76 /// plain identifier and no variant is minted for them.
77 ///
78 /// Grammar-level gates ([`MutationSyntax`], [`StatementDdlGates`], [`SelectSyntax`],
79 /// [`TableExpressionSyntax`], …) never *contend for a trigger* — each is introduced by
80 /// a disjoint keyword (or occupies a distinct clause position) with no ordering
81 /// contention, so enabling any combination resolves to one production. They have their
82 /// own, MECE-disjoint hazard instead: a *dependency*, where a refinement flag rides a
83 /// base flag and is inert without it. That family is the sibling
84 /// [`feature_dependencies`](Self::feature_dependencies) registry; contention here stays
85 /// a purely *lexical* property covering only shared tokenizer triggers.
86 pub const fn lexical_conflict(&self) -> Option<LexicalConflict> {
87 // `"`: a string constant and a quoted identifier cannot both claim the byte.
88 if self.string_literals.double_quoted_strings
89 && identifier_quotes_open_with(self.identifier_quotes, '"')
90 {
91 return Some(LexicalConflict::DoubleQuoteStringVersusIdentifier);
92 }
93 // `[`: a bracket identifier quote claims `[` at lex time, so the `[`-punctuation
94 // expression grammar (subscript, array constructor, DuckDB list literal) — and the
95 // table-position PartiQL / SUPER path root (`FROM src[0].a`) — can never receive it.
96 if identifier_quotes_open_with(self.identifier_quotes, '[')
97 && (self.expression_syntax.subscript
98 || self.expression_syntax.array_constructor
99 || self.expression_syntax.collection_literals
100 || self.table_expressions.table_json_path)
101 {
102 return Some(LexicalConflict::BracketIdentifierVersusArraySyntax);
103 }
104 // `:`+identifier: a colon-named parameter, array slicing with a bare-identifier
105 // upper bound, and a collection literal's `key: value` separator before a
106 // bare-identifier value all claim it. The `:name` scanner binds `:`+identifier
107 // as one parameter, so inside `a[x:y]` it swallows `:y` (the slice's upper
108 // bound), inside `{a: b}` it swallows `:b` (the entry's value), and inside
109 // `a:b` it swallows the path key; a feature set enabling any of those grammars
110 // with colon parameters must pick one meaning.
111 // (`::` and a lone `:` before a non-identifier byte stay the typecast and
112 // separator regardless.)
113 if self.parameters.named_colon
114 && (self.expression_syntax.subscript
115 || self.expression_syntax.collection_literals
116 || self.expression_syntax.semi_structured_access)
117 {
118 return Some(LexicalConflict::ColonParameterVersusSliceBound);
119 }
120 // `$`+digit: a money literal and a positional parameter cannot both claim it
121 // (the scanner tries money first, shadowing the parameter).
122 if self.numeric_literals.money_literals && self.parameters.positional_dollar {
123 return Some(LexicalConflict::MoneyVersusPositionalDollar);
124 }
125 // `$`+identifier-start: a SQLite `$name` parameter and a PostgreSQL
126 // `$tag$…$tag$` dollar-quote opener both lead with `$` then a tag/identifier
127 // byte (the classes overlap), so a feature set enabling both resolves the byte
128 // by fixed scan precedence, shadowing one. `$`+digit stays disjoint (that is
129 // the money-vs-positional trigger above), so only the identifier-lead contends.
130 if self.parameters.named_dollar && self.string_literals.dollar_quoted_strings {
131 return Some(LexicalConflict::NamedDollarParameterVersusDollarQuotedString);
132 }
133 // `#`: a line comment and a `#`-led identifier cannot both claim it (the comment
134 // branch wins, so a `#`-start byte class never lexes a `#name` word).
135 if self.comment_syntax.line_comment_hash
136 && self
137 .byte_classes
138 .has_class(b'#', lex_class::CLASS_IDENTIFIER_START)
139 {
140 return Some(LexicalConflict::HashCommentVersusHashIdentifier);
141 }
142 // `#`: PostgreSQL's bitwise-XOR operator and a `#` line comment cannot both claim
143 // it (the comment is skipped as trivia before the operator scan can see `#`, so the
144 // XOR operator is silently shadowed). The third claimant of the `#` trigger, after
145 // the comment-vs-identifier pair above.
146 if self.hash_bitwise_xor && self.comment_syntax.line_comment_hash {
147 return Some(LexicalConflict::HashXorOperatorVersusHashComment);
148 }
149 // `#`: DuckDB's `#n` positional column reference and PostgreSQL's `#` bitwise-XOR
150 // operator both claim the `#` trigger. The positional scan arm is placed before
151 // the XOR arm, so `#`+digit lexes as a positional reference, silently shadowing
152 // the XOR reading of that byte. The fourth claimant of the `#` trigger.
153 if self.expression_syntax.positional_column && self.hash_bitwise_xor {
154 return Some(LexicalConflict::HashXorOperatorVersusPositionalColumn);
155 }
156 // `#`: DuckDB's `#n` positional column reference and a `#` line comment both claim
157 // the `#` trigger. The comment is consumed as trivia before the positional scan
158 // sees `#`, silently shadowing the `#n` reference, so a feature set enabling both
159 // must pick one meaning for `#`. (A `#`-led identifier byte class instead resolves
160 // by scan order — the identifier scan precedes the positional arm — so it does not
161 // contend here, mirroring the XOR-vs-identifier case.)
162 if self.expression_syntax.positional_column && self.comment_syntax.line_comment_hash {
163 return Some(LexicalConflict::HashCommentVersusPositionalColumn);
164 }
165 // `@name`: a named-at parameter and a user-variable read cannot both claim it
166 // (the scanner tries the user variable first, shadowing the parameter). The
167 // `@@` system-variable form is disjoint from both — it needs a second `@` —
168 // so it never enters this conflict.
169 if self.parameters.named_at && self.session_variables.user_variables {
170 return Some(LexicalConflict::AtNameParameterVersusUserVariable);
171 }
172 // `<@`: PostgreSQL's "contained by" operator and an abutting `@name` sigil both
173 // claim `<`+`@`. The scanner munches `<@` to the containment operator whenever
174 // containment is on, shadowing the abutting `a<@x` (meaning `a < @x`) that a
175 // `@name` parameter or user-variable read would otherwise lex. (`@>` is safe —
176 // its second byte `>` is not identifier-start — so only `<@` contends.)
177 if self.operator_syntax.containment_operators
178 && (self.parameters.named_at || self.session_variables.user_variables)
179 {
180 return Some(LexicalConflict::ContainmentOperatorVersusAtName);
181 }
182 // `?`: PostgreSQL's `jsonb` key-existence operator (also the lead byte of `?|`/`?&`)
183 // and the anonymous `?` placeholder both claim the byte. The anonymous-parameter
184 // dispatch arm precedes the operator arm, so `?` lexes as the placeholder whenever
185 // that is on, silently shadowing the operator. No shipped preset pairs them
186 // (PostgreSQL enables the operators and has no `?` parameter; the placeholder
187 // dialects leave the operators off).
188 if self.operator_syntax.jsonb_operators && self.parameters.anonymous_question {
189 return Some(LexicalConflict::JsonbKeyExistsVersusAnonymousParameter);
190 }
191 // `@@`: PostgreSQL's `jsonb`/text-search match operator and MySQL's `@@name`
192 // system-variable sigil both claim `@`+`@`. The system-variable dispatch arm
193 // precedes the operator arm, so `@@name` lexes as the variable whenever that is on,
194 // silently shadowing the operator. (`@?` is disjoint from every `@` claimant by its
195 // second byte, so it never contends.) No shipped preset pairs them.
196 if self.operator_syntax.jsonb_operators && self.session_variables.system_variables {
197 return Some(LexicalConflict::JsonbSearchOperatorVersusSystemVariable);
198 }
199 // `@`: the general bare-`@` operator (`custom_operators`) and an abutting `@name`
200 // sigil (`named_at`/`user_variables`) both claim `@`+identifier. The sigil dispatch
201 // arms precede the bare-`@` operator arm, so `@x` lexes as the sigil, shadowing the
202 // operator. (The `<@`/`@@` two-byte triggers are the containment/jsonb pairs above;
203 // this is the single `@`.) No shipped preset pairs them.
204 if self.operator_syntax.custom_operators
205 && (self.parameters.named_at || self.session_variables.user_variables)
206 {
207 return Some(LexicalConflict::CustomOperatorVersusAtName);
208 }
209 // `@@`: the general `@@` operator (`custom_operators`, when the `jsonb` family is off
210 // so `@@` is not already claimed by the pair above) and MySQL's `@@name` system
211 // variable both claim `@`+`@`. The system-variable arm precedes the operator arm, so
212 // `@@name` lexes as the variable, shadowing the operator. No shipped preset pairs them.
213 if self.operator_syntax.custom_operators
214 && !self.operator_syntax.jsonb_operators
215 && self.session_variables.system_variables
216 {
217 return Some(LexicalConflict::CustomOperatorVersusSystemVariable);
218 }
219 // Byte-class hygiene: a feature that leads with a sigil byte assumes that byte
220 // dispatches to its sigil scan, so a custom `ByteClasses` must not also mark the
221 // byte `CLASS_IDENTIFIER_START` — the same either/or the `#` check above enforces
222 // for a `#`-led comment. Every shipped preset uses a table that marks none of these
223 // bytes identifier-start (`STANDARD_BYTE_CLASSES`, or `POSTGRES_BYTE_CLASSES` which
224 // only adds the vertical tab to the whitespace class), so only a custom table trips
225 // these.
226 if (self.string_literals.dollar_quoted_strings
227 || self.parameters.positional_dollar
228 || self.numeric_literals.money_literals)
229 && self
230 .byte_classes
231 .has_class(b'$', lex_class::CLASS_IDENTIFIER_START)
232 {
233 return Some(LexicalConflict::DollarSigilVersusIdentifierByte);
234 }
235 if (self.parameters.named_at
236 || self.session_variables.user_variables
237 || self.session_variables.system_variables)
238 && self
239 .byte_classes
240 .has_class(b'@', lex_class::CLASS_IDENTIFIER_START)
241 {
242 return Some(LexicalConflict::AtSigilVersusIdentifierByte);
243 }
244 if self.parameters.named_colon
245 && self
246 .byte_classes
247 .has_class(b':', lex_class::CLASS_IDENTIFIER_START)
248 {
249 return Some(LexicalConflict::ColonSigilVersusIdentifierByte);
250 }
251 None
252 }
253
254 /// Whether this feature set has no [`lexical_conflict`](Self::lexical_conflict) —
255 /// every shared tokenizer trigger has exactly one claimant.
256 pub const fn is_lexically_consistent(&self) -> bool {
257 self.lexical_conflict().is_none()
258 }
259
260 /// The first grammar-dependency violation in this feature set — a refinement flag set
261 /// without the base flag it rides on — or `None` when every dependent flag has its
262 /// prerequisite.
263 ///
264 /// The grammar sibling of [`lexical_conflict`](Self::lexical_conflict). Several flags
265 /// only *refine* a production another flag opens: the extra slice bound needs the
266 /// bracket subscript, the `MERGE` extensions need `MERGE` dispatch, the `[FORCE]
267 /// CHECKPOINT <db>` operands need the bare `CHECKPOINT` statement, and so on. Each
268 /// documents the relationship in its field doc ("rides on" / "only reachable where" /
269 /// "inert without"); this predicate turns that prose into an explicit, testable
270 /// property.
271 ///
272 /// The severity is *inertness*, not unsoundness: a dependent flag whose base is off is
273 /// simply unreachable — the parser never reaches the grammar position, so the flag has
274 /// no effect and no wrong parse results. That is why this is a debug/test-time property
275 /// (every shipped preset and any custom [`FeatureDelta`] should return `None`) rather
276 /// than a runtime reject, and why [`try_with`](Self::try_with) — whose contract is the
277 /// *lexical*-soundness gate — deliberately does not fold it in.
278 ///
279 /// MECE with [`lexical_conflict`](Self::lexical_conflict): every variant here is a pure
280 /// grammar dependency with no tokenizer trigger of its own, and no shared-trigger
281 /// hazard appears here. A flag that both rides a base grammar flag *and* claims a
282 /// tokenizer trigger belongs in each registry for its respective axis.
283 ///
284 /// The predicate reads the *whole* [`FeatureSet`], so a dependent flag may name a base on
285 /// a different syntax axis (a cross-axis dependency) as freely as a same-axis one; the
286 /// machinery does not restrict a dependency to one sub-struct. What it *does* require is
287 /// the inertness contract above — a registrable dependent must be fully inert when its
288 /// base is off. That requirement, not the axis, is what excludes the one measured
289 /// near-miss:
290 ///
291 /// * [`SessionVariableSyntax::variable_assignment`] — a two-facet flag.
292 /// Its *parser* facet (the MySQL `SET` variable-assignment grammar) is unreachable
293 /// without [`ShowSyntax::session_statements`], which gates all `SET`/`RESET`/`SHOW`
294 /// dispatch — a genuine cross-axis grammar dependency. But its *lexer* facet (`:=`
295 /// munching to one `ColonEquals` operator token) fires independently of that gate, so
296 /// the flag is **not** inert when `session_statements` is off: it still changes
297 /// tokenization (measured — `:=` lexes to one token with the flag on vs two, `:` then
298 /// `=`, with it off). A dependent that observably changes tokenization while its base is
299 /// off violates the inertness contract *and* the "no tokenizer trigger of its own" MECE
300 /// line, so registering it as a variant would falsely certify it safe-to-dangle.
301 /// `variable_assignment` is therefore a documented exemption, not a variant: its field
302 /// doc names the independent lexer facet as the reason the flag is not fully inert, and
303 /// its cross-axis parser dependency is recorded there rather than here.
304 pub const fn feature_dependencies(&self) -> Option<FeatureDependencyViolation> {
305 use FeatureDependencyViolation as V;
306
307 // QueryTailSyntax: the row-locking refinements ride the base `locking_clauses` gate
308 // (they refine the strength keyword / repeat the shared clause).
309 if self.query_tail_syntax.key_lock_strengths && !self.query_tail_syntax.locking_clauses {
310 return Some(V::KeyLockStrengthsWithoutLockingClauses);
311 }
312 if self.query_tail_syntax.stacked_locking_clauses && !self.query_tail_syntax.locking_clauses
313 {
314 return Some(V::StackedLockingClausesWithoutLockingClauses);
315 }
316
317 // TableFactorSyntax: the `WITH OFFSET` tail rides an `UNNEST` table factor.
318 if self.table_factor_syntax.unnest_with_offset && !self.table_factor_syntax.unnest {
319 return Some(V::UnnestWithOffsetWithoutUnnest);
320 }
321
322 // ExpressionSyntax: the third slice bound rides the bracket `subscript`; the
323 // multidimensional bare-bracket sub-row rides the `ARRAY[…]` constructor.
324 if self.expression_syntax.slice_step && !self.expression_syntax.subscript {
325 return Some(V::SliceStepWithoutSubscript);
326 }
327 if self.expression_syntax.multidim_array_literals
328 && !self.expression_syntax.array_constructor
329 {
330 return Some(V::MultidimArrayLiteralsWithoutArrayConstructor);
331 }
332
333 // OperatorSyntax: the list-operand and arbitrary-operator quantifier forms ride the
334 // base `quantified_comparisons` reading of `ANY`/`ALL`/`SOME`; the DuckDB lambda is
335 // inert without the `->` lexeme `json_arrow_operators` munches.
336 if self.operator_syntax.quantified_comparison_lists
337 && !self.operator_syntax.quantified_comparisons
338 {
339 return Some(V::QuantifiedComparisonListsWithoutQuantifiedComparisons);
340 }
341 if self.operator_syntax.quantified_arbitrary_operator
342 && !self.operator_syntax.quantified_comparisons
343 {
344 return Some(V::QuantifiedArbitraryOperatorWithoutQuantifiedComparisons);
345 }
346 if self.operator_syntax.lambda_expressions && !self.operator_syntax.json_arrow_operators {
347 return Some(V::LambdaExpressionsWithoutJsonArrowOperators);
348 }
349
350 // MutationSyntax: the CTE-before-MERGE clause and the three MERGE-action extensions
351 // are only reachable where `merge` dispatches `MERGE` at all.
352 if self.mutation_syntax.cte_before_merge && !self.mutation_syntax.merge {
353 return Some(V::CteBeforeMergeWithoutMerge);
354 }
355 if self.mutation_syntax.merge_when_not_matched_by && !self.mutation_syntax.merge {
356 return Some(V::MergeWhenNotMatchedByWithoutMerge);
357 }
358 if self.mutation_syntax.merge_insert_default_values && !self.mutation_syntax.merge {
359 return Some(V::MergeInsertDefaultValuesWithoutMerge);
360 }
361 if self.mutation_syntax.merge_insert_overriding && !self.mutation_syntax.merge {
362 return Some(V::MergeInsertOverridingWithoutMerge);
363 }
364
365 // IndexAlterSyntax: the extended-`ALTER TABLE` actions and guards are only
366 // reachable through the `alter_table_extended` path.
367 if self.index_alter_syntax.alter_existence_guards
368 && !self.index_alter_syntax.alter_table_extended
369 {
370 return Some(V::AlterExistenceGuardsWithoutAlterTableExtended);
371 }
372 if self.index_alter_syntax.alter_column_set_data_type
373 && !self.index_alter_syntax.alter_table_extended
374 {
375 return Some(V::AlterColumnSetDataTypeWithoutAlterTableExtended);
376 }
377
378 // UtilitySyntax: each operand/guard refinement rides the base statement gate.
379 if self.maintenance_syntax.checkpoint_database && !self.maintenance_syntax.checkpoint {
380 return Some(V::CheckpointDatabaseWithoutCheckpoint);
381 }
382 if self.maintenance_syntax.analyze_columns && !self.maintenance_syntax.analyze {
383 return Some(V::AnalyzeColumnsWithoutAnalyze);
384 }
385 if self.utility_syntax.load_bare_name && !self.utility_syntax.load_extension {
386 return Some(V::LoadBareNameWithoutLoadExtension);
387 }
388 if self.utility_syntax.call_bare_name && !self.utility_syntax.call {
389 return Some(V::CallBareNameWithoutCall);
390 }
391 if self.utility_syntax.detach_if_exists && !self.utility_syntax.attach {
392 return Some(V::DetachIfExistsWithoutAttach);
393 }
394 if self.utility_syntax.use_qualified_name && !self.utility_syntax.use_statement {
395 return Some(V::UseQualifiedNameWithoutUseStatement);
396 }
397 if self.utility_syntax.use_string_literal_name && !self.utility_syntax.use_statement {
398 return Some(V::UseStringLiteralNameWithoutUseStatement);
399 }
400 if self.access_control_syntax.access_control_extended_objects
401 && !self.access_control_syntax.access_control
402 {
403 return Some(V::AccessControlExtendedObjectsWithoutAccessControl);
404 }
405 if self.access_control_syntax.access_control_account_grants
406 && !self.access_control_syntax.access_control
407 {
408 return Some(V::AccountGrantsWithoutAccessControl);
409 }
410 if self.utility_syntax.prepare_typed_parameters && !self.utility_syntax.prepared_statements
411 {
412 return Some(V::PrepareTypedParametersWithoutPreparedStatements);
413 }
414
415 None
416 }
417
418 /// Whether this feature set has no
419 /// [`feature_dependencies`](Self::feature_dependencies) violation — every dependent
420 /// grammar flag has the base flag it rides on.
421 pub const fn has_satisfied_feature_dependencies(&self) -> bool {
422 self.feature_dependencies().is_none()
423 }
424
425 /// This feature set with every *inert* refinement flag cleared — the dependency
426 /// registry's normal form, in which
427 /// [`has_satisfied_feature_dependencies`](Self::has_satisfied_feature_dependencies)
428 /// holds.
429 ///
430 /// Each [`feature_dependencies`](Self::feature_dependencies) violation is a refinement
431 /// flag enabled without the base it rides on; the registry's own contract is that such
432 /// a flag is *inert* (the parser never reaches its grammar position), so turning it off
433 /// cannot change any parse. This walks the registry, clearing one dangling dependent
434 /// per step until none remain, and is therefore an outcome-preserving projection onto
435 /// the parser's self-consistency precondition (the parse-entry `debug_assert!`) — the
436 /// tool a caller that assembles a [`FeatureDelta`] by toggling individual flags uses to
437 /// drop the inert leftovers before handing the set to the parser, rather than reasoning
438 /// out the dependency closure by hand.
439 ///
440 /// This is the dependency sibling of the lexical [`try_with`](Self::try_with): where
441 /// `try_with` reports the lexical verdict as a value, this repairs the (benign)
442 /// dependency one. It deliberately does *not* touch a
443 /// [`lexical_conflict`](Self::lexical_conflict) or a
444 /// [`grammar_conflict`](Self::grammar_conflict) — those are *soundness* hazards with no
445 /// inert-and-safe reading to normalize away, so a set carrying one has no defined parse
446 /// and this returns it unchanged.
447 pub fn without_dangling_dependents(&self) -> FeatureSet {
448 use FeatureDependencyViolation as V;
449
450 let mut features = self.clone();
451 // Each step clears exactly the dependent named by the first violation, so the
452 // violation count strictly decreases and the loop terminates.
453 while let Some(violation) = features.feature_dependencies() {
454 match violation {
455 V::KeyLockStrengthsWithoutLockingClauses => {
456 features.query_tail_syntax.key_lock_strengths = false;
457 }
458 V::StackedLockingClausesWithoutLockingClauses => {
459 features.query_tail_syntax.stacked_locking_clauses = false;
460 }
461 V::UnnestWithOffsetWithoutUnnest => {
462 features.table_factor_syntax.unnest_with_offset = false;
463 }
464 V::SliceStepWithoutSubscript => {
465 features.expression_syntax.slice_step = false;
466 }
467 V::MultidimArrayLiteralsWithoutArrayConstructor => {
468 features.expression_syntax.multidim_array_literals = false;
469 }
470 V::QuantifiedComparisonListsWithoutQuantifiedComparisons => {
471 features.operator_syntax.quantified_comparison_lists = false;
472 }
473 V::QuantifiedArbitraryOperatorWithoutQuantifiedComparisons => {
474 features.operator_syntax.quantified_arbitrary_operator = false;
475 }
476 V::LambdaExpressionsWithoutJsonArrowOperators => {
477 features.operator_syntax.lambda_expressions = false;
478 }
479 V::CteBeforeMergeWithoutMerge => {
480 features.mutation_syntax.cte_before_merge = false;
481 }
482 V::MergeWhenNotMatchedByWithoutMerge => {
483 features.mutation_syntax.merge_when_not_matched_by = false;
484 }
485 V::MergeInsertDefaultValuesWithoutMerge => {
486 features.mutation_syntax.merge_insert_default_values = false;
487 }
488 V::MergeInsertOverridingWithoutMerge => {
489 features.mutation_syntax.merge_insert_overriding = false;
490 }
491 V::AlterExistenceGuardsWithoutAlterTableExtended => {
492 features.index_alter_syntax.alter_existence_guards = false;
493 }
494 V::AlterColumnSetDataTypeWithoutAlterTableExtended => {
495 features.index_alter_syntax.alter_column_set_data_type = false;
496 }
497 V::CheckpointDatabaseWithoutCheckpoint => {
498 features.maintenance_syntax.checkpoint_database = false;
499 }
500 V::AnalyzeColumnsWithoutAnalyze => {
501 features.maintenance_syntax.analyze_columns = false;
502 }
503 V::LoadBareNameWithoutLoadExtension => {
504 features.utility_syntax.load_bare_name = false;
505 }
506 V::CallBareNameWithoutCall => {
507 features.utility_syntax.call_bare_name = false;
508 }
509 V::DetachIfExistsWithoutAttach => {
510 features.utility_syntax.detach_if_exists = false;
511 }
512 V::UseQualifiedNameWithoutUseStatement => {
513 features.utility_syntax.use_qualified_name = false;
514 }
515 V::UseStringLiteralNameWithoutUseStatement => {
516 features.utility_syntax.use_string_literal_name = false;
517 }
518 V::AccessControlExtendedObjectsWithoutAccessControl => {
519 features
520 .access_control_syntax
521 .access_control_extended_objects = false;
522 }
523 V::AccountGrantsWithoutAccessControl => {
524 features.access_control_syntax.access_control_account_grants = false;
525 }
526 V::PrepareTypedParametersWithoutPreparedStatements => {
527 features.utility_syntax.prepare_typed_parameters = false;
528 }
529 }
530 }
531 features
532 }
533
534 /// The first grammar-position mutual exclusion in this feature set — two features whose
535 /// grammars read the *same* token sequence at the *same* parser-position head with no
536 /// lookahead to tell them apart — or `None` when no such pair is enabled together.
537 ///
538 /// The third self-consistency sibling, after
539 /// [`lexical_conflict`](Self::lexical_conflict) (shared *tokenizer* triggers) and
540 /// [`feature_dependencies`](Self::feature_dependencies) (a refinement flag without its
541 /// base). This one covers the class the other two cannot: two features whose grammars
542 /// claim the same head, where the tokenizer is innocent (each byte lexes to one fixed
543 /// token) and neither flag rides the other. The parser resolves the head by a fixed
544 /// branch order, so enabling both silently shadows one reading — a *soundness* hazard
545 /// like a lexical conflict but at the grammar layer, which is exactly why neither sibling
546 /// registry can catch it (the [`prefix_colon_alias`](SelectSyntax::prefix_colon_alias)
547 /// field doc first named this gap).
548 ///
549 /// A collision is registrable here only when the contention has *no defined resolution*
550 /// and *no shipped preset enables both*. A preset that pairs two head-claimants with a
551 /// documented, deterministic resolution — DuckDB's `SUMMARIZE`-vs-MySQL-`DESCRIBE`
552 /// dispatch order under Lenient, or the `GROUP BY ALL` lookahead split — is a
553 /// conflict-*free* permissive union, not a mutual exclusion, and gets no variant. Like
554 /// the siblings this is a debug/test-time property — every shipped preset and any custom
555 /// [`FeatureDelta`] should return `None`.
556 ///
557 /// MECE with both siblings: every variant here is a pure parser-position contention —
558 /// no shared tokenizer trigger (that is a [`LexicalConflict`]) and no base-flag
559 /// dependency (that is a [`FeatureDependencyViolation`]).
560 pub const fn grammar_conflict(&self) -> Option<GrammarConflict> {
561 use GrammarConflict as G;
562
563 // `<ident> :` head: DuckDB's prefix colon alias (`SELECT j : 42` / `FROM b : a`, the
564 // alias written before its value) and semi-structured access (`base : key`, a postfix
565 // path) both read a bare identifier then `:` at a value / select-item head. The `:`
566 // always lexes as a lone `Colon` punctuation token (no tokenizer trigger), and
567 // neither flag rides the other, so the hazard is purely grammatical: the prefix-alias
568 // branch is tried first, binding `a : b` as an alias and silently shadowing the path
569 // reading. No shipped preset pairs them (DuckDB/Lenient enable the prefix alias with
570 // `semi_structured_access` off; Snowflake/Databricks enable the path with
571 // `prefix_colon_alias` off).
572 // Either projection or table-factor prefix-colon alias collides with semi-structured
573 // `base : key` at an `<ident> :` head.
574 if (self.select_syntax.prefix_colon_alias || self.table_expressions.prefix_colon_alias)
575 && self.expression_syntax.semi_structured_access
576 {
577 return Some(G::PrefixColonAliasVersusSemiStructuredAccess);
578 }
579
580 // Leading `DO` head: PostgreSQL's anonymous-code-block statement
581 // ([`do_statement`](UtilitySyntax::do_statement)) and MySQL's evaluate-and-discard
582 // expression list ([`do_expression_list`](UtilitySyntax::do_expression_list)) both
583 // dispatch on a bare leading `DO`. The `DO` byte lexes to one contextual keyword (no
584 // tokenizer trigger), and neither flag rides the other, so the hazard is purely
585 // grammatical: the code-block branch is tried first, so `DO 'x'` (MySQL intent) mis-parses
586 // as a PG block body and `DO 1, 2` over-rejects. No shipped preset pairs them (PostgreSQL
587 // and Lenient arm the block with the expression list off; MySQL the reverse).
588 if self.utility_syntax.do_statement && self.utility_syntax.do_expression_list {
589 return Some(G::DoStatementVersusDoExpressionList);
590 }
591
592 // Leading `PREPARE`/`EXECUTE`/`DEALLOCATE` head: DuckDB's typed-`AS` lifecycle
593 // ([`prepared_statements`](UtilitySyntax::prepared_statements)) and MySQL's
594 // `FROM`/`USING` lifecycle
595 // ([`prepared_statements_from`](UtilitySyntax::prepared_statements_from)) claim the same
596 // three leading keywords with different grammars. The dispatch resolves the two heads
597 // DuckDB-first, but the `DEALLOCATE` tail resolves MySQL-first (the `PREPARE` keyword is
598 // mandatory whenever `prepared_statements_from` is on), so the combination is incoherent
599 // across one lifecycle — see the parser sites in `query.rs`/`util.rs`. No shipped preset
600 // pairs them (DuckDB/PostgreSQL/Lenient arm the typed-`AS` form; MySQL the `FROM`/`USING`
601 // form). Registry-rejecting the combination is what lets the three keyword sites leave the
602 // both-on semantics undefined.
603 if self.utility_syntax.prepared_statements && self.utility_syntax.prepared_statements_from {
604 return Some(G::PreparedStatementsVersusPreparedStatementsFrom);
605 }
606
607 // `GRANT`/`REVOKE` head, a ROUTE-flag conflict (see the enum doc): the MySQL account route
608 // ([`access_control_account_grants`](AccessControlSyntax::access_control_account_grants))
609 // dispatches the whole account-based grammar before the standard/PostgreSQL extended-object
610 // grammar ([`access_control_extended_objects`](AccessControlSyntax::access_control_extended_objects))
611 // is consulted, so enabling both silently deadens the extended-object reading. The two are
612 // independent intents (a custom delta can set both), and the route's branch order picks the
613 // account grammar with no lookahead — a contradiction with no defined resolution. No shipped
614 // preset pairs them: MySQL arms the account route with extended objects off; ANSI/PostgreSQL/
615 // DuckDB/Lenient keep the extended-object grammar with the account route off.
616 if self.access_control_syntax.access_control_account_grants
617 && self.access_control_syntax.access_control_extended_objects
618 {
619 return Some(G::AccountGrantsVersusExtendedObjects);
620 }
621
622 None
623 }
624
625 /// Whether this feature set has no [`grammar_conflict`](Self::grammar_conflict) — no two
626 /// features contend for the same parser-position head.
627 pub const fn has_no_grammar_conflict(&self) -> bool {
628 self.grammar_conflict().is_none()
629 }
630
631 /// Apply `delta` to this base, returning the customized set only when it is
632 /// lexically consistent, or the first [`LexicalConflict`] it would introduce.
633 ///
634 /// The checked counterpart to [`with`](Self::with): a builder that wants the
635 /// conflict verdict as a *value* uses `try_with`, while [`with`](Self::with) stays
636 /// the documented-unchecked fast path the presets build on (its result feeds the
637 /// construction-time `debug_assert!` at the parse/tokenize entry seam). See
638 /// [`lexical_conflict`](Self::lexical_conflict) for what the shared triggers are.
639 ///
640 /// This is a *lexical* gate only — its contract is the tokenizer-soundness check, and
641 /// it does not verify the sibling [`feature_dependencies`](Self::feature_dependencies)
642 /// property (a dependent grammar flag without its base is inert, not unsound, so it
643 /// need not block a `try_with`). A builder wanting both verdicts checks
644 /// [`feature_dependencies`](Self::feature_dependencies) on the returned set.
645 pub const fn try_with(&self, delta: FeatureDelta) -> Result<Self, LexicalConflict> {
646 let candidate = self.with(delta);
647 match candidate.lexical_conflict() {
648 Some(conflict) => Err(conflict),
649 None => Ok(candidate),
650 }
651 }
652}
653
654/// Whether any style in `quotes` opens with `open` (a `const` membership test, so the
655/// [`FeatureSet::lexical_conflict`] checks fold at compile time).
656const fn identifier_quotes_open_with(quotes: &[IdentifierQuote], open: char) -> bool {
657 let mut index = 0;
658 while index < quotes.len() {
659 if quotes[index].open() == open {
660 return true;
661 }
662 index += 1;
663 }
664 false
665}
666
667/// A mutual exclusion between two features that both claim the *same* context-free
668/// tokenizer trigger — surfaced by [`FeatureSet::lexical_conflict`].
669///
670/// These are the dialect-data combinations the per-feature model cannot make
671/// independent: the tokenizer resolves each trigger byte to one token kind, so two
672/// claimants of one trigger are mutually exclusive even though each is its own feature
673/// field. The variants partition the contested triggers MECE — one per trigger, no
674/// overlap.
675///
676/// # Adding a tokenizer-trigger feature?
677///
678/// A feature that makes the scanner branch on a byte (a new sigil, quote, comment, or
679/// operator lead) must be audited against this registry before it lands — the
680/// "MECE and exhaustive" claim is hand-maintained, and both the `:`-slice and `<@`
681/// hazards postdated an earlier version of it:
682/// 1. Identify the lead byte(s) the feature's scan arm dispatches on.
683/// 2. For each, list every *other* feature whose scan arm can lead with the same byte
684/// (grep `scan.rs` for the byte and for its `features.` guards).
685/// 3. If two can be enabled at once and the scanner resolves the byte to one by fixed
686/// precedence, add a variant here and a per-variant detection test — never rely on
687/// "no real dialect pairs them", which is exactly how untracked hazards accrue.
688/// 4. If the byte instead stays disambiguated by lookahead (a second fixed byte or a
689/// required follow-set), record *that* in the [`lexical_conflict`] doc's
690/// lookahead-disjoint list rather than minting a variant.
691///
692/// Also confirm the lead byte is not marked [`CLASS_IDENTIFIER_START`] by any preset's
693/// [`ByteClasses`] (the sigil-vs-identifier-byte variants below cover a *custom* table).
694///
695/// [`lexical_conflict`]: FeatureSet::lexical_conflict
696/// [`CLASS_IDENTIFIER_START`]: crate::dialect::lex_class::CLASS_IDENTIFIER_START
697#[derive(Clone, Copy, Debug, PartialEq, Eq)]
698#[non_exhaustive]
699pub enum LexicalConflict {
700 /// `"` is claimed by both [`StringLiteralSyntax::double_quoted_strings`] (→ a
701 /// string constant) and an [`identifier_quotes`](FeatureSet::identifier_quotes)
702 /// style opening with `"` (→ a quoted identifier). The tokenizer makes `"…"` a
703 /// string whenever `double_quoted_strings` is on, silently shadowing the quoted
704 /// identifier, so a feature set must pick one meaning for `"`.
705 DoubleQuoteStringVersusIdentifier,
706 /// `[` is claimed by both an [`identifier_quotes`](FeatureSet::identifier_quotes)
707 /// style opening with `[` (T-SQL bracket identifiers, → a quoted identifier) and
708 /// the `[`-punctuation expression grammar that
709 /// [`ExpressionSyntax::subscript`]/[`array_constructor`](ExpressionSyntax::array_constructor)/[`collection_literals`](ExpressionSyntax::collection_literals)
710 /// need, or the table-position PartiQL / SUPER path root that
711 /// [`TableExpressionSyntax::table_json_path`](TableExpressionSyntax::table_json_path)
712 /// enters on a `[` after a table name (`FROM src[0].a`). The tokenizer claims `[` for the
713 /// identifier before the parser sees `[` punctuation, so those forms (and any other
714 /// `[`-led syntax, such as array-type suffixes or the DuckDB bare-bracket list literal)
715 /// can never fire — pick bracket identifiers *or* `[` expression / table-path syntax.
716 BracketIdentifierVersusArraySyntax,
717 /// `$`+digit is claimed by both [`NumericLiteralSyntax::money_literals`] (T-SQL
718 /// `$1234.56`, → a number) and [`ParameterSyntax::positional_dollar`] (PostgreSQL
719 /// `$1`, → a parameter). The scanner tries money first, silently shadowing the
720 /// positional parameter, so a feature set must pick one meaning for `$`+digit.
721 MoneyVersusPositionalDollar,
722 /// `@name` is claimed by both [`ParameterSyntax::named_at`] (T-SQL, → a named
723 /// parameter placeholder) and [`SessionVariableSyntax::user_variables`] (MySQL, → a
724 /// user-variable read). The two are the same surface with different meaning, so a
725 /// feature set must pick one for `@name`; the scanner tries the user variable first,
726 /// silently shadowing the parameter. The `@@` system-variable form is disjoint from
727 /// both (its second `@` is no identifier byte), so it is not part of this conflict.
728 AtNameParameterVersusUserVariable,
729 /// `$`+identifier-start is claimed by both [`ParameterSyntax::named_dollar`]
730 /// (SQLite `$name`, → a parameter) and [`StringLiteralSyntax::dollar_quoted_strings`]
731 /// (PostgreSQL `$tag$…$tag$`, → a string constant): a dollar-quote tag byte is the
732 /// same class as an identifier-start, so both scan arms lead with `$` then that
733 /// byte. The scanner resolves it by fixed precedence — one shadows the other — so a
734 /// feature set must pick one meaning for `$`+identifier. (`$`+digit stays the
735 /// separate money-vs-positional trigger, disjoint by its follow byte.)
736 NamedDollarParameterVersusDollarQuotedString,
737 /// `#` is claimed by both [`CommentSyntax::line_comment_hash`] (→ a line comment)
738 /// and a [`byte_classes`](FeatureSet::byte_classes) table marking `#` an
739 /// identifier-start byte (→ a `#name` word). The comment branch wins, so `#` must
740 /// stay out of the identifier-start class wherever it opens a comment.
741 HashCommentVersusHashIdentifier,
742 /// `#` is claimed by both [`FeatureSet::hash_bitwise_xor`] (PostgreSQL, → the bitwise-XOR
743 /// operator) and [`CommentSyntax::line_comment_hash`] (MySQL, → a line comment). The
744 /// comment is consumed as trivia before the operator scanner sees `#`, silently shadowing
745 /// the XOR operator, so a feature set enabling both must pick one meaning for `#`. (A
746 /// `#`-led identifier byte class instead resolves by scan order — the identifier scan
747 /// precedes the XOR arm — so it does not contend here.)
748 HashXorOperatorVersusHashComment,
749 /// `#`+digit is claimed by both [`ExpressionSyntax::positional_column`] (DuckDB `#n`,
750 /// → a positional column reference) and [`FeatureSet::hash_bitwise_xor`] (PostgreSQL, →
751 /// the bitwise-XOR operator, which claims every `#`). The positional scan arm precedes the
752 /// XOR arm, so `#`+digit lexes as the positional reference, silently shadowing the XOR
753 /// reading of that byte — a feature set enabling both must pick one meaning for `#`.
754 /// (No shipped preset pairs them: DuckDB has the positional form and spells nothing
755 /// with `#`-XOR; PostgreSQL has `#`-XOR and no positional form.)
756 HashXorOperatorVersusPositionalColumn,
757 /// `#`+digit is claimed by both [`ExpressionSyntax::positional_column`] (DuckDB `#n`,
758 /// → a positional column reference) and [`CommentSyntax::line_comment_hash`] (MySQL, →
759 /// a line comment, which claims every `#`). The comment is consumed as trivia before
760 /// the positional scan sees `#`, silently shadowing the `#n` reference, so a feature
761 /// set enabling both must pick one meaning for `#`. (This is why Lenient, which keeps
762 /// `#` a line comment, cannot also enable the positional form. A `#`-led identifier
763 /// byte class instead resolves by scan order — the identifier scan precedes the
764 /// positional arm — so it does not contend here, like the XOR case.)
765 HashCommentVersusPositionalColumn,
766 /// `:`+identifier-start is claimed by [`ParameterSyntax::named_colon`]
767 /// (Oracle/SQLite `:name`, → a parameter) and by grammars that write a `:`
768 /// before a bare-identifier operand: array slicing under
769 /// [`ExpressionSyntax::subscript`] (`a[x:y]`, → the slice `:` separator then the
770 /// bound `y`) and the DuckDB collection literals under
771 /// [`ExpressionSyntax::collection_literals`] (`{a: b}` / `MAP {k: v}`, → the
772 /// `key: value` separator then the value), plus semi-structured access under
773 /// [`ExpressionSyntax::semi_structured_access`] (`a:b`, → a path key). The `:name`
774 /// scanner binds `:`+identifier as one parameter token, so it swallows `:y` / `:b`
775 /// and the operand is lost — pick colon-named parameters *or* the `:`-separated
776 /// grammars.
777 /// (`::` stays the typecast and a lone `:` before a non-identifier byte stays the
778 /// separator regardless — only a `:` abutting an identifier contends.)
779 ColonParameterVersusSliceBound,
780 /// `<@` is claimed by both [`OperatorSyntax::containment_operators`] (PostgreSQL
781 /// "contained by", → the `LtAt` operator) and an abutting `@name` sigil —
782 /// [`ParameterSyntax::named_at`] or [`SessionVariableSyntax::user_variables`] (→ a
783 /// `<` operator then an `@name` parameter / user-variable read). The scanner munches
784 /// `<`+`@` to the containment operator whenever containment is on, silently shadowing
785 /// the MySQL-legal abutting `a<@x` (meaning `a < @x`), so a feature set enabling both
786 /// must pick one meaning for `<@`. (`@>` is safe — its second byte `>` is not
787 /// identifier-start — so only `<@` contends.)
788 ContainmentOperatorVersusAtName,
789 /// `?` is claimed by both [`OperatorSyntax::jsonb_operators`] (PostgreSQL `jsonb`
790 /// key-existence operator, and the lead byte of `?|`/`?&`) and
791 /// [`ParameterSyntax::anonymous_question`] (ODBC/JDBC anonymous placeholder). The
792 /// placeholder scan arm precedes the operator arm, so `?` lexes as the placeholder
793 /// whenever it is on, silently shadowing the operator — a feature set enabling both must
794 /// pick one meaning for `?`. (No shipped preset pairs them: PostgreSQL has the operators
795 /// and no `?` parameter; the placeholder dialects leave the operators off.)
796 JsonbKeyExistsVersusAnonymousParameter,
797 /// `@@` is claimed by both [`OperatorSyntax::jsonb_operators`] (PostgreSQL's
798 /// `jsonb`/text-search match operator) and [`SessionVariableSyntax::system_variables`]
799 /// (MySQL `@@name`). The system-variable scan arm precedes the operator arm, so `@@name`
800 /// lexes as the variable whenever it is on, silently shadowing the operator — a feature
801 /// set enabling both must pick one meaning for `@@`. (`@?` is disjoint from every `@`
802 /// claimant by its second byte, so only `@@` contends. No shipped preset pairs them.)
803 JsonbSearchOperatorVersusSystemVariable,
804 /// A bare `@` is claimed by both [`OperatorSyntax::custom_operators`] (the general
805 /// symbolic operator, e.g. the prefix absolute-value `@ x`) and an abutting `@name`
806 /// sigil — [`ParameterSyntax::named_at`] or [`SessionVariableSyntax::user_variables`]
807 /// (→ an `@name` parameter / user-variable read). The sigil dispatch arms precede the
808 /// bare-`@` operator arm, so `@x` lexes as the sigil whenever one is on, silently
809 /// shadowing the operator — a feature set enabling both must pick one meaning for a
810 /// `@`-then-identifier. (A `@ ` not abutting an identifier stays the operator, since the
811 /// sigil arms require an identifier-start follow byte. No shipped preset pairs them:
812 /// PostgreSQL enables the operators and has no `@name` sigil.) Distinct from
813 /// [`ContainmentOperatorVersusAtName`](Self::ContainmentOperatorVersusAtName), which is
814 /// the `<@` two-byte trigger, not the bare `@`.
815 CustomOperatorVersusAtName,
816 /// `@@` is claimed by both [`OperatorSyntax::custom_operators`] (the general `@@`
817 /// operator — e.g. the prefix box-centre `@@ box` — when the `jsonb` family is off) and
818 /// [`SessionVariableSyntax::system_variables`] (MySQL `@@name`). The system-variable scan
819 /// arm precedes the operator arm, so `@@name` lexes as the variable whenever it is on,
820 /// silently shadowing the operator — a feature set enabling both must pick one meaning for
821 /// `@@`. (When the `jsonb` family is also on, the
822 /// [`JsonbSearchOperatorVersusSystemVariable`](Self::JsonbSearchOperatorVersusSystemVariable)
823 /// pair — checked first — owns the `@@` trigger instead. No shipped preset pairs these.)
824 CustomOperatorVersusSystemVariable,
825 /// `$` is claimed as a leading sigil by [`NumericLiteralSyntax::money_literals`],
826 /// [`ParameterSyntax::positional_dollar`], or
827 /// [`StringLiteralSyntax::dollar_quoted_strings`], yet a
828 /// [`byte_classes`](FeatureSet::byte_classes) table also marks `$` an
829 /// identifier-start byte. Two claimants for `$` is the same either/or as
830 /// [`HashCommentVersusHashIdentifier`](Self::HashCommentVersusHashIdentifier): keep `$` out of the identifier-start class
831 /// wherever a `$`-led feature is on.
832 DollarSigilVersusIdentifierByte,
833 /// `@` is claimed as a leading sigil by the `@`-family features
834 /// ([`ParameterSyntax::named_at`], [`SessionVariableSyntax::user_variables`], or
835 /// [`SessionVariableSyntax::system_variables`]), yet a
836 /// [`byte_classes`](FeatureSet::byte_classes) table also marks `@` an
837 /// identifier-start byte. Same either/or as [`HashCommentVersusHashIdentifier`](Self::HashCommentVersusHashIdentifier):
838 /// keep `@` out of the identifier-start class wherever an `@`-family feature is on.
839 AtSigilVersusIdentifierByte,
840 /// `:` is claimed as a leading sigil by [`ParameterSyntax::named_colon`], yet a
841 /// [`byte_classes`](FeatureSet::byte_classes) table also marks `:` an
842 /// identifier-start byte — so a lone `:` would begin an identifier instead of the
843 /// `:name` parameter or the slice separator. Same either/or as
844 /// [`HashCommentVersusHashIdentifier`](Self::HashCommentVersusHashIdentifier): keep `:` out of the identifier-start class
845 /// while `named_colon` is on.
846 ColonSigilVersusIdentifierByte,
847}
848
849/// A grammar-flag dependency left unsatisfied — a refinement feature enabled without the
850/// base feature it rides on — surfaced by [`FeatureSet::feature_dependencies`].
851///
852/// The grammar sibling of [`LexicalConflict`], and MECE-disjoint from it: every variant
853/// here is a pure grammar dependency (a flag inert without its base), never a shared
854/// tokenizer trigger. Unlike a lexical conflict, an unsatisfied dependency is not a
855/// soundness bug — the dependent flag is simply unreachable — so the registry is a
856/// test/debug-time property, not a runtime reject.
857///
858/// Each variant is named `<Dependent>Without<Base>` and its doc names both the dependent
859/// flag and the base flag it requires.
860///
861/// # Adding a dependent flag?
862///
863/// A flag that only *refines* a production another flag opens (a further clause, operand,
864/// guard, or bound reachable only once the base grammar is admitted) must be registered
865/// here before it lands — the "every preset is clean" claim is hand-maintained:
866/// 1. Identify the base flag whose grammar position the new flag extends (the field doc's
867/// "rides on" / "only reachable where" / "inert without" sentence names it).
868/// 2. Add a `<Dependent>Without<Base>` variant, a guard in
869/// [`feature_dependencies`](FeatureSet::feature_dependencies), and a per-variant
870/// detection test.
871/// 3. Point the field's prose sentence at the new variant so the doc and the registry stay
872/// in lock-step.
873///
874/// If the flag instead claims its *own* tokenizer trigger, that hazard is a
875/// [`LexicalConflict`], not a dependency — keep the two registries MECE.
876#[derive(Clone, Copy, Debug, PartialEq, Eq)]
877#[non_exhaustive]
878pub enum FeatureDependencyViolation {
879 /// [`QueryTailSyntax::key_lock_strengths`] (the `FOR NO KEY UPDATE` / `FOR KEY SHARE`
880 /// strengths) refines the strength keyword after `FOR`, so it requires
881 /// [`QueryTailSyntax::locking_clauses`]. Without the base gate the `FOR` clause is never
882 /// read, so the strength refinement is unreachable.
883 KeyLockStrengthsWithoutLockingClauses,
884 /// [`QueryTailSyntax::stacked_locking_clauses`] (multiple `FOR UPDATE`/`FOR SHARE` clauses
885 /// on one query) repeats the shared locking clause, so it requires
886 /// [`QueryTailSyntax::locking_clauses`]. Without the base gate no locking clause is read,
887 /// so there is nothing to stack.
888 StackedLockingClausesWithoutLockingClauses,
889 /// [`TableFactorSyntax::unnest_with_offset`] (the BigQuery `WITH OFFSET` tail) sits
890 /// on an `UNNEST` table factor, so it requires [`TableFactorSyntax::unnest`].
891 /// Without the base gate `UNNEST(` is left to the named-table path and the tail is
892 /// never reached.
893 UnnestWithOffsetWithoutUnnest,
894 /// [`ExpressionSyntax::slice_step`] (the third `base[lower:upper:step]` bound) is
895 /// reachable only once the bracket subscript has opened, so it requires
896 /// [`ExpressionSyntax::subscript`]. Without the base gate `[` is never read as a
897 /// subscript and the extra bound is unreachable.
898 SliceStepWithoutSubscript,
899 /// [`ExpressionSyntax::multidim_array_literals`] (the bare-bracket sub-row) is only a
900 /// value inside an `ARRAY[…]` constructor, so it requires
901 /// [`ExpressionSyntax::array_constructor`]. Without the base gate there is no
902 /// array-constructor element position for the sub-row to occupy.
903 MultidimArrayLiteralsWithoutArrayConstructor,
904 /// [`OperatorSyntax::quantified_comparison_lists`] (the scalar list/array operand of a
905 /// quantified comparison) rides the base quantifier reading, so it requires
906 /// [`OperatorSyntax::quantified_comparisons`]. Without the base gate `ANY`/`ALL`/`SOME`
907 /// is not read as a quantifier and the list operand is unreachable.
908 QuantifiedComparisonListsWithoutQuantifiedComparisons,
909 /// [`OperatorSyntax::quantified_arbitrary_operator`] (extending the quantifier past the
910 /// comparison operators) rides the base quantifier reading, so it requires
911 /// [`OperatorSyntax::quantified_comparisons`]. Without the base gate the quantifier is
912 /// unread and the arbitrary-operator extension is unreachable.
913 QuantifiedArbitraryOperatorWithoutQuantifiedComparisons,
914 /// [`OperatorSyntax::lambda_expressions`] (the DuckDB `x -> body` lambda) is a
915 /// grammar-position reading of the `->` lexeme, which is munched only under
916 /// [`OperatorSyntax::json_arrow_operators`], so it is inert without it. Without the base
917 /// gate no `->` token is produced and the lambda reading never fires.
918 LambdaExpressionsWithoutJsonArrowOperators,
919 /// [`MutationSyntax::cte_before_merge`] (a leading `WITH` before `MERGE`) is only
920 /// reachable where [`MutationSyntax::merge`] dispatches `MERGE`. Without the base gate
921 /// the `MERGE` after the CTE list is never dispatched.
922 CteBeforeMergeWithoutMerge,
923 /// [`MutationSyntax::merge_when_not_matched_by`] (the `WHEN NOT MATCHED BY
924 /// SOURCE | TARGET` arms) is only reachable where [`MutationSyntax::merge`] dispatches
925 /// `MERGE`. Without the base gate the `MERGE` statement is never parsed.
926 MergeWhenNotMatchedByWithoutMerge,
927 /// [`MutationSyntax::merge_insert_default_values`] (the `INSERT DEFAULT VALUES` merge
928 /// action) is only reachable where [`MutationSyntax::merge`] dispatches `MERGE`. Without
929 /// the base gate the `MERGE` statement is never parsed.
930 MergeInsertDefaultValuesWithoutMerge,
931 /// [`MutationSyntax::merge_insert_overriding`] (the `OVERRIDING {SYSTEM | USER} VALUE`
932 /// merge-insert override) is only reachable where [`MutationSyntax::merge`] dispatches
933 /// `MERGE`. Without the base gate the `MERGE` statement is never parsed.
934 MergeInsertOverridingWithoutMerge,
935 /// [`IndexAlterSyntax::alter_existence_guards`] (the `IF [NOT] EXISTS` guards inside
936 /// `ALTER TABLE`) is parsed only on the extended `ALTER TABLE` path, so it requires
937 /// [`IndexAlterSyntax::alter_table_extended`]. Without the base gate the non-extended
938 /// path parses no guard.
939 AlterExistenceGuardsWithoutAlterTableExtended,
940 /// [`IndexAlterSyntax::alter_column_set_data_type`] (the `ALTER COLUMN SET DATA TYPE`
941 /// / `SET`/`DROP NOT NULL` actions) is reached only on the extended `ALTER TABLE` path,
942 /// so it requires [`IndexAlterSyntax::alter_table_extended`]. Without the base gate the
943 /// `ALTER COLUMN` action is never reached.
944 AlterColumnSetDataTypeWithoutAlterTableExtended,
945 /// [`MaintenanceSyntax::checkpoint_database`] (the DuckDB `[FORCE] CHECKPOINT <database>`
946 /// operands) rides the base `CHECKPOINT` statement, so it requires
947 /// [`MaintenanceSyntax::checkpoint`]. Without the base gate `CHECKPOINT` is not dispatched
948 /// and the operands are unreachable.
949 CheckpointDatabaseWithoutCheckpoint,
950 /// [`MaintenanceSyntax::analyze_columns`] (the DuckDB `ANALYZE <table> (<cols>)` column
951 /// list) rides the base `ANALYZE` statement, so it requires
952 /// [`MaintenanceSyntax::analyze`]. Without the base gate `ANALYZE` is not dispatched and
953 /// the column list is unreachable.
954 AnalyzeColumnsWithoutAnalyze,
955 /// [`UtilitySyntax::load_bare_name`] (the DuckDB bare-identifier `LOAD <name>` argument)
956 /// rides the base `LOAD` statement, so it requires [`UtilitySyntax::load_extension`].
957 /// Without the base gate `LOAD` is not dispatched and the bare-name argument is
958 /// unreachable.
959 LoadBareNameWithoutLoadExtension,
960 /// [`UtilitySyntax::call_bare_name`] (MySQL's bare `CALL <name>` form, no parenthesized
961 /// argument list) rides the base `CALL` statement, so it requires
962 /// [`UtilitySyntax::call`]. Without the base gate `CALL` is not dispatched and the
963 /// bare-name form is unreachable.
964 CallBareNameWithoutCall,
965 /// [`UtilitySyntax::detach_if_exists`] (the `DETACH DATABASE IF EXISTS` guard) rides the
966 /// base `ATTACH`/`DETACH` statement, so it requires [`UtilitySyntax::attach`]. Without
967 /// the base gate `DETACH` is not dispatched and the guard is unreachable.
968 DetachIfExistsWithoutAttach,
969 /// [`UtilitySyntax::use_qualified_name`] (DuckDB's dotted `USE <catalog> . <schema>`
970 /// name, widening the accepted `USE` name arity from one to two parts) refines the name
971 /// grammar of the base `USE` statement, so it requires [`UtilitySyntax::use_statement`].
972 /// Without the base gate the leading `USE` is not dispatched, so the parser never reaches
973 /// the arity check the flag widens and the flag is inert.
974 UseQualifiedNameWithoutUseStatement,
975 /// [`UtilitySyntax::use_string_literal_name`] (DuckDB's `USE 'n'` / `E'n'` / `$$n$$`
976 /// single-part string-literal target) refines the name grammar of the base `USE`
977 /// statement, so it requires [`UtilitySyntax::use_statement`]. Without the base gate the
978 /// leading `USE` is not dispatched and the flag is inert.
979 UseStringLiteralNameWithoutUseStatement,
980 /// [`AccessControlSyntax::access_control_extended_objects`] (the extended `GRANT`/`REVOKE`
981 /// object and prefix grammar) builds on the base `GRANT`/`REVOKE` statements, so it
982 /// requires [`AccessControlSyntax::access_control`]. Without the base gate the access-control
983 /// statements are not dispatched and the extended forms are unreachable.
984 AccessControlExtendedObjectsWithoutAccessControl,
985 /// [`AccessControlSyntax::access_control_account_grants`] (the MySQL account-based
986 /// `GRANT`/`REVOKE` grammar) is a route of the base `GRANT`/`REVOKE` statements, so it
987 /// requires [`AccessControlSyntax::access_control`]. Without the base gate the access-control
988 /// statements are not dispatched and the account-based grammar is unreachable.
989 AccountGrantsWithoutAccessControl,
990 /// [`UtilitySyntax::prepare_typed_parameters`] (the PostgreSQL `PREPARE
991 /// name(<type>, …)` parenthesized parameter-type list) widens the name position of the
992 /// base `PREPARE` grammar, so it requires [`UtilitySyntax::prepared_statements`].
993 /// Without the base gate `PREPARE` is not dispatched and the type list is unreachable.
994 PrepareTypedParametersWithoutPreparedStatements,
995}
996
997/// A grammar-position mutual exclusion — two features that both read the *same*
998/// parser-position head, which the parser resolves by a fixed branch order so enabling both
999/// silently shadows one reading — surfaced by [`FeatureSet::grammar_conflict`].
1000///
1001/// The third self-consistency registry, MECE-disjoint from its two siblings: unlike a
1002/// [`LexicalConflict`] the contended surface is not a *tokenizer* trigger (each byte lexes to
1003/// one fixed token; the contention is which *grammar* claims the token sequence), and unlike
1004/// a [`FeatureDependencyViolation`] neither flag rides the other (both are independent
1005/// grammar positions). Like a lexical conflict the severity is *unsoundness* — the shadowed
1006/// reading is silently mis-parsed — but the shadow falls in the parser, not the tokenizer,
1007/// so no lexical precedence governs it.
1008///
1009/// Each variant is named `<A>Versus<B>` and its doc names both features and the shared head
1010/// they contend for.
1011///
1012/// # Two modelling species
1013///
1014/// The registered pairs fall into two shapes, both registrable under the same criteria:
1015///
1016/// - **Surface-overlap pairs** — two features that each *add* a reading at a shared head, so
1017/// enabling both leaves the head over-claimed. The `<ident> :` alias-vs-path pair and the two
1018/// `DO`-keyword readings ([`DoStatementVersusDoExpressionList`](Self::DoStatementVersusDoExpressionList))
1019/// are of this kind: each flag contributes one grammar to a position the other also reads.
1020/// - **Route flags** — a flag that selects *one of two mutually-exclusive whole grammars* for a
1021/// head rather than adding surface. When the route flag is on it dispatches its grammar
1022/// *before* the rival grammar is consulted, so the rival — even when explicitly enabled —
1023/// is silently deadened. [`AccountGrantsVersusExtendedObjects`](Self::AccountGrantsVersusExtendedObjects)
1024/// is the exemplar: [`access_control_account_grants`](AccessControlSyntax::access_control_account_grants)
1025/// routes `GRANT`/`REVOKE` to the MySQL account grammar, bypassing the
1026/// [`access_control_extended_objects`](AccessControlSyntax::access_control_extended_objects)
1027/// reading. A route flag is registrable **only when the grammar it displaces is itself a
1028/// feature flag** — that is what makes the both-on state independently expressible and its
1029/// resolution undefined-in-intent.
1030///
1031/// Not every route flag qualifies. MySQL's
1032/// [`variable_assignment`](SessionVariableSyntax::variable_assignment) is also a route flag — it
1033/// dispatches `SET` to the MySQL variable-assignment grammar before the standard `SET TIME ZONE`
1034/// / `SET SESSION AUTHORIZATION` forms are read — but the grammar it displaces is *unconditional
1035/// base grammar with no rival flag*. There is therefore no second flag to express "I also want
1036/// the standard `SET` forms", so no both-on contradiction exists, and MySQL (a shipped preset)
1037/// already exercises the route deterministically. Per the registrability criterion below, a
1038/// deterministic resolution a shipped preset relies on is a conflict-*free* union, not a mutual
1039/// exclusion (the same reasoning that leaves the `DESCRIBE`/`SUMMARIZE` leader unregistered), so
1040/// `variable_assignment` gets **no variant**. Registering it would require either promoting the
1041/// standard `SET` config grammar to its own flag or converting `variable_assignment` to an enum
1042/// axis (the `PipeOperator`/`DoubleAmpersand` either-by-type precedent) — an architectural
1043/// modelling change, not a registry addition.
1044///
1045/// # Registry coverage
1046///
1047/// The four entries cover the `<ident> :` lexical pair and three parser-position mutual
1048/// exclusions — the `DO`, prepared-statement, and `GRANT` heads. The grammar-level gates are
1049/// therefore *not* all pairwise-independent: a preset that unions two of these contenders
1050/// needs a registered resolution, which is why this registry exists rather than relying on
1051/// the gates being independent.
1052///
1053/// # Adding a grammar-position feature?
1054///
1055/// A feature whose grammar reads a token sequence at a position another feature's grammar
1056/// also reads must be audited against this registry before it lands — the "every preset is
1057/// clean" claim is hand-maintained:
1058/// 1. Identify the parser-position head the new grammar dispatches on (the field doc's "reads
1059/// the same … head" / "must not be enabled together" sentence names the rival).
1060/// 2. Confirm the contention is *undefined* — the two branches share no lookahead or fixed
1061/// dispatch order that keeps both readable — and that *no shipped preset enables both*. A
1062/// pair a preset unions with a documented deterministic resolution (the
1063/// `DESCRIBE`/`SUMMARIZE` dispatch, the `GROUP BY ALL` lookahead split) is a
1064/// conflict-*free* union, not a mutual exclusion, and gets no variant.
1065/// 3. Add an `<A>Versus<B>` variant, a guard in
1066/// [`grammar_conflict`](FeatureSet::grammar_conflict), and a per-variant detection test.
1067/// 4. Point each field's prose sentence at the new variant so the docs and the registry stay
1068/// in lock-step.
1069///
1070/// If the contended surface is instead a shared *tokenizer* trigger, that hazard is a
1071/// [`LexicalConflict`]; if one flag merely rides the other's base grammar, it is a
1072/// [`FeatureDependencyViolation`] — keep the three registries MECE.
1073#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1074#[non_exhaustive]
1075pub enum GrammarConflict {
1076 /// The `<ident> :` head is claimed by DuckDB's prefix colon alias — either
1077 /// [`SelectSyntax::prefix_colon_alias`] (projection `SELECT j : 42`) or
1078 /// [`TableExpressionSyntax::prefix_colon_alias`] (table-factor `FROM b : a`) — and by
1079 /// [`ExpressionSyntax::semi_structured_access`] (the `base : key` postfix path). The `:`
1080 /// always lexes as a lone `Colon` punctuation token — no [`LexicalConflict`] governs it —
1081 /// so the contention is purely grammatical: at a value / select-item head the prefix-alias
1082 /// branch is tried first, binding `a : b` as an alias and silently shadowing the path
1083 /// reading. Enabling either alias position with semi-structured access is a conflict.
1084 /// No shipped preset pairs them (DuckDB/Lenient enable both alias positions with
1085 /// semi-structured access off; Snowflake/Databricks enable the path with both alias
1086 /// flags off).
1087 PrefixColonAliasVersusSemiStructuredAccess,
1088 /// The leading `DO` head is claimed by both [`UtilitySyntax::do_statement`] (PostgreSQL's
1089 /// `DO [LANGUAGE <lang>] '<body>'` anonymous code block) and
1090 /// [`UtilitySyntax::do_expression_list`] (MySQL's `DO <expr> [, <expr> …]`
1091 /// evaluate-and-discard statement). The `DO` byte lexes to one contextual keyword — no
1092 /// [`LexicalConflict`] governs it — so the contention is purely grammatical: the code-block
1093 /// branch is tried first, so under both-on `DO 'x'` (MySQL intent) mis-parses as a PostgreSQL
1094 /// block body and `DO 1, 2` over-rejects, the reading fixed only by dispatch order. No shipped
1095 /// preset pairs them (PostgreSQL and Lenient arm [`do_statement`](UtilitySyntax::do_statement)
1096 /// with [`do_expression_list`](UtilitySyntax::do_expression_list) off; MySQL the reverse).
1097 DoStatementVersusDoExpressionList,
1098 /// The leading `PREPARE`/`EXECUTE`/`DEALLOCATE` head is claimed by both
1099 /// [`UtilitySyntax::prepared_statements`] (DuckDB's typed-`AS` prepared-statement lifecycle)
1100 /// and [`UtilitySyntax::prepared_statements_from`] (MySQL's `FROM`/`USING` lifecycle) —
1101 /// different grammars on the same three leading keywords, each byte lexing to one contextual
1102 /// keyword (no [`LexicalConflict`]). The statement dispatch resolves the `PREPARE`/`EXECUTE`
1103 /// heads DuckDB-first by branch order, but the `DEALLOCATE` tail resolves MySQL-first (the
1104 /// `PREPARE` keyword becomes mandatory whenever `prepared_statements_from` is on), so the
1105 /// combination is *incoherent across one lifecycle*: `DEALLOCATE p` (valid under
1106 /// `prepared_statements` alone) errors while `PREPARE p AS …` keeps the DuckDB reading. No
1107 /// shipped preset pairs them (DuckDB/PostgreSQL/Lenient arm the typed-`AS` form; MySQL the
1108 /// `FROM`/`USING` form). Because the combination is registry-rejected, the three keyword sites
1109 /// (`query.rs` dispatch, `util.rs` `finish_deallocate_statement`) leave the both-on semantics
1110 /// deliberately undefined rather than reconciling the two winners.
1111 PreparedStatementsVersusPreparedStatementsFrom,
1112 /// The `GRANT`/`REVOKE` head is claimed by both
1113 /// [`AccessControlSyntax::access_control_account_grants`] (MySQL's account-based grammar) and
1114 /// [`AccessControlSyntax::access_control_extended_objects`] (the standard/PostgreSQL extended
1115 /// object and prefix grammar). A **route-flag** conflict (see the enum doc): the account route
1116 /// dispatches its whole grammar before the extended-object reading is consulted, so enabling
1117 /// both silently deadens the extended-object grammar even when it is explicitly on. The `GRANT`
1118 /// byte lexes to one keyword (no [`LexicalConflict`]) and neither flag rides the other, so the
1119 /// contention is purely grammatical, resolved by fixed branch order with no lookahead. No
1120 /// shipped preset pairs them: MySQL arms the account route with extended objects off; ANSI/
1121 /// PostgreSQL/DuckDB/Lenient keep the extended-object grammar with the account route off — the
1122 /// asymmetry the [`access_control_account_grants`](AccessControlSyntax::access_control_account_grants)
1123 /// field doc's "a dialect cannot enable both grant grammars at once" sentence records.
1124 AccountGrantsVersusExtendedObjects,
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use crate::dialect::lex_class::CLASS_IDENTIFIER_START;
1130 use crate::dialect::*;
1131
1132 #[test]
1133 fn lexical_conflict_flags_a_sigil_byte_marked_identifier_start() {
1134 // The `#`-comment/`#`-identifier either/or, generalized: a feature that leads
1135 // with a sigil byte must not also have that byte in the identifier-start class of
1136 // a custom `ByteClasses`, or an identifier scan would shadow the sigil dispatch.
1137 // No shipped preset trips these — `STANDARD_BYTE_CLASSES` marks none of `$`/`@`/`:`
1138 // identifier-start — so each is reached only through a custom table.
1139 let dollar = FeatureSet::ANSI.with(
1140 FeatureDelta::EMPTY
1141 .parameters(ParameterSyntax {
1142 positional_dollar: true,
1143 ..ParameterSyntax::ANSI
1144 })
1145 .byte_classes(
1146 FeatureSet::ANSI
1147 .byte_classes
1148 .with_class(b'$', CLASS_IDENTIFIER_START),
1149 ),
1150 );
1151 assert_eq!(
1152 dollar.lexical_conflict(),
1153 Some(LexicalConflict::DollarSigilVersusIdentifierByte),
1154 );
1155
1156 let at = FeatureSet::ANSI.with(
1157 FeatureDelta::EMPTY
1158 .parameters(ParameterSyntax {
1159 named_at: true,
1160 ..ParameterSyntax::ANSI
1161 })
1162 .byte_classes(
1163 FeatureSet::ANSI
1164 .byte_classes
1165 .with_class(b'@', CLASS_IDENTIFIER_START),
1166 ),
1167 );
1168 assert_eq!(
1169 at.lexical_conflict(),
1170 Some(LexicalConflict::AtSigilVersusIdentifierByte),
1171 );
1172
1173 let colon = FeatureSet::ANSI.with(
1174 FeatureDelta::EMPTY
1175 .parameters(ParameterSyntax {
1176 named_colon: true,
1177 ..ParameterSyntax::ANSI
1178 })
1179 .byte_classes(
1180 FeatureSet::ANSI
1181 .byte_classes
1182 .with_class(b':', CLASS_IDENTIFIER_START),
1183 ),
1184 );
1185 assert_eq!(
1186 colon.lexical_conflict(),
1187 Some(LexicalConflict::ColonSigilVersusIdentifierByte),
1188 );
1189
1190 // The check is the pair, not the byte class alone: marking `$` identifier-start
1191 // with no `$`-led feature on is a coherent custom choice, not a conflict.
1192 let dollar_identifier_only = FeatureSet::ANSI.with(
1193 FeatureDelta::EMPTY.byte_classes(
1194 FeatureSet::ANSI
1195 .byte_classes
1196 .with_class(b'$', CLASS_IDENTIFIER_START),
1197 ),
1198 );
1199 assert_eq!(dollar_identifier_only.lexical_conflict(), None);
1200 }
1201
1202 #[test]
1203 fn positional_column_conflicts_on_the_hash_trigger() {
1204 // DuckDB's `#n` positional reference claims `#`+digit; pairing it with either
1205 // other `#` claimant is a conflict, since the tokenizer resolves `#` to one
1206 // meaning and shadows the rest. No shipped preset pairs them (DuckDB has neither
1207 // rival), so each is reached only through a constructed combination.
1208 let with_xor = FeatureSet::DUCKDB.with(FeatureDelta::EMPTY.hash_bitwise_xor(true));
1209 assert_eq!(
1210 with_xor.lexical_conflict(),
1211 Some(LexicalConflict::HashXorOperatorVersusPositionalColumn),
1212 );
1213
1214 let with_comment =
1215 FeatureSet::DUCKDB.with(FeatureDelta::EMPTY.comment_syntax(CommentSyntax {
1216 line_comment_hash: true,
1217 ..FeatureSet::DUCKDB.comment_syntax
1218 }));
1219 assert_eq!(
1220 with_comment.lexical_conflict(),
1221 Some(LexicalConflict::HashCommentVersusPositionalColumn),
1222 );
1223
1224 // DuckDB itself — positional on (proven by the parser round-trip tests), no rival
1225 // `#` reading — is consistent.
1226 assert_eq!(FeatureSet::DUCKDB.lexical_conflict(), None);
1227 }
1228
1229 #[test]
1230 fn every_shipped_preset_satisfies_its_feature_dependencies() {
1231 // The dependency sibling of the per-preset lexical assertions: every dependent
1232 // grammar flag a shipped preset sets also has the base flag it rides on, so no
1233 // preset ships an inert flag.
1234 for preset in [
1235 FeatureSet::ANSI,
1236 FeatureSet::POSTGRES,
1237 FeatureSet::MYSQL,
1238 FeatureSet::SQLITE,
1239 FeatureSet::DUCKDB,
1240 FeatureSet::BIGQUERY,
1241 FeatureSet::HIVE,
1242 FeatureSet::CLICKHOUSE,
1243 FeatureSet::DATABRICKS,
1244 FeatureSet::MSSQL,
1245 FeatureSet::SNOWFLAKE,
1246 FeatureSet::REDSHIFT,
1247 FeatureSet::LENIENT,
1248 ] {
1249 assert_eq!(preset.feature_dependencies(), None);
1250 assert!(preset.has_satisfied_feature_dependencies());
1251 }
1252 }
1253
1254 #[test]
1255 fn feature_dependencies_detects_each_unsatisfied_dependency() {
1256 // ANSI is dependency-clean, so flipping exactly one base off while its dependent
1257 // stays on isolates that variant as the first (and only) violation. Sibling
1258 // dependents on the same multi-dependent base (`locking_clauses`,
1259 // `quantified_comparisons`, `merge`, `alter_table_extended`) are pinned off so the
1260 // intended variant is the one returned.
1261 use FeatureDependencyViolation as V;
1262
1263 let key_lock =
1264 FeatureSet::ANSI.with(FeatureDelta::EMPTY.query_tail_syntax(QueryTailSyntax {
1265 locking_clauses: false,
1266 key_lock_strengths: true,
1267 stacked_locking_clauses: false,
1268 ..FeatureSet::ANSI.query_tail_syntax
1269 }));
1270 assert_eq!(
1271 key_lock.feature_dependencies(),
1272 Some(V::KeyLockStrengthsWithoutLockingClauses),
1273 );
1274
1275 let stacked =
1276 FeatureSet::ANSI.with(FeatureDelta::EMPTY.query_tail_syntax(QueryTailSyntax {
1277 locking_clauses: false,
1278 key_lock_strengths: false,
1279 stacked_locking_clauses: true,
1280 ..FeatureSet::ANSI.query_tail_syntax
1281 }));
1282 assert_eq!(
1283 stacked.feature_dependencies(),
1284 Some(V::StackedLockingClausesWithoutLockingClauses),
1285 );
1286
1287 let unnest_offset =
1288 FeatureSet::ANSI.with(FeatureDelta::EMPTY.table_factor_syntax(TableFactorSyntax {
1289 unnest: false,
1290 unnest_with_offset: true,
1291 ..FeatureSet::ANSI.table_factor_syntax
1292 }));
1293 assert_eq!(
1294 unnest_offset.feature_dependencies(),
1295 Some(V::UnnestWithOffsetWithoutUnnest),
1296 );
1297
1298 let slice =
1299 FeatureSet::ANSI.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
1300 subscript: false,
1301 slice_step: true,
1302 ..FeatureSet::ANSI.expression_syntax
1303 }));
1304 assert_eq!(
1305 slice.feature_dependencies(),
1306 Some(V::SliceStepWithoutSubscript)
1307 );
1308
1309 let multidim =
1310 FeatureSet::ANSI.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
1311 array_constructor: false,
1312 multidim_array_literals: true,
1313 ..FeatureSet::ANSI.expression_syntax
1314 }));
1315 assert_eq!(
1316 multidim.feature_dependencies(),
1317 Some(V::MultidimArrayLiteralsWithoutArrayConstructor),
1318 );
1319
1320 let quant_lists =
1321 FeatureSet::ANSI.with(FeatureDelta::EMPTY.operator_syntax(OperatorSyntax {
1322 quantified_comparisons: false,
1323 quantified_comparison_lists: true,
1324 quantified_arbitrary_operator: false,
1325 ..FeatureSet::ANSI.operator_syntax
1326 }));
1327 assert_eq!(
1328 quant_lists.feature_dependencies(),
1329 Some(V::QuantifiedComparisonListsWithoutQuantifiedComparisons),
1330 );
1331
1332 let quant_arbitrary =
1333 FeatureSet::ANSI.with(FeatureDelta::EMPTY.operator_syntax(OperatorSyntax {
1334 quantified_comparisons: false,
1335 quantified_comparison_lists: false,
1336 quantified_arbitrary_operator: true,
1337 ..FeatureSet::ANSI.operator_syntax
1338 }));
1339 assert_eq!(
1340 quant_arbitrary.feature_dependencies(),
1341 Some(V::QuantifiedArbitraryOperatorWithoutQuantifiedComparisons),
1342 );
1343
1344 let lambda = FeatureSet::ANSI.with(FeatureDelta::EMPTY.operator_syntax(OperatorSyntax {
1345 json_arrow_operators: false,
1346 lambda_expressions: true,
1347 ..FeatureSet::ANSI.operator_syntax
1348 }));
1349 assert_eq!(
1350 lambda.feature_dependencies(),
1351 Some(V::LambdaExpressionsWithoutJsonArrowOperators),
1352 );
1353
1354 // The four `MERGE` extensions, each isolated with `merge` off and its three
1355 // siblings pinned off.
1356 let merge_base = MutationSyntax {
1357 merge: false,
1358 cte_before_merge: false,
1359 merge_when_not_matched_by: false,
1360 merge_insert_default_values: false,
1361 merge_insert_overriding: false,
1362 merge_update_set_star: false,
1363 merge_insert_star_by_name: false,
1364 merge_error_action: false,
1365 ..FeatureSet::ANSI.mutation_syntax
1366 };
1367 let cte_merge =
1368 FeatureSet::ANSI.with(FeatureDelta::EMPTY.mutation_syntax(MutationSyntax {
1369 cte_before_merge: true,
1370 ..merge_base
1371 }));
1372 assert_eq!(
1373 cte_merge.feature_dependencies(),
1374 Some(V::CteBeforeMergeWithoutMerge),
1375 );
1376 let when_not_matched =
1377 FeatureSet::ANSI.with(FeatureDelta::EMPTY.mutation_syntax(MutationSyntax {
1378 merge_when_not_matched_by: true,
1379 ..merge_base
1380 }));
1381 assert_eq!(
1382 when_not_matched.feature_dependencies(),
1383 Some(V::MergeWhenNotMatchedByWithoutMerge),
1384 );
1385 let insert_defaults =
1386 FeatureSet::ANSI.with(FeatureDelta::EMPTY.mutation_syntax(MutationSyntax {
1387 merge_insert_default_values: true,
1388 ..merge_base
1389 }));
1390 assert_eq!(
1391 insert_defaults.feature_dependencies(),
1392 Some(V::MergeInsertDefaultValuesWithoutMerge),
1393 );
1394 let insert_overriding =
1395 FeatureSet::ANSI.with(FeatureDelta::EMPTY.mutation_syntax(MutationSyntax {
1396 merge_insert_overriding: true,
1397 merge_update_set_star: false,
1398 merge_insert_star_by_name: false,
1399 merge_error_action: false,
1400 ..merge_base
1401 }));
1402 assert_eq!(
1403 insert_overriding.feature_dependencies(),
1404 Some(V::MergeInsertOverridingWithoutMerge),
1405 );
1406
1407 let alter_guards =
1408 FeatureSet::ANSI.with(FeatureDelta::EMPTY.index_alter_syntax(IndexAlterSyntax {
1409 alter_table_extended: false,
1410 alter_existence_guards: true,
1411 alter_column_set_data_type: false,
1412 ..FeatureSet::ANSI.index_alter_syntax
1413 }));
1414 assert_eq!(
1415 alter_guards.feature_dependencies(),
1416 Some(V::AlterExistenceGuardsWithoutAlterTableExtended),
1417 );
1418 let alter_set_type =
1419 FeatureSet::ANSI.with(FeatureDelta::EMPTY.index_alter_syntax(IndexAlterSyntax {
1420 alter_table_extended: false,
1421 alter_existence_guards: false,
1422 alter_column_set_data_type: true,
1423 ..FeatureSet::ANSI.index_alter_syntax
1424 }));
1425 assert_eq!(
1426 alter_set_type.feature_dependencies(),
1427 Some(V::AlterColumnSetDataTypeWithoutAlterTableExtended),
1428 );
1429
1430 let checkpoint_db =
1431 FeatureSet::ANSI.with(FeatureDelta::EMPTY.maintenance_syntax(MaintenanceSyntax {
1432 checkpoint: false,
1433 checkpoint_database: true,
1434 ..FeatureSet::ANSI.maintenance_syntax
1435 }));
1436 assert_eq!(
1437 checkpoint_db.feature_dependencies(),
1438 Some(V::CheckpointDatabaseWithoutCheckpoint),
1439 );
1440 let analyze_cols =
1441 FeatureSet::ANSI.with(FeatureDelta::EMPTY.maintenance_syntax(MaintenanceSyntax {
1442 analyze: false,
1443 analyze_columns: true,
1444 ..FeatureSet::ANSI.maintenance_syntax
1445 }));
1446 assert_eq!(
1447 analyze_cols.feature_dependencies(),
1448 Some(V::AnalyzeColumnsWithoutAnalyze),
1449 );
1450 let load_bare = FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1451 load_extension: false,
1452 load_bare_name: true,
1453 ..FeatureSet::ANSI.utility_syntax
1454 }));
1455 assert_eq!(
1456 load_bare.feature_dependencies(),
1457 Some(V::LoadBareNameWithoutLoadExtension),
1458 );
1459 let call_bare = FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1460 call: false,
1461 call_bare_name: true,
1462 ..FeatureSet::ANSI.utility_syntax
1463 }));
1464 assert_eq!(
1465 call_bare.feature_dependencies(),
1466 Some(V::CallBareNameWithoutCall),
1467 );
1468 let detach = FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1469 attach: false,
1470 detach_if_exists: true,
1471 ..FeatureSet::ANSI.utility_syntax
1472 }));
1473 assert_eq!(
1474 detach.feature_dependencies(),
1475 Some(V::DetachIfExistsWithoutAttach),
1476 );
1477 let use_qualified =
1478 FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1479 use_statement: false,
1480 use_qualified_name: true,
1481 ..FeatureSet::ANSI.utility_syntax
1482 }));
1483 assert_eq!(
1484 use_qualified.feature_dependencies(),
1485 Some(V::UseQualifiedNameWithoutUseStatement),
1486 );
1487 let use_string = FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1488 use_statement: false,
1489 use_string_literal_name: true,
1490 ..FeatureSet::ANSI.utility_syntax
1491 }));
1492 assert_eq!(
1493 use_string.feature_dependencies(),
1494 Some(V::UseStringLiteralNameWithoutUseStatement),
1495 );
1496 let extended_ac = FeatureSet::ANSI.with(FeatureDelta::EMPTY.access_control_syntax(
1497 AccessControlSyntax {
1498 access_control: false,
1499 access_control_extended_objects: true,
1500 user_role_management: false,
1501 access_control_account_grants: false,
1502 alter_role_rename: false,
1503 },
1504 ));
1505 assert_eq!(
1506 extended_ac.feature_dependencies(),
1507 Some(V::AccessControlExtendedObjectsWithoutAccessControl),
1508 );
1509 let account_grants = FeatureSet::ANSI.with(FeatureDelta::EMPTY.access_control_syntax(
1510 AccessControlSyntax {
1511 access_control: false,
1512 access_control_extended_objects: false,
1513 user_role_management: false,
1514 access_control_account_grants: true,
1515 alter_role_rename: false,
1516 },
1517 ));
1518 assert_eq!(
1519 account_grants.feature_dependencies(),
1520 Some(V::AccountGrantsWithoutAccessControl),
1521 );
1522 let typed_params =
1523 FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1524 prepared_statements: false,
1525 prepare_typed_parameters: true,
1526 ..FeatureSet::ANSI.utility_syntax
1527 }));
1528 assert_eq!(
1529 typed_params.feature_dependencies(),
1530 Some(V::PrepareTypedParametersWithoutPreparedStatements),
1531 );
1532 }
1533
1534 #[test]
1535 fn feature_dependencies_and_lexical_conflict_stay_mece() {
1536 // A set can carry a lexical conflict and be dependency-clean, or vice versa —
1537 // the two registries never both claim the same combination. ANSI plus a bracket
1538 // slice with no subscript is a pure dependency violation and no lexical conflict.
1539 let dep_only =
1540 FeatureSet::ANSI.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
1541 subscript: false,
1542 slice_step: true,
1543 ..FeatureSet::ANSI.expression_syntax
1544 }));
1545 assert_eq!(
1546 dep_only.feature_dependencies(),
1547 Some(FeatureDependencyViolation::SliceStepWithoutSubscript),
1548 );
1549 assert_eq!(dep_only.lexical_conflict(), None);
1550 }
1551
1552 #[test]
1553 fn without_dangling_dependents_clears_only_the_inert_refinements() {
1554 // A base flag turned off under a preset that had refinements riding it leaves those
1555 // refinements dangling; the normal form clears exactly them, satisfies the
1556 // dependency registry, and touches nothing else (the base flag it *did* keep, the
1557 // rest of the set). PostgreSQL enables `merge` plus its `cte_before_merge` /
1558 // `merge_when_not_matched_by` refinements — dropping `merge` dangles both.
1559 let dangling =
1560 FeatureSet::POSTGRES.with(FeatureDelta::EMPTY.mutation_syntax(MutationSyntax {
1561 merge: false,
1562 ..FeatureSet::POSTGRES.mutation_syntax
1563 }));
1564 assert!(dangling.feature_dependencies().is_some());
1565
1566 let cleaned = dangling.without_dangling_dependents();
1567 assert_eq!(cleaned.feature_dependencies(), None);
1568 assert!(cleaned.has_satisfied_feature_dependencies());
1569 // The inert refinements were cleared…
1570 assert!(!cleaned.mutation_syntax.cte_before_merge);
1571 assert!(!cleaned.mutation_syntax.merge_when_not_matched_by);
1572 // …and nothing that was already consistent changed: the (still-off) base is
1573 // untouched, and an already-clean set is returned verbatim.
1574 assert_eq!(
1575 cleaned.mutation_syntax.merge,
1576 dangling.mutation_syntax.merge
1577 );
1578 assert_eq!(
1579 FeatureSet::POSTGRES.without_dangling_dependents(),
1580 FeatureSet::POSTGRES,
1581 );
1582 }
1583
1584 #[test]
1585 fn every_shipped_preset_has_no_grammar_conflict() {
1586 // The parser-position sibling of the per-preset lexical and dependency assertions:
1587 // no shipped preset enables two features that contend for the same grammar head, so this
1588 // one loop covers every registered variant collectively (any preset tripping a new guard
1589 // would return `Some` here). The four registered pairs are each split across presets:
1590 // `prefix_colon_alias` vs `semi_structured_access` (DuckDB/Lenient have the prefix alias,
1591 // Snowflake/Databricks the path); `do_statement` vs `do_expression_list` (PostgreSQL/
1592 // Lenient the block, MySQL the expression list); `prepared_statements` vs
1593 // `prepared_statements_from` (DuckDB/PostgreSQL/Lenient the typed-`AS` form, MySQL the
1594 // `FROM`/`USING` form); and `access_control_account_grants` vs
1595 // `access_control_extended_objects` (MySQL the account route, everyone else the extended
1596 // objects). Lenient — the union preset that pairs `describe` with `describe_summarize` —
1597 // stays clean because that shared `DESCRIBE` leader is a deterministic dispatch-order
1598 // union, not a registered conflict.
1599 for preset in [
1600 FeatureSet::ANSI,
1601 FeatureSet::POSTGRES,
1602 FeatureSet::MYSQL,
1603 FeatureSet::SQLITE,
1604 FeatureSet::DUCKDB,
1605 FeatureSet::BIGQUERY,
1606 FeatureSet::HIVE,
1607 FeatureSet::CLICKHOUSE,
1608 FeatureSet::DATABRICKS,
1609 FeatureSet::MSSQL,
1610 FeatureSet::SNOWFLAKE,
1611 FeatureSet::REDSHIFT,
1612 FeatureSet::LENIENT,
1613 ] {
1614 assert_eq!(preset.grammar_conflict(), None);
1615 assert!(preset.has_no_grammar_conflict());
1616 }
1617 }
1618
1619 #[test]
1620 fn grammar_conflict_detects_prefix_colon_versus_semi_structured() {
1621 // DuckDB ships the prefix colon alias with `semi_structured_access` off; forcing the
1622 // path grammar on alongside it pairs the two `<ident> :` head claimants, which the
1623 // registry must flag. (`named_colon` stays off, so this is a pure grammar contention
1624 // and not the sibling `ColonParameterVersusSliceBound` lexical conflict.)
1625 let both =
1626 FeatureSet::DUCKDB.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
1627 semi_structured_access: true,
1628 ..FeatureSet::DUCKDB.expression_syntax
1629 }));
1630 assert_eq!(
1631 both.grammar_conflict(),
1632 Some(GrammarConflict::PrefixColonAliasVersusSemiStructuredAccess),
1633 );
1634 assert!(!both.has_no_grammar_conflict());
1635 // Neither flag alone is a conflict.
1636 assert_eq!(FeatureSet::DUCKDB.grammar_conflict(), None);
1637 assert_eq!(FeatureSet::SNOWFLAKE.grammar_conflict(), None);
1638 }
1639
1640 #[test]
1641 fn grammar_conflict_stays_mece_with_the_lexical_and_dependency_siblings() {
1642 // A pure grammar-position contention carries no tokenizer trigger and no base-flag
1643 // dependency: DuckDB plus `semi_structured_access` is a grammar conflict yet stays
1644 // lexically consistent (`named_colon` is off, so `:` has one claimant) and
1645 // dependency-clean.
1646 let grammar_only =
1647 FeatureSet::DUCKDB.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
1648 semi_structured_access: true,
1649 ..FeatureSet::DUCKDB.expression_syntax
1650 }));
1651 assert_eq!(
1652 grammar_only.grammar_conflict(),
1653 Some(GrammarConflict::PrefixColonAliasVersusSemiStructuredAccess),
1654 );
1655 assert_eq!(grammar_only.lexical_conflict(), None);
1656 assert_eq!(grammar_only.feature_dependencies(), None);
1657 }
1658
1659 #[test]
1660 fn grammar_conflict_detects_do_statement_versus_do_expression_list() {
1661 // MySQL ships the `DO <expr-list>` statement (`do_expression_list` on) with the PostgreSQL
1662 // code block off; forcing `do_statement` on alongside it pairs the two `DO`-head readings.
1663 let both = FeatureSet::MYSQL.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1664 do_statement: true,
1665 ..FeatureSet::MYSQL.utility_syntax
1666 }));
1667 assert_eq!(
1668 both.grammar_conflict(),
1669 Some(GrammarConflict::DoStatementVersusDoExpressionList),
1670 );
1671 assert!(!both.has_no_grammar_conflict());
1672 // Neither dialect alone is a conflict.
1673 assert_eq!(FeatureSet::MYSQL.grammar_conflict(), None);
1674 assert_eq!(FeatureSet::POSTGRES.grammar_conflict(), None);
1675 }
1676
1677 #[test]
1678 fn grammar_conflict_detects_prepared_statements_versus_prepared_statements_from() {
1679 // DuckDB ships the typed-`AS` prepared-statement lifecycle (`prepared_statements` on) with
1680 // MySQL's `FROM`/`USING` form off; forcing `prepared_statements_from` on alongside it pairs
1681 // the two lifecycles on the shared `PREPARE`/`EXECUTE`/`DEALLOCATE` keywords — the
1682 // combination whose `DEALLOCATE` tail is incoherent with the dispatch order.
1683 let both = FeatureSet::DUCKDB.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
1684 prepared_statements_from: true,
1685 ..FeatureSet::DUCKDB.utility_syntax
1686 }));
1687 assert_eq!(
1688 both.grammar_conflict(),
1689 Some(GrammarConflict::PreparedStatementsVersusPreparedStatementsFrom),
1690 );
1691 assert!(!both.has_no_grammar_conflict());
1692 // Neither dialect alone is a conflict.
1693 assert_eq!(FeatureSet::DUCKDB.grammar_conflict(), None);
1694 assert_eq!(FeatureSet::MYSQL.grammar_conflict(), None);
1695 }
1696
1697 #[test]
1698 fn grammar_conflict_detects_account_grants_versus_extended_objects() {
1699 // MySQL ships the account-based grant route (`access_control_account_grants` on) with the
1700 // extended-object grammar off; forcing `access_control_extended_objects` on alongside it
1701 // pairs the two `GRANT`/`REVOKE` grammars — the route-flag conflict, where the account
1702 // route deadens the explicitly-enabled extended-object reading. (`access_control` stays on,
1703 // so the extended-object flag is not a dependency violation.)
1704 let both = FeatureSet::MYSQL.with(FeatureDelta::EMPTY.access_control_syntax(
1705 AccessControlSyntax {
1706 access_control_extended_objects: true,
1707 ..FeatureSet::MYSQL.access_control_syntax
1708 },
1709 ));
1710 assert_eq!(
1711 both.grammar_conflict(),
1712 Some(GrammarConflict::AccountGrantsVersusExtendedObjects),
1713 );
1714 assert!(!both.has_no_grammar_conflict());
1715 assert_eq!(both.feature_dependencies(), None);
1716 // Neither route alone is a conflict.
1717 assert_eq!(FeatureSet::MYSQL.grammar_conflict(), None);
1718 assert_eq!(FeatureSet::POSTGRES.grammar_conflict(), None);
1719 }
1720}