Skip to main content

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}