Skip to main content

squonk_ast/dialect/
lenient.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The optional `LENIENT` tooling preset — an honest, documented permissive union.
5//!
6//! Gated behind the default-off `lenient` cargo feature, so a build without it
7//! compiles none of this data. [`FeatureSet::LENIENT`](super::FeatureSet::LENIENT) is the "parse anything" mode tooling reaches
8//! for when it must accept SQL of unknown origin: it enables the permissive value of
9//! every independent feature and, where two features contend for one tokenizer trigger,
10//! resolves the conflict explicitly (documented per rule below).
11//!
12//! It is deliberately **not** called "generic": "generic" is [`ANSI`](super::ANSI), the
13//! principled SQL:2016 baseline. `LENIENT` is the opposite construction — a maximal
14//! union whose every inclusion and every conflict-resolution is spelled out, never a
15//! vibe-union of whatever several dialects happen to accept. The honesty bar is that a
16//! reader can predict, from this module alone, exactly what `LENIENT` accepts and which
17//! meaning it picks for every contested form.
18//!
19//! # Conflict-resolution rules
20//!
21//! Most features are independent and simply take their accepting value. The exceptions
22//! are the features that claim a *shared* context-free tokenizer trigger; for those,
23//! enabling both claimants is a [`LexicalConflict`](super::LexicalConflict), so `LENIENT`
24//! picks one meaning and forgoes the other:
25//!
26//! 1. **`"…"` is a quoted identifier, not a string.** `double_quoted_strings` is OFF so
27//!    `"` stays an [identifier quote](LENIENT_IDENTIFIER_QUOTES). The whole point of the
28//!    multi-quote union is to accept `"weird name"` as an identifier everywhere; the
29//!    MySQL `ANSI_QUOTES`-off reading of `"…"` as a string is the sacrifice.
30//! 2. **`[…]` is a quoted identifier; `[`-punctuation syntax is off.** The tokenizer
31//!    resolves `[` context-free, so the same byte cannot also be array subscript /
32//!    constructor punctuation. `subscript`, `array_constructor`, and
33//!    `collection_literals` are OFF (and array *type* suffixes `T[]`, which are not
34//!    feature-gated, likewise will not parse). T-SQL `[bracketed]` identifiers win;
35//!    PostgreSQL `a[1]` / `ARRAY[…]` and the DuckDB `[1, 2]` list literal are the
36//!    sacrifice.
37//! 3. **`$<digit>` is a positional parameter, not money.** `money_literals` is OFF and
38//!    `positional_dollar` is ON. The PostgreSQL `$1` parameter is the dominant real-world
39//!    meaning; the T-SQL `$1234.56` money literal is the sacrifice (the scanner tries
40//!    money first, so the two cannot coexist on `$`+digit).
41//! 4. **`||` is string concatenation, not logical OR.** [`PipeOperator::StringConcat`] is
42//!    the SQL-standard / ANSI / PostgreSQL meaning (and MySQL under `PIPES_AS_CONCAT`);
43//!    MySQL's default `||`=OR is the sacrifice. Both still parse — only the operator's
44//!    identity and precedence differ.
45//! 5. **Reserved-identifier model is ANSI's.** `LENIENT` keeps the position-aware
46//!    [`RESERVED_COLUMN_NAME`] family. It is deliberately
47//!    *not* the empty set (the load-bearing reserved words must stay reserved or the
48//!    grammar cannot disambiguate `SELECT a select b`) and *not* a cross-dialect
49//!    intersection (that would couple `lenient` to the gated `mysql`/`postgres` data). A
50//!    handful of words a specific dialect frees but ANSI reserves (e.g. `OFFSET`) stay
51//!    reserved — quote them to use as identifiers.
52//! 6. **Identity folding is [`Casing::Preserve`].** Neither ANSI's upper-fold nor the
53//!    PostgreSQL/MySQL lower-fold is imposed; exact text is preserved. This is a tooling
54//!    convenience (do not invent a fold a source we cannot identify never asked for) and
55//!    does not affect what parses.
56//! 7. **Null ordering is [`NullOrdering::NullsLast`]** (the ANSI/PostgreSQL default).
57//!    MySQL's nulls-first default is the sacrifice; it only affects the semantics of an
58//!    omitted `NULLS FIRST`/`LAST`, never acceptance.
59//! 8. **Bitwise XOR is `^`, not `#`.** [`CaretOperator::BitwiseXor`] admits the MySQL `^`
60//!    XOR spelling; the PostgreSQL `#` spelling would need `#` to lex as an operator, but
61//!    `#` opens a line comment here (rule for `#`, [`CommentSyntax::LENIENT`]), so
62//!    `#`-XOR is the sacrifice ([`LexicalConflict::HashXorOperatorVersusHashComment`](super::LexicalConflict::HashXorOperatorVersusHashComment)).
63//!    Its precedence follows the STANDARD table (looser than additive), not MySQL's tight
64//!    `^` rank — a precedence-only sacrifice, like rule 4's `||`.
65//!
66//! Rules 1-8 above are the *lexical* conflict resolutions — a shared tokenizer trigger
67//! whose either/or is a [`LexicalConflict`](super::LexicalConflict). Many permissive
68//! choices genuinely are pure additions with no contended trigger: every
69//! string/number/parameter prefix form, `&&`-as-`AND`, the MySQL keyword infix operators,
70//! `#` line comments, MySQL `/*!…*/` versioned comments (always included — see
71//! [`CommentSyntax::LENIENT`]), `$`-in-identifiers, and most of the table / mutation /
72//! select / type-name / utility (`COPY`) extension surface.
73//!
74//! But the *statement-head* extensions are not blanket-additive: several leading keywords
75//! are claimed by two or more features, and `LENIENT` resolves each by a lookahead split, a
76//! dispatch precedence, or a deliberate one-reading exclusion. Those resolutions — including
77//! the flags this preset turns *off* (`do_expression_list`, `prepared_statements_from`,
78//! `drop_database`, `index_drop_on_table`, `access_control_account_grants`,
79//! `variable_assignment`) and the accepted-both unions (`LOAD`, `VACUUM`, `ANALYZE`,
80//! `ALTER VIEW`, `ALTER DATABASE`, `IMPORT`, `UPDATE`, `LOCK`/`UNLOCK`, `CACHE`) — are
81//! enumerated, one row per head, in the
82//! [`MULTI_CLAIMANT_STATEMENT_HEADS`](super::MULTI_CLAIMANT_STATEMENT_HEADS) ledger. Each
83//! `false` in a statement-gate field below that carries a "stays off" / conflict-resolution
84//! note has a corresponding exclusion row there; the ledger's `lenient_exclusions_match_the_ledger`
85//! test proves that correspondence is complete in both directions.
86
87use super::{
88    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
89    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
90    DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
91    IdentifierQuote, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, KeywordOperators, KeywordSet,
92    MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
93    ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, RESERVED_BARE_ALIAS,
94    RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME, STANDARD_BYTE_CLASSES,
95    SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates, StringFuncForms,
96    StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling, TypeNameSyntax,
97    UtilitySyntax,
98};
99use crate::precedence::{
100    BindingPowerTable, IS_PREDICATE_BELOW_COMPARISON, RANGE_PREDICATE_ABOVE_COMPARISON,
101    STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS,
102};
103
104/// The permissive multi-style identifier quoting `LENIENT` accepts: the SQL-standard
105/// `"…"`, the MySQL backtick `` `…` ``, and the T-SQL bracket `[…]` — all at once.
106///
107/// This is the one real cost of "parse anything": the tokenizer matches an opening
108/// delimiter against the whole set ([`FeatureSet::identifier_quotes`] is already a
109/// slice, and the scanner iterates it), so no per-dialect single-style assumption is
110/// baked in. The three openers (`"`, `` ` ``, `[`) are distinct bytes, so their order is
111/// immaterial — there is no precedence ambiguity to resolve. `"` appears here (and
112/// `double_quoted_strings` is correspondingly OFF) per conflict-resolution rule 1.
113pub const LENIENT_IDENTIFIER_QUOTES: &[IdentifierQuote] = &[
114    IdentifierQuote::Symmetric('"'),
115    IdentifierQuote::Symmetric('`'),
116    IdentifierQuote::Asymmetric {
117        open: '[',
118        close: ']',
119    },
120];
121
122impl CommentSyntax {
123    /// `LENIENT`: accept `#` line comments (MySQL) on top of the baseline `--` / `/* */`.
124    /// Safe with [`STANDARD_BYTE_CLASSES`], where `#` is not an identifier-start byte
125    /// (conflict-resolution: a `#`-led identifier would be shadowed by the comment).
126    ///
127    /// MySQL `/*!…*/` versioned comments are conditional inclusion with an unbounded
128    /// version gate (`u32::MAX`): a version-agnostic reader executes every conditional
129    /// region rather than modelling one server's skip window, so a MySQL or MariaDB
130    /// dump's `/*!NNNNN … */` bodies always parse. Block-comment *nesting* stays on —
131    /// the two knobs pull in opposite directions for MySQL fidelity, but `LENIENT`
132    /// keeps the permissive PostgreSQL superset it has always accepted (nesting
133    /// rejects strictly fewer inputs than it accepts only on the degenerate
134    /// `/* a /* b */`-unbalanced family, which no dump emits).
135    pub const LENIENT: Self = Self {
136        line_comment_hash: true,
137        // Off: a `\r` that does not end a comment keeps the comment running longer, which
138        // rejects strictly fewer inputs — the permissive reading `LENIENT` favours. (A
139        // lone-`\r`-separated old-Mac source would merge lines into one comment, the only
140        // input this changes; `\r\n` endings still terminate at the `\n`.)
141        line_comment_ends_at_carriage_return: false,
142        nested_block_comments: true,
143        versioned_comments: Some(u32::MAX),
144        // Union widening: SQLite silently closes an unterminated `/* …` at EOF, so the
145        // permissive union accepts it too (a pure accept-side addition — a `/*` with a byte
146        // after it that runs off the end becomes trailing trivia rather than an error).
147        unterminated_block_comment_at_eof: true,
148    };
149}
150
151impl StringLiteralSyntax {
152    /// `LENIENT`: every dialect string form *except* `double_quoted_strings`.
153    ///
154    /// `double_quoted_strings` is OFF because `"` is a quoted-identifier delimiter here
155    /// (conflict-resolution rule 1). `backslash_escapes` is ON: it is a strict superset
156    /// of escape recognition — the standard `''` doubling still closes a string, and `\'`
157    /// additionally escapes — so it accepts the most input (the one form it changes is a
158    /// trailing `'a\'`, which becomes an escaped quote rather than a close).
159    ///
160    /// `national_strings` is ON from the MySQL/T-SQL presets (PostgreSQL does not arm it —
161    /// its scanner has no `N'…'` constant and reads `N'x'` as the typed literal `nchar 'x'`;
162    /// see the `POSTGRES` preset). Keeping it here means `N'x'` lexes as one national-string
163    /// token under LENIENT, shadowing that PG typed-literal reading. This is a grammar-level
164    /// shape choice, not a [`LexicalConflict`](super::LexicalConflict): the alternative
165    /// reading is the parser's typed-literal fallback, not a second enabled lexer claimant,
166    /// and both readings *accept* — so the union keeps the token form, consistent with the
167    /// accept-most-input model.
168    pub const LENIENT: Self = Self {
169        escape_strings: true,
170        dollar_quoted_strings: true,
171        national_strings: true,
172        double_quoted_strings: false,
173        backslash_escapes: true,
174        unicode_strings: true,
175        bit_string_literals: true,
176        // The lenient superset keeps `X'…'` the permissive deferred bit-string (odd hex
177        // tolerated), so the eager even-byte blob gate stays off.
178        blob_literals: false,
179        charset_introducers: true,
180        // Accept MySQL's same-line adjacent-literal concatenation in the superset.
181        same_line_adjacent_concat: true,
182    };
183}
184
185impl NumericLiteralSyntax {
186    /// `LENIENT`: every radix and separator form, but *not* `money_literals`.
187    ///
188    /// `money_literals` is OFF because `$`+digit is a positional parameter here
189    /// (conflict-resolution rule 3): the two cannot coexist, and `$1` parameters are the
190    /// dominant real-world meaning.
191    pub const LENIENT: Self = Self {
192        hex_integers: true,
193        octal_integers: true,
194        binary_integers: true,
195        underscore_separators: true,
196        // Every separator form, including PG's leading-underscore radix body (`0x_1F`).
197        // A pure widening: with `reject_trailing_junk` off `0x_1F` already accepts as a
198        // bare `0` plus a `x_1F` alias, so this only folds it into one number token.
199        radix_leading_underscore: true,
200        money_literals: false,
201        // Lenient accepts the maximal input surface, so it never rejects trailing
202        // numeric junk — the leftover falls through to the ordinary word/alias scan.
203        reject_trailing_junk: false,
204    };
205}
206
207impl ParameterSyntax {
208    /// `LENIENT`: every placeholder spelling — PostgreSQL `$1`, ODBC/JDBC `?`,
209    /// Oracle/SQLite `:name`, and T-SQL `@name`. The sigils are lookahead-disjoint, so
210    /// enabling all four is a pure addition. `@name` keeps its parameter meaning under
211    /// `LENIENT` (`named_at`), so `user_variables` stays off in
212    /// [`SessionVariableSyntax::LENIENT`] — the two claim the same `@name` trigger — but
213    /// the disjoint `@@sysvar` form is still admitted there.
214    ///
215    /// The SQLite `$name` form (`named_dollar`) is the one placeholder left off: it
216    /// contends with the PostgreSQL `$tag$…$tag$` dollar-quote this union already
217    /// admits ([`StringLiteralSyntax::LENIENT`], both lead with `$`+identifier), so the
218    /// union resolves that `$` trigger to dollar-quoting — the spelled-out
219    /// conflict-resolution the `LENIENT` honesty bar requires.
220    pub const LENIENT: Self = Self {
221        positional_dollar: true,
222        anonymous_question: true,
223        named_colon: true,
224        named_at: true,
225        named_dollar: false,
226        // Union widening: SQLite's numbered `?NNN` parameter is follow-set-disjoint from the
227        // anonymous `?` (it needs a digit), a pure accept-side addition.
228        numbered_question: true,
229    };
230}
231
232impl SessionVariableSyntax {
233    /// `LENIENT`: the `@@[scope.]name` MySQL system-variable form, additive over the
234    /// `@name` parameter that [`ParameterSyntax::LENIENT`] already lexes (`@@` is
235    /// disjoint from `@name` by its second `@`). `user_variables` stays *off*: `@name`
236    /// is claimed as a named-at parameter here, and the two meanings cannot both hold
237    /// the trigger (a [`LexicalConflict`](super::LexicalConflict)).
238    pub const LENIENT: Self = Self {
239        user_variables: false,
240        system_variables: true,
241        // The MySQL variable-assignment `SET` grammar (and its `:=` operator) stays *off*
242        // here: LENIENT lexes `@name` as a *parameter* (`user_variables` off, above), so the
243        // user-variable `SET @v = …` item cannot lex anyway, and keeping the generic
244        // PostgreSQL `SET` avoids reshaping every `SET x = …` in the superset. A MySQL-only
245        // behaviour, enabled solely by the fitted `MySql` preset.
246        variable_assignment: false,
247    };
248}
249
250impl IdentifierSyntax {
251    /// `LENIENT`: accept `$` as an identifier-continue byte (PostgreSQL/MySQL). A *leading*
252    /// `$`+digit still dispatches to the parameter form — `$` only continues an
253    /// identifier, it does not start one — so this does not conflict with rule 3.
254    pub const LENIENT: Self = Self {
255        dollar_in_identifiers: true,
256        // The permissive union admits SQLite's string-literal identifier in the two
257        // corpus-admitted name positions (relation target, `PRIMARY KEY`/`UNIQUE` column
258        // list). Each is position-driven and unambiguous — a bare string is never a valid
259        // literal there — so the union shadows no rival reading (unlike `indexed_by`, whose
260        // contextual `INDEXED` keyword would collide with a table named `indexed`, kept off
261        // here). Matches the `empty_in_list` / `bare_constraint_name` Lenient-on precedent.
262        string_literal_identifiers: true,
263        // Union widening: SQLite accepts an empty quoted identifier in every quote style, so
264        // the permissive union does too (a pure accept-side addition).
265        empty_quoted_identifiers: true,
266    };
267}
268
269impl TableExpressionSyntax {
270    /// The `LENIENT` preset for table expression syntax.
271    pub const LENIENT: Self = Self {
272        only: true,
273        table_sample: true,
274        parenthesized_joins: true,
275        table_alias_column_lists: true,
276        join_using_alias: true,
277        // Accept MySQL's index hints and `PARTITION (…)` selection — additive forms.
278        index_hints: true,
279        // Accept MSSQL's `WITH (...)` table hints — additive form.
280        table_hints: true,
281        partition_selection: true,
282        base_table_alias_column_lists: true,
283        // Accept DuckDB's string-literal table alias (`FROM t AS 't'('k')`) — additive.
284        string_literal_aliases: true,
285        aliased_parenthesized_join: true,
286        // The permissive union keeps the bare table alias on the wide `ColId` set (the
287        // accepting side), so it never sacrifices the SQLite JOIN-keyword-as-alias reading.
288        bare_table_alias_is_bare_label: false,
289        // Accept the BigQuery/MSSQL/Databricks table version / time-travel modifiers —
290        // additive form.
291        table_version: true,
292        // OFF, not additive: the Redshift/Snowflake PartiQL / SUPER table-position path is
293        // entered on a `[` after a table name, but Lenient claims `[` for its bracket
294        // identifier quote (like `subscript` / `collection_literals` above, kept off for the
295        // same reason). Enabling it would trip the
296        // `BracketIdentifierVersusArraySyntax` lexical conflict, so Lenient forgoes the path
297        // to keep bracket identifiers.
298        table_json_path: false,
299        // OFF, not additive: turning on SQLite's `INDEXED BY` directive would decline a bare
300        // `INDEXED` as a correlation alias at the base-table position (so `FROM t indexed`
301        // rejects), and the directive reading versus the bare-alias reading are mutually
302        // exclusive given the keyword's one-position semantics. Lenient's maximal-accept goal
303        // prefers the more permissive bare-alias reading, so it forgoes the directive to keep
304        // `indexed` an ordinary alias everywhere.
305        indexed_by: false,
306    };
307}
308
309impl JoinSyntax {
310    /// The `LENIENT` preset for join syntax.
311    pub const LENIENT: Self = Self {
312        stacked_join_qualifiers: true,
313        full_outer_join: true,
314        // Accept SQLite's `NATURAL CROSS JOIN` — an additive form led by the reserved
315        // `NATURAL` keyword, normalized into the canonical natural-inner shape.
316        natural_cross_join: true,
317        // `LENIENT` accepts every additive form, including MySQL's `STRAIGHT_JOIN`.
318        straight_join: true,
319        // Accept DuckDB's `ASOF`/`POSITIONAL` joins — additive keyword-led forms with
320        // no tokenizer trigger. The words stay unreserved (conflict-resolution rule 5
321        // keeps the ANSI reserved model), so on a bare table factor the alias reading
322        // wins (`FROM l ASOF JOIN r …` reads `asof` as `l`'s alias, then a plain
323        // join), exactly as before these flags; the join parses where no alias can
324        // claim the word (after an explicit alias, `FROM l AS a ASOF JOIN r …`).
325        // DuckDB itself reserves both words, so the bare-factor spelling needs the
326        // DuckDb preset — the same reserved-model sacrifice documented for `QUALIFY`.
327        asof_join: true,
328        positional_join: true,
329        semi_anti_join: true,
330        // Accept Spark/Hive's sided `{LEFT|RIGHT} {SEMI|ANTI} JOIN` — an additive form
331        // led by the reserved `LEFT`/`RIGHT` keyword, so no alias-model sacrifice; a
332        // separate gate from `semi_anti_join` because DuckDb rejects the sided spelling.
333        sided_semi_anti_join: true,
334        // Accept MSSQL's `CROSS`/`OUTER APPLY` — an additive form led by the reserved
335        // `CROSS`/`OUTER` keyword, so no alias-model sacrifice like the `ASOF` pair
336        // above (the leading keyword already anchors the operator).
337        apply_join: true,
338        // Accept the SQL:2023 recursive-query SEARCH/CYCLE clauses (the accepting side).
339        recursive_search_cycle: true,
340        // Keep the recursive-CTE UNION modifier restriction off (the accepting side).
341        recursive_union_rejects_order_limit: false,
342        // Accept DuckDB's `USING KEY` recursive-CTE key clause (the accepting side); the
343        // leading `USING` sits before `AS`, shadowing no other spelling.
344        recursive_using_key: true,
345    };
346}
347
348impl TableFactorSyntax {
349    /// The `LENIENT` preset for table factor syntax.
350    pub const LENIENT: Self = Self {
351        lateral: true,
352        table_functions: true,
353        rows_from: true,
354        // The permissive superset admits the first-class UNNEST factor; `WITH OFFSET`
355        // stays preset-less (no oracle-backed dialect enables it).
356        unnest: true,
357        unnest_with_offset: false,
358        table_function_ordinality: true,
359        // Accept a special value function as a `FROM` source and an alias on a parenthesized
360        // join — additive (the permissive union takes the accepting side).
361        special_function_table_source: true,
362        // Accept DuckDB's PIVOT/UNPIVOT operators — additive forms.
363        pivot: true,
364        unpivot: true,
365        // Accept DuckDB's DESCRIBE/SHOW/SUMMARIZE table source — an additive form.
366        show_ref: true,
367        // Accept DuckDB's bare `FROM VALUES (…) AS t` row-list table factor — additive.
368        from_values: true,
369        // Accept the SQL/JSON JSON_TABLE and SQL/XML XMLTABLE table factors (accepting side).
370        json_table: true,
371        xml_table: true,
372        // Accept `TABLE(<expr>)` (Snowflake/Oracle's factor, the accepting side): no
373        // oracle-backed preset enables it, so it stays a Lenient-only permissive extra.
374        table_expr_factor: true,
375        // Accept the standard PIVOT's extended value sources (`ANY`, subquery) and
376        // `DEFAULT ON NULL` — the accepting side of the permissive superset.
377        pivot_value_sources: true,
378        // Accept the SQL:2016 MATCH_RECOGNIZE table factor — the accepting side of the
379        // permissive superset (also on for Snowflake).
380        match_recognize: true,
381        // Accept SQL Server's OPENJSON table factor — the accepting side of the permissive
382        // superset (also on for MSSQL).
383        open_json: true,
384    };
385}
386
387impl ExpressionSyntax {
388    /// `LENIENT`: every PostgreSQL postfix/constructor form *except* the `[`-punctuation
389    /// forms.
390    ///
391    /// `subscript`, `array_constructor`, and `collection_literals` are OFF because `[`
392    /// is a bracket identifier-quote opener here (conflict-resolution rule 2): the
393    /// tokenizer claims `[` before the parser can read it as array punctuation.
394    /// Everything that does not need `[` (typecast `::`, `COLLATE`, `AT TIME ZONE`,
395    /// `ROW(...)`, field selection, typed literals) is ON.
396    pub const LENIENT: Self = Self {
397        typecast_operator: true,
398        subscript: false,
399        // `[` is a bracket identifier quote here, so subscripting is off and the three-bound
400        // slice (a subscript extension) cannot apply either.
401        slice_step: false,
402        collate: true,
403        at_time_zone: true,
404        semi_structured_access: false,
405        array_constructor: false,
406        multidim_array_literals: false,
407        collection_literals: false,
408        row_constructor: true,
409        // BigQuery's `STRUCT(...)` value constructor: additive over ANSI (the `STRUCT`
410        // word only opens it before `(`/`<`, otherwise stays an ordinary call/name), so
411        // the permissive union admits it.
412        struct_constructor: true,
413        field_selection: true,
414        field_wildcard: true,
415        typed_string_literals: true,
416        // Lenient is maximally permissive: it keeps the ANSI prefix-typed interval literal
417        // (`INTERVAL '1' HOUR TO SECOND`, the unit-less `INTERVAL '1'`) that MySQL lacks, so
418        // the operator reader's declined ANSI spellings still parse via this literal path.
419        typed_interval_literal: true,
420        // Lenient accepts DuckDB's relaxed interval spellings too — a purely additive
421        // superset over the standard quoted form.
422        relaxed_interval_syntax: true,
423        mysql_interval_operator: true,
424        // DuckDB's `#n` positional column reference is OFF here: `#` is already claimed by
425        // the MySQL-style line comment (`line_comment_hash` on), so the positional form
426        // would be the `HashCommentVersusPositionalColumn` sacrifice — Lenient keeps `#`
427        // a comment (conflict-resolution rule 8, the same call it makes for `#`-XOR).
428        positional_column: false,
429        lambda_keyword: true,
430    };
431}
432
433impl OperatorSyntax {
434    /// `LENIENT`: the explicit-operator construct, both SQLite equality spellings, MySQL
435    /// `<=>`, the bitwise family, and quantified comparisons — all pure additions with no
436    /// contended trigger. The PostgreSQL `@`-family operators (`<@`/`->`/`->>`) stay off:
437    /// `LENIENT` enables the `@name` session-variable read
438    /// ([`SessionVariableSyntax::LENIENT`]), which claims the same `@`+identifier trigger
439    /// the prefix `@` absolute-value operator would, so a permissive superset gaining the
440    /// `@`-family is a scoped follow-up.
441    pub const LENIENT: Self = Self {
442        operator_construct: true,
443        containment_operators: false,
444        json_arrow_operators: false,
445        // OFF, not on principle but by conflict: the `?`-led members share the `?` trigger
446        // with `anonymous_question` (on in this union), and the `@@`/`@?` members ride the
447        // `@` family held off above — so the `jsonb` operators cannot join the parse-anything
448        // union without shadowing the placeholder. Held off with the `@`-family, like `->`.
449        jsonb_operators: false,
450        // SQLite's `==` and general `IS` are pure additions — no shared trigger, and
451        // each only widens what parses — so the parse-anything union admits both.
452        double_equals: true,
453        // DuckDB's `//` integer-division spelling — a pure addition (no shared trigger; no
454        // preset lexes `//` as a comment), so the parse-anything union admits it.
455        integer_divide_slash: true,
456        starts_with_operator: false,
457        is_general_equality: true,
458        // The standard truth-value tests (F571); the parser checks them ahead of the
459        // general-equality reading, so the superset admits `IS UNKNOWN` as the predicate
460        // while `is_general_equality` still covers SQLite's `IS <expr>`.
461        truth_value_tests: true,
462        // Accept MySQL's `<=>` (no lexical conflict with `<=`/`<>`) in the superset.
463        null_safe_equals: true,
464        // OFF, not on principle but by dependency: the lambda gate re-reads a `->`
465        // token that only `json_arrow_operators` lexes, and that flag is off here
466        // (held with the `@`-family above), so enabling this would be dead data —
467        // `a -> b` does not tokenize under `LENIENT` at all. It follows the arrows:
468        // the same scoped follow-up that admits `->`/`->>` decides this one.
469        lambda_expressions: false,
470        // The shared bitwise `| & ~ << >>` family is a pure addition (no contended
471        // trigger), so the parse-anything union admits it. Bitwise XOR's `^` spelling is on
472        // via `caret_operator` on the preset below (conflict-resolution rule 8).
473        bitwise_operators: true,
474        quantified_comparisons: true,
475        quantified_comparison_lists: true,
476        // The permissive union carries PostgreSQL's any-operator quantifier.
477        quantified_arbitrary_operator: true,
478        // OFF, not on principle but by conflict: the general operator surface adds the bare
479        // `@` operator, which shares the `@` trigger with the `@name`/`@@name` sigils this
480        // union keeps (`named_at`/`system_variables` on) — the tracked
481        // `LexicalConflict::CustomOperatorVersusAtName` / `CustomOperatorVersusSystemVariable`.
482        // Held off with the rest of the `@`-family (containment/`jsonb`/`->`) for the same
483        // reason: the union picks the sigils over the `@`-lead operators.
484        custom_operators: false,
485        null_test_postfix: true,
486        // ON as a pure additive parser position: a trailing symbolic operator with no operand
487        // conflicts with nothing, so the parse-anything union admits DuckDB's postfix reading.
488        // Only the always-lexed `Op` tokens (`!`/`~`/the bitwise family) reach it here —
489        // `custom_operators` is held off above for the `@`-sigil conflict, so the `Custom`
490        // residue does not tokenize under this preset.
491        postfix_operators: true,
492    };
493}
494
495impl CallSyntax {
496    /// The `LENIENT` preset for call syntax.
497    pub const LENIENT: Self = Self {
498        named_argument: true,
499        utc_special_functions: true,
500        columns_expression: true,
501        extract_from_syntax: true,
502        try_cast: true,
503        // `LENIENT` accepts every cast target; it never narrows to MySQL's `cast_type`.
504        restricted_cast_targets: false,
505        // The DuckDB call tails — quoted `EXTRACT` field, dot-method chaining, and
506        // in-parenthesis null-treatment — are pure additions with no contended trigger.
507        extract_string_field: true,
508        method_chaining: true,
509        sqljson_constructors_require_argument: false,
510        // Lenient admits the full SQL/JSON expression-function grammar; the empty
511        // constructor floor stays off (the arity-floor flag above is false), so
512        // `JSON()`/`JSON_SCALAR()`/`JSON_SERIALIZE()` fall back to ordinary niladic calls.
513        sqljson_expression_functions: true,
514        // Lenient admits the full SQL/XML expression-function grammar.
515        xml_expression_functions: true,
516        variadic_argument: true,
517        // Lenient inherits PostgreSQL's `merge_action()` support function.
518        merge_action_function: true,
519        convert_function: true,
520    };
521}
522
523impl StringFuncForms {
524    /// The `LENIENT` preset for string func forms.
525    pub const LENIENT: Self = Self {
526        // Lenient admits every additive string special form — the full PostgreSQL
527        // substring/position/overlay/trim surface plus MySQL's SUBSTR keyword head —
528        // and none of the restrictions (no plain-call arity floor, no
529        // PLACING-required overlay, symmetric b_expr POSITION operands, the loose
530        // trim_list tails on).
531        substring_from_for: true,
532        substring_leading_for: true,
533        substring_similar: true,
534        substring_plain_call_requires_2_or_3_args: false,
535        substr_from_for: true,
536        position_in: true,
537        position_asymmetric_operands: false,
538        overlay_placing: true,
539        overlay_requires_placing: false,
540        trim_from: true,
541        trim_list_syntax: true,
542        // Lenient inherits PostgreSQL's `COLLATION FOR (<expr>)` common-subexpr.
543        collation_for_expression: true,
544        // Lenient accepts sqlparser-rs's `CEIL(x TO field)` parity surface — no probed
545        // oracle engine's grammar admits it.
546        ceil_to_field: true,
547        // Lenient accepts sqlparser-rs's `FLOOR(x TO field)` parity surface — no probed
548        // oracle engine's grammar admits it.
549        floor_to_field: true,
550        // Lenient is a permissive superset — accept MySQL's `MATCH (…) AGAINST (…)`.
551        match_against: true,
552    };
553}
554
555impl AggregateCallSyntax {
556    /// The `LENIENT` preset for aggregate call syntax.
557    pub const LENIENT: Self = Self {
558        group_concat_separator: true,
559        within_group: true,
560        aggregate_filter: true,
561        // Lenient is the permissive superset: it admits both the standard `FILTER (WHERE …)`
562        // and DuckDB's keyword-less `FILTER (…)`.
563        filter_optional_where: true,
564        // `LENIENT` is maximally permissive: it never makes a space before `(` significant,
565        // so both `COUNT ( * )` and the adjacent `COUNT(*)` parse.
566        aggregate_args_require_adjacent_paren: false,
567        null_treatment: true,
568        // Lenient is maximally permissive: it admits empty aggregate calls and `OVER` on
569        // any function, so neither MySQL-only restriction fires.
570        aggregate_calls_reject_empty_arguments: false,
571        over_requires_windowable_function: false,
572        window_function_tail: false,
573        standalone_argument_order_by: true,
574    };
575}
576
577impl PredicateSyntax {
578    /// The `LENIENT` preset for predicate syntax.
579    pub const LENIENT: Self = Self {
580        like: true,
581        ilike: true,
582        similar_to: true,
583        // The permissive union carries the standard `OVERLAPS` period predicate.
584        overlaps_period_predicate: true,
585        // The permissive union accepts DuckDB's unparenthesized `IN <value>` too; it does
586        // not contend with the standard `IN (list)` (the `(` lookahead splits them).
587        unparenthesized_in_list: true,
588        // The permissive union carries PostgreSQL's `LIKE/ILIKE ANY|ALL (array)`.
589        pattern_match_quantifier: true,
590        between_symmetric: true,
591        is_normalized: true,
592        // The permissive union carries SQLite's empty `IN ()` list.
593        empty_in_list: true,
594        // The permissive union carries the SQLite/DuckDB two-word `<expr> NOT NULL` postfix.
595        null_test_two_word_postfix: true,
596    };
597}
598
599impl MutationSyntax {
600    /// The `LENIENT` preset for mutation syntax.
601    pub const LENIENT: Self = Self {
602        returning: true,
603        on_conflict: true,
604        on_duplicate_key_update: true,
605        multi_column_assignment: true,
606        update_tuple_value_row_arity: false,
607        where_current_of: true,
608        merge: true,
609        // The permissive union also accepts the MySQL `REPLACE` statement and the
610        // `INSERT`/`REPLACE ... SET` source — both additive, distinct keyword triggers.
611        replace_into: true,
612        insert_set: true,
613        // The MySQL single-table `UPDATE`/`DELETE ... ORDER BY ... LIMIT` tails are
614        // additive (trailing clauses in a distinct position), so the union accepts them.
615        update_delete_tails: true,
616        // The SQLite `INSERT OR`/`UPDATE OR <action>` prefix is additive (a distinct `OR`
617        // trigger after the verb), so the permissive union accepts it.
618        or_conflict_action: true,
619        insert_column_matching: true,
620        delete_using: true,
621        update_from: true,
622        // Accept an alias on a `DELETE … USING` target and a leading `WITH` before
623        // `INSERT` or `MERGE` — additive.
624        delete_using_target_alias: true,
625        cte_before_insert: true,
626        cte_before_merge: true,
627        // Data-modifying CTE bodies are additive (distinct DML keyword triggers inside
628        // `AS (…)`), so the permissive union accepts them.
629        data_modifying_ctes: true,
630        // The MERGE residual grammar (`WHEN NOT MATCHED BY SOURCE/TARGET`, `INSERT
631        // DEFAULT VALUES`, and the `OVERRIDING` merge insert override) is additive, so
632        // the permissive union accepts all three.
633        merge_when_not_matched_by: true,
634        merge_insert_default_values: true,
635        merge_insert_overriding: true,
636        merge_update_set_star: true,
637        merge_insert_star_by_name: true,
638        merge_error_action: true,
639        update_set_qualified_column: true,
640    };
641}
642
643impl StatementDdlGates {
644    /// The `LENIENT` preset for statement ddl gates.
645    pub const LENIENT: Self = Self {
646        // The parse-anything union accepts the SQLite `CREATE TRIGGER` body form too.
647        create_trigger: true,
648        // …and the DuckDB `CREATE MACRO`/live-body `FUNCTION` macro DDL.
649        create_macro: true,
650        create_secret: true,
651        // …and DuckDB's `CREATE`/`DROP TYPE` user-defined-type DDL.
652        create_type: true,
653        // …and SQLite's `CREATE VIRTUAL TABLE … USING <module>(<args>)`.
654        create_virtual_table: true,
655        // …and the PostgreSQL/DuckDB `CREATE`/`DROP SEQUENCE` T176 generator.
656        create_sequence: true,
657        extension_ddl: true,
658        transform_ddl: true,
659        alter_system: true,
660        // MySQL's tablespace / logfile-group storage DDL — a pure addition in the union: the
661        // leading `TABLESPACE`/`LOGFILE`/`UNDO` keywords collide with no other statement.
662        tablespace_ddl: true,
663        logfile_group_ddl: true,
664        schemas: true,
665        // Lenient is the union of every real dialect's surface, so PostgreSQL's
666        // embedded schema-element form is on here too (no-shadowing doctrine).
667        schema_elements: true,
668        databases: true,
669        // Conflict resolution: `drop_database` would recast `DROP SCHEMA` as MySQL's
670        // single-name synonym drop and forfeit the more permissive PostgreSQL/DuckDB
671        // name-list-plus-`CASCADE` `DROP SCHEMA`. LENIENT keeps the name-list path and
672        // forgoes the MySQL `DROP DATABASE` spelling.
673        drop_database: false,
674        materialized_views: true,
675        temporary_views: true,
676        routines: true,
677        or_replace: true,
678        // …and DuckDB's `CREATE [OR REPLACE] [TEMP] RECURSIVE VIEW` (no-shadowing
679        // doctrine: the union carries every real dialect's surface).
680        recursive_views: true,
681        // The permissive union carries MySQL's compound-statement body grammar.
682        compound_statements: true,
683        alter_database: true,
684        alter_database_options: true,
685        server_definition: true,
686        alter_instance: true,
687        spatial_reference_system: true,
688        resource_group: true,
689        alter_sequence: true,
690        alter_object_set_schema: true,
691        view_definition_options: true,
692    };
693}
694
695impl CreateTableClauseSyntax {
696    /// The `LENIENT` preset for create table clause syntax.
697    pub const LENIENT: Self = Self {
698        table_options: true,
699        // The parse-anything union accepts the SQLite trailing `WITHOUT ROWID` table
700        // option — additive over the shared surface.
701        without_rowid_table_option: true,
702        // The parse-anything union accepts the SQLite trailing `STRICT` table option —
703        // additive over the shared surface.
704        strict_table_option: true,
705        // …and DuckDB's `CREATE OR REPLACE TABLE` / `CREATE [PERSISTENT] SECRET`.
706        create_or_replace_table: true,
707        storage_parameters: true,
708        on_commit: true,
709        create_table_as_with_data: true,
710        create_table_as_execute: true,
711        // Lenient is the permissive superset — accept the PostgreSQL partitioning grammar.
712        declarative_partitioning: true,
713        // The permissive superset accepts the legacy inheritance clause and the LIKE element.
714        table_inheritance: true,
715        like_source_table: true,
716        // …and MySQL's statement-level table-clone body (the bare `LIKE src` form; the
717        // parenthesized `(LIKE …)` reads as the PostgreSQL element superset when both are on).
718        statement_level_table_like: true,
719        unlogged_tables: true,
720        table_access_method: true,
721        without_oids: true,
722        typed_tables: true,
723    };
724}
725
726impl ColumnDefinitionSyntax {
727    /// The `LENIENT` preset for column definition syntax.
728    pub const LENIENT: Self = Self {
729        // The union accepts the keywordless generated-column `AS (…)` shorthand and the
730        // SQLite `CREATE TABLE` decoration cluster — all additive.
731        generated_column_shorthand: true,
732        // The parse-anything union accepts the SQLite column-level `ON CONFLICT
733        // <resolution>` clause — additive over the shared surface.
734        column_conflict_resolution_clause: true,
735        // The parse-anything union accepts the SQLite typeless column definition too.
736        typeless_column_definitions: true,
737        // The parse-anything union accepts DuckDB's type-optional generated column as well
738        // (subsumed by the wider typeless rule above, but flagged on for completeness).
739        typeless_generated_columns: true,
740        // The parse-anything union accepts the SQLite joined `AUTOINCREMENT` attribute —
741        // additive over the shared surface.
742        joined_autoincrement_attribute: true,
743        // The parse-anything union accepts the SQLite inline-`PRIMARY KEY` `ASC`/`DESC`
744        // ordering too — additive over the shared surface.
745        inline_primary_key_ordering: true,
746        // The parse-anything union accepts the SQLite `CONSTRAINT <name>` prefix on a column
747        // `COLLATE` too — additive over the bare column COLLATE surface.
748        named_column_collate_constraint: true,
749        identity_columns: true,
750        // Accept a bare expression default and a `CONSTRAINT <name>` on any inline column
751        // constraint — the permissive union never adds the MySQL restriction.
752        default_expression_requires_parens: false,
753        column_default_requires_b_expr: false,
754        // Lenient is the permissive superset: every CREATE TABLE residue surface is on.
755        column_collation: true,
756        column_storage: true,
757    };
758}
759
760impl ConstraintSyntax {
761    /// The `LENIENT` preset for constraint syntax.
762    pub const LENIENT: Self = Self {
763        deferrable_constraints: true,
764        named_inline_non_check_constraints: true,
765        // Lenient is the permissive superset — accept SQLite's bodyless `CONSTRAINT <name>`.
766        bare_constraint_name: true,
767        exclusion_constraints: true,
768        constraint_no_inherit_not_valid: true,
769        index_constraint_parameters: true,
770        constraint_column_collate_order: true,
771        referential_action_cascade_set: true,
772        check_constraint_subqueries: true,
773    };
774}
775
776impl IndexAlterSyntax {
777    /// The `LENIENT` preset for index alter syntax.
778    pub const LENIENT: Self = Self {
779        drop_behavior: true,
780        // Conflict resolution: `index_drop_on_table`'s mandatory-`ON` MySQL form would displace
781        // the shared bare-name `DROP INDEX <name> [, …]`. LENIENT keeps the more permissive
782        // name-list drop and forgoes the MySQL `DROP INDEX … ON <table>` form.
783        index_drop_on_table: false,
784        index_concurrently: true,
785        index_using_method: true,
786        partial_index: true,
787        index_if_not_exists: true,
788        index_nulls_order: true,
789        alter_table_extended: true,
790        alter_nested_column_paths: true,
791        alter_existence_guards: true,
792        alter_column_set_data_type: true,
793        routine_arg_types: true,
794        routine_arg_defaults: true,
795        routine_arg_modes: true,
796        // The permissive superset admits the PostgreSQL string-constant `LANGUAGE` spelling.
797        routine_language_string: true,
798        alter_table_multiple_actions: true,
799    };
800}
801
802impl ExistenceGuards {
803    /// The `LENIENT` preset for existence guards.
804    pub const LENIENT: Self = Self {
805        if_exists: true,
806        view_if_not_exists: true,
807        create_database_if_not_exists: true,
808    };
809}
810
811impl SelectSyntax {
812    /// The `LENIENT` preset for select syntax.
813    pub const LENIENT: Self = Self {
814        distinct_on: true,
815        select_into: true,
816        // Accept the empty target list (`SELECT`, `SELECT FROM t`) — a pure addition.
817        empty_target_list: true,
818        // Accept DuckDB's `QUALIFY` clause — a pure acceptance addition. `QUALIFY`
819        // stays unreserved here (conflict-resolution rule 5 keeps the ANSI reserved
820        // model), so in a position where a bare alias is legal (`SELECT 1 qualify`,
821        // `FROM t QUALIFY …`) the alias reading wins, exactly as before this flag;
822        // the clause parses where no alias can claim the word (after a GROUP
823        // BY/HAVING/WINDOW clause or a non-aliasable expression). DuckDB itself
824        // reserves the word, so its `FROM t QUALIFY …` spelling needs the DuckDb
825        // preset, not `LENIENT` — the same reserved-model sacrifice rule 5 documents
826        // for `OFFSET`.
827        qualify: true,
828        // Accept MySQL's string-literal column aliases in the permissive superset.
829        alias_string_literals: true,
830        // Union widening: accept SQLite's bare (`AS`-less) string alias (`SELECT 1 'x'`) too.
831        bare_alias_string_literals: true,
832        // Accept DuckDB's `UNION [ALL] BY NAME` name-matched set operation — a pure
833        // acceptance addition. Conflict-free: `BY` after a set operator opens no other
834        // grammar, so admitting it shadows no existing reading.
835        union_by_name: true,
836        wildcard_modifiers: true,
837        // Pure-accept superset: admit the PostgreSQL/DuckDB qualified-wildcard alias too.
838        qualified_wildcard_alias: true,
839        // Accept DuckDB's FROM-first SELECT (`FROM t SELECT x`, bare `FROM t`) — a pure
840        // acceptance addition, conflict-free because `FROM` is reserved in the ANSI model
841        // rule 5 keeps, so a leading `FROM` can never be a bare column/alias.
842        from_first: true,
843        parenthesized_query_operands: true,
844        // `LENIENT` is a pure-acceptance superset, so it does *not* enforce DuckDB's
845        // parse-time equal-arity reject — a ragged VALUES constructor stays accepted here.
846        values_rows_require_equal_arity: false,
847        // A pure-acceptance superset admits the bare-parenthesized query-position VALUES
848        // constructor every non-MySQL dialect spells.
849        values_row_constructor: true,
850        // LENIENT is a pure-acceptance superset, so the projection `AS` alias admits
851        // reserved words (no reroute to the stricter bare-alias set).
852        as_alias_rejects_reserved: false,
853        // A pure-acceptance superset admits DuckDB's trailing-comma list tolerance.
854        trailing_comma: true,
855        // DuckDB's prefix colon alias (`SELECT j : 42`, `FROM b : a`): a conflict-free
856        // pure-acceptance addition the lenient charter admits. `semi_structured_access` is
857        // off here, so nothing else claims the `<ident> :` head — the one construct it
858        // would collide with — and a `:` at a select-item / table-factor head was
859        // otherwise a clean reject, so turning it on only widens acceptance.
860        prefix_colon_alias: true,
861        // Hive/Spark `LATERAL VIEW`: a conflict-free pure-acceptance addition the
862        // lenient charter admits. It shares the `LATERAL` lead with the derived-table
863        // factor (`table_factor_syntax.lateral`, also on here), but the two occupy
864        // disjoint grammar positions (a table-factor head vs after the complete FROM
865        // list) and split on the `VIEW` follow token, so each declines the other's
866        // `LATERAL` and the union stays unambiguous — the property that makes enabling
867        // both conflict-free.
868        lateral_view_clause: true,
869        // The Oracle-style `START WITH`/`CONNECT BY` hierarchical query clause: a
870        // conflict-free pure-acceptance addition the lenient charter admits. It parses
871        // only after `WHERE` (a position no other clause claims), and its `PRIOR` operator
872        // is scoped to the `CONNECT BY` condition, so it shadows no existing reading —
873        // `START WITH`/`CONNECT BY`/`PRIOR`/`NOCYCLE` stay ordinary identifiers everywhere
874        // else, exactly as before this flag.
875        connect_by_clause: true,
876    };
877}
878
879impl QueryTailSyntax {
880    /// The `LENIENT` preset for query tail syntax.
881    pub const LENIENT: Self = Self {
882        fetch_first: true,
883        limit_offset_comma: true,
884        // Accept the query-tail row-locking clauses (`FOR UPDATE`/`FOR SHARE`, MySQL's
885        // `LOCK IN SHARE MODE`) — a pure acceptance addition.
886        locking_clauses: true,
887        // Accept PostgreSQL's `NO KEY UPDATE`/`KEY SHARE` strengths and stacked clauses —
888        // pure acceptance additions over the shared locking core.
889        key_lock_strengths: true,
890        stacked_locking_clauses: true,
891        using_sample: true,
892        leading_offset: true,
893        limit_expressions: true,
894        // A pure-acceptance superset: DuckDB's percentage `LIMIT` (`LIMIT 40 PERCENT` /
895        // `LIMIT 35%`) is admitted alongside every other dialect's `LIMIT` surface.
896        limit_percent: true,
897        with_ties_requires_order_by: false,
898        // BigQuery/ZetaSQL `|>` pipe syntax stays OFF here *for now*, the one deliberate
899        // exception to "admit every conflict-free pure-acceptance form". The `|>` munch is
900        // feature-gated so it shadows nothing (conflict-free), and the charter would
901        // otherwise admit it — but the framework ships only the reference `WHERE` operator,
902        // so enabling it now would make the "parse anything" preset accept `|> WHERE` while
903        // rejecting every other pipe operator: a fragment a reader of this module could not
904        // predict, breaking the honesty bar. Flip it on as a pure-acceptance addition once
905        // the `planner-parity-pipe-*` operator surface is coherent.
906        pipe_syntax: false,
907        // ClickHouse `LIMIT n [OFFSET m] BY …` per-group limiting: a conflict-free
908        // pure-acceptance addition the lenient charter admits (a plain `LIMIT n` still
909        // parses unchanged; only a trailing `BY` diverts to the LIMIT BY shape).
910        limit_by_clause: true,
911        // ClickHouse `SETTINGS name = value, …` query tail: a conflict-free
912        // pure-acceptance addition the lenient charter admits (`SETTINGS` is contextual,
913        // so it only diverts at the query tail with the gate on).
914        settings_clause: true,
915        // ClickHouse `FORMAT <name>` query tail: a conflict-free pure-acceptance addition
916        // the lenient charter admits (`FORMAT` is contextual, so it only diverts at the
917        // query tail with the gate on).
918        format_clause: true,
919        // MSSQL `FOR XML`/`FOR JSON` result-shaping tail: a conflict-free pure-acceptance
920        // addition the lenient charter admits. It shares the `FOR` lead with the locking
921        // clauses (also on here), but the two partition on the follow token
922        // (`XML`/`JSON` vs `UPDATE`/`SHARE`/`NO`/`KEY`), so each declines the other's
923        // `FOR` and the union stays unambiguous — the property that makes enabling both
924        // conflict-free.
925        for_xml_json_clause: true,
926    };
927}
928
929impl GroupingSyntax {
930    /// The `LENIENT` preset for grouping syntax.
931    pub const LENIENT: Self = Self {
932        grouping_sets: true,
933        with_rollup: true,
934        // Accept PostgreSQL's operator-driven `USING` sort form (a pure addition).
935        order_by_using: true,
936        // Accept DuckDB's `GROUP BY ALL` / `ORDER BY ALL` clause modes — pure
937        // acceptance additions, conflict-free because `ALL` is reserved in the ANSI
938        // model rule 5 keeps (no dialect reads a bare `all` there as an identifier).
939        group_by_all: true,
940        // PostgreSQL's grouping-set quantifier stays disambiguated from the DuckDB mode
941        // above by lookahead: bare `GROUP BY ALL` is the mode, `GROUP BY ALL <items>` is
942        // the quantifier (an item list follows). Conflict-free superset (see the flag doc).
943        group_by_set_quantifier: true,
944        order_by_all: true,
945    };
946}
947
948impl UtilitySyntax {
949    /// The `LENIENT` preset for utility syntax.
950    pub const LENIENT: Self = Self {
951        copy: true,
952        // Snowflake's `COPY INTO` load/unload — a pure addition on top of the PostgreSQL
953        // `COPY`, dispatched by the `INTO` after `COPY`, in keeping with the permissive
954        // parse-anything union.
955        copy_into: true,
956        stage_references: false,
957        comment_on: true,
958        pragma: true,
959        attach: true,
960        kill: true,
961        // MySQL's `HANDLER` cursor family — a pure addition in the union: the leading
962        // `HANDLER` keyword collides with no other statement, so the superset accepts it.
963        handler_statements: true,
964        // MySQL's `INSTALL`/`UNINSTALL` `PLUGIN`/`COMPONENT` family — a pure addition in the
965        // union: the leading `INSTALL`/`UNINSTALL` keywords collide with no other statement.
966        plugin_component_statements: true,
967        // MySQL's server-administration families — pure additions in the union. The leading
968        // SHUTDOWN/RESTART/CLONE/HELP/BINLOG keywords collide with nothing; IMPORT TABLE and
969        // DuckDB's IMPORT DATABASE (export_import_database) share the `IMPORT` keyword but split
970        // on the second keyword (TABLE vs DATABASE), so both can be on without colliding.
971        shutdown: true,
972        restart: true,
973        clone: true,
974        import_table: true,
975        help_statement: true,
976        binlog: true,
977        // MySQL's `CACHE INDEX` / `LOAD INDEX INTO CACHE` key-cache pair — a pure addition:
978        // leading `CACHE` collides with nothing, and the `LOAD INDEX` lookahead keeps it MECE
979        // against the DuckDB/PostgreSQL `LOAD <extension>` statement the union also admits.
980        key_cache_statements: true,
981        // The `USE` catalog-switch statement — a pure addition (its leading `USE`
982        // contends with nothing at statement position), in keeping with the union.
983        use_statement: true,
984        // Admit the DuckDB dotted `USE catalog.schema` name (the superset direction: it
985        // accepts a strict superset of MySQL's single-ident form).
986        use_qualified_name: true,
987        // The DuckDB prepared-statement lifecycle and `CALL` — pure additions (their
988        // leading keywords `PREPARE`/`EXECUTE`/`DEALLOCATE`/`CALL` contend with nothing),
989        // in keeping with the parse-anything union.
990        prepared_statements: true,
991        // The PostgreSQL `PREPARE name(<type>, …)` typed parameter-type list — a pure
992        // addition on top of `prepared_statements` (a widening of the name position,
993        // contending with nothing), in keeping with the union.
994        prepare_typed_parameters: true,
995        call: true,
996        // MySQL's bare `CALL name` form — a pure addition (a `CALL name` with no `(` contends
997        // with nothing), in keeping with the parse-anything union.
998        call_bare_name: true,
999        load_extension: true,
1000        load_bare_name: true,
1001        load_data: true,
1002        reset_scope: true,
1003        detach_if_exists: true,
1004        // PostgreSQL's `DO` anonymous code block — a pure addition, in keeping with the
1005        // permissive union.
1006        do_statement: true,
1007        // MySQL's `DO <expr-list>` is a DIFFERENT behaviour on the same `DO` keyword, not a
1008        // pure addition: it collides with the PostgreSQL code block on inputs like `DO 'x'`
1009        // and cannot express the `DO LANGUAGE <lang>` clause. The union resolves the one
1010        // keyword to the richer PostgreSQL code-block reading (`do_statement` above), so this
1011        // stays off — the sole non-additive `DO` choice, called out here deliberately.
1012        do_expression_list: false,
1013        // MySQL's `PREPARE ... FROM` / `EXECUTE ... USING` / `{DEALLOCATE|DROP} PREPARE` is a
1014        // DIFFERENT grammar on the same three keywords as DuckDB's typed-`AS`
1015        // `prepared_statements` (on above), not a pure addition: `PREPARE p FROM 'x'` collides
1016        // with `PREPARE p AS <stmt>`, and the `USING @var` / bare-`FROM` surfaces have no
1017        // positional-argument spelling. The union resolves the keywords to the richer DuckDB
1018        // reading, so this stays off — a non-additive choice mirroring `do_expression_list`.
1019        prepared_statements_from: false,
1020        // MySQL's `LOCK/UNLOCK {TABLES|TABLE}` per-table locking — a pure addition *today*
1021        // (no other shipped preset dispatches a leading `LOCK`/`UNLOCK`), in keeping with
1022        // the union. When the PostgreSQL statement-level mode-list reading of `LOCK` lands,
1023        // the union owes the same one-reading decision `DO` got above; that gate does not
1024        // exist yet, so nothing is being resolved away here.
1025        lock_tables: true,
1026        // MySQL's `LOCK INSTANCE FOR BACKUP`/`UNLOCK INSTANCE` backup-lock pair — a pure
1027        // addition (collision-free even against the future PostgreSQL `LOCK` reading, which
1028        // never continues `LOCK instance` with `FOR`).
1029        lock_instance: true,
1030        // SQLite's `BEGIN {DEFERRED|IMMEDIATE|EXCLUSIVE}` transaction-mode modifier — a
1031        // pure addition, in keeping with the union.
1032        begin_transaction_mode: true,
1033        // MySQL's `XA` distributed-transaction family — a pure addition: `XA` is a unique
1034        // leading keyword no other dialect claims, so the union simply admits it.
1035        xa_transactions: true,
1036        // MySQL's standalone `RENAME TABLE`/`RENAME USER` statements — a pure addition.
1037        rename_statement: true,
1038        signal_diagnostics: true,
1039        // DuckDB's `EXPORT`/`IMPORT DATABASE` pair — a pure addition, in keeping with the
1040        // permissive union.
1041        export_import_database: true,
1042        // DuckDB's `UPDATE EXTENSIONS` refresh statement — a pure addition. The `EXTENSIONS`
1043        // lookahead only claims `UPDATE EXTENSIONS [(names)]` at statement end / before the
1044        // list, so the DML `UPDATE` union surface is untouched.
1045        update_extensions: true,
1046        // MySQL's `FLUSH` / `PURGE BINARY LOGS` server-administration statements — armed in
1047        // the permissive superset.
1048        flush: true,
1049        purge_binary_logs: true,
1050        replication_statements: true,
1051    };
1052}
1053
1054impl ShowSyntax {
1055    /// The `LENIENT` preset for show syntax.
1056    pub const LENIENT: Self = Self {
1057        describe: true,
1058        describe_summarize: true,
1059        session_statements: true,
1060        show_tables: true,
1061        show_columns: true,
1062        show_create_table: true,
1063        show_functions: true,
1064        show_routine_status: true,
1065        show_verbose: true,
1066        show_admin: true,
1067    };
1068}
1069
1070impl MaintenanceSyntax {
1071    /// The `LENIENT` preset for maintenance syntax.
1072    pub const LENIENT: Self = Self {
1073        vacuum: true,
1074        // DuckDB's `VACUUM [ANALYZE] <table> (<cols>)` grammar. The parser accepts the
1075        // exact union of the two grammars, never a cross-dialect hybrid (the SQLite `INTO`
1076        // tail is admitted only on a SQLite-shaped prefix — engine-measured, both engines
1077        // reject every hybrid such as `VACUUM ANALYZE t (a) INTO 'f'`). Still NOT a pure
1078        // addition: the shared bare `VACUUM <name>` operand takes the DuckDB reading (the
1079        // *qualified* table) when both gates are on — a one-reading precedence, recorded as
1080        // the `DispatchOrderUnion` `VACUUM` row of
1081        // [`MULTI_CLAIMANT_STATEMENT_HEADS`](super::MULTI_CLAIMANT_STATEMENT_HEADS).
1082        vacuum_analyze: true,
1083        reindex: true,
1084        analyze: true,
1085        // DuckDB's `ANALYZE <table> (<cols>)` column list — a pure addition to the union.
1086        analyze_columns: true,
1087        // The PostgreSQL/DuckDB `CHECKPOINT`/`LOAD` statements and DuckDB's
1088        // `[FORCE] CHECKPOINT [db]` / bare-name `LOAD` / `RESET`-scope /
1089        // `DETACH … IF EXISTS` extensions — pure additions, in keeping with the union.
1090        checkpoint: true,
1091        checkpoint_database: true,
1092        // The MySQL admin-table verb family — a pure addition, in keeping with the union.
1093        table_maintenance: true,
1094    };
1095}
1096
1097impl AccessControlSyntax {
1098    /// The `LENIENT` preset for access control syntax.
1099    pub const LENIENT: Self = Self {
1100        access_control: true,
1101        // A pure-acceptance superset admits the schema-scoped grant objects and the
1102        // `{GRANT|ADMIN} OPTION FOR` prefix.
1103        access_control_extended_objects: true,
1104        // The permissive superset admits the MySQL account-management DDL family.
1105        user_role_management: true,
1106        // Off deliberately: the MySQL account-based grant grammar is a *route* that structurally
1107        // conflicts with (does not extend) the PostgreSQL-extended grant grammar above, so the
1108        // superset cannot enable both — the registered
1109        // `GrammarConflict::AccountGrantsVersusExtendedObjects`. Lenient keeps the richer
1110        // PostgreSQL forms (schema objects,
1111        // `GRANTED BY`, `CASCADE`, routine signatures) that a MySQL route would forfeit.
1112        access_control_account_grants: false,
1113    };
1114}
1115
1116impl TypeNameSyntax {
1117    /// The `LENIENT` preset for type name syntax.
1118    pub const LENIENT: Self = Self {
1119        extended_scalar_type_names: true,
1120        enum_type: true,
1121        set_type: true,
1122        numeric_modifiers: true,
1123        integer_display_width: true,
1124        composite_types: true,
1125        // Accept a length-less `VARCHAR` and the zoned temporal types — the permissive
1126        // union never adds the MySQL length requirement or zoned-type restriction.
1127        varchar_requires_length: false,
1128        zoned_temporal_types: true,
1129        // The permissive superset admits MySQL's `CHARACTER SET`/`ASCII`/`UNICODE`/`BYTE`/
1130        // `BINARY` char-type annotation and DuckDB's empty `DECIMAL()`/`DEC()`/`NUMERIC()`.
1131        empty_type_parens: true,
1132        character_set_annotation: true,
1133        // The permissive superset admits PostgreSQL's signed `numeric` modifier.
1134        signed_type_modifier: true,
1135        // The ClickHouse preset and the permissive union both carry ClickHouse's `Nullable(T)`
1136        // combinator (no differential oracle), the `composite_types` / `format_clause` precedent.
1137        nullable_type: true,
1138        // Same for the sibling `LowCardinality(T)` combinator — ClickHouse/Lenient, no oracle.
1139        low_cardinality_type: true,
1140        // Same for `FixedString(N)`, the fixed-length byte-string constructor — ClickHouse/Lenient,
1141        // no oracle.
1142        fixed_string_type: true,
1143        // Same for `DateTime64(P[, 'tz'])`, the sub-second timestamp constructor —
1144        // ClickHouse/Lenient, no oracle.
1145        datetime64_type: true,
1146        // Same for `Nested(name Type, ...)`, the named-field repeated-group composite —
1147        // ClickHouse/Lenient, no oracle.
1148        nested_type: true,
1149        // Same for the `Int8`…`Int256`/`UInt*` fixed-bit-width integer names — ClickHouse/Lenient,
1150        // no oracle.
1151        bit_width_integer_names: true,
1152        // The permissive superset admits SQLite's liberal multi-word / two-argument affinity
1153        // type names (`LONG INTEGER`, `VARCHAR(123,456)`).
1154        liberal_type_names: true,
1155        string_type_modifiers: false,
1156        angle_bracket_types: true,
1157    };
1158}
1159
1160impl FeatureSet {
1161    /// The optional permissive **tooling** union — see the module-level docs for the
1162    /// full inclusion list and every conflict-resolution rule.
1163    ///
1164    /// This is the "parse anything" mode behind the `lenient` cargo feature, and the
1165    /// honest counterpart to the ban on a "generic" union: it is precise, not a
1166    /// vibe. It is a `const` like every other preset, so the `Lenient` dialect's
1167    /// `features()` hands back a `'static` borrow and the parser's field reads
1168    /// const-fold under `Parser<Lenient>` — zero per-parse cost, same code path as
1169    /// [`ANSI`](FeatureSet::ANSI).
1170    pub const LENIENT: Self = Self {
1171        // Preserve exact text; impose no dialect's fold (conflict-resolution rule 6).
1172        identifier_casing: Casing::Preserve,
1173        // The multi-style union — the one real cost of "parse anything".
1174        identifier_quotes: LENIENT_IDENTIFIER_QUOTES,
1175        // ANSI/PostgreSQL default; semantics only (conflict-resolution rule 7).
1176        default_null_ordering: NullOrdering::NullsLast,
1177        // ANSI position-aware reserved model (conflict-resolution rule 5): keep the
1178        // grammar-disambiguating reserved words; everything marginal is freed or quotable.
1179        reserved_column_name: RESERVED_COLUMN_NAME,
1180        reserved_function_name: RESERVED_FUNCTION_NAME,
1181        reserved_type_name: RESERVED_TYPE_NAME,
1182        reserved_bare_alias: RESERVED_BARE_ALIAS,
1183        // `LENIENT` accepts every additive form: all keywords as ColLabels, and the
1184        // three-part catalog-qualified relation name.
1185        reserved_as_label: KeywordSet::EMPTY,
1186        catalog_qualified_names: true,
1187        // Backtick/bracket quoting, `#`, `&&`, and `$` all dispatch from the standard
1188        // byte classes gated by the knobs below — no bespoke byte-class table is needed,
1189        // and keeping `#` out of the identifier-start class keeps rule (`#`) consistent.
1190        byte_classes: STANDARD_BYTE_CLASSES,
1191        // The permissive union follows PostgreSQL: range/pattern/membership predicates bind
1192        // one tier above comparison, so `a = b BETWEEN c AND d` groups `a = (b BETWEEN c AND
1193        // d)` (see [`BindingPowerTable::range_predicate_override`]).
1194        binding_powers: BindingPowerTable {
1195            range_predicate_override: Some(RANGE_PREDICATE_ABOVE_COMPARISON),
1196            // The `IS`-family predicates rank one tier below comparison (PostgreSQL/DuckDB
1197            // `%nonassoc IS`), so `a <> b IS NULL` groups `(a <> b) IS NULL`. The comparison-tier
1198            // bare `IS`/`<=>` null-safe (in)equality (SQLite/MySQL) is spelling-distinguished
1199            // and unaffected.
1200            is_predicate_override: Some(IS_PREDICATE_BELOW_COMPARISON),
1201            ..STANDARD_BINDING_POWERS
1202        },
1203        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
1204        string_literals: StringLiteralSyntax::LENIENT,
1205        numeric_literals: NumericLiteralSyntax::LENIENT,
1206        parameters: ParameterSyntax::LENIENT,
1207        session_variables: SessionVariableSyntax::LENIENT,
1208        identifier_syntax: IdentifierSyntax::LENIENT,
1209        table_expressions: TableExpressionSyntax::LENIENT,
1210        join_syntax: JoinSyntax::LENIENT,
1211        table_factor_syntax: TableFactorSyntax::LENIENT,
1212        expression_syntax: ExpressionSyntax::LENIENT,
1213        operator_syntax: OperatorSyntax::LENIENT,
1214        call_syntax: CallSyntax::LENIENT,
1215        string_func_forms: StringFuncForms::LENIENT,
1216        aggregate_call_syntax: AggregateCallSyntax::LENIENT,
1217        predicate_syntax: PredicateSyntax::LENIENT,
1218        // `||` is concatenation (conflict-resolution rule 4).
1219        pipe_operator: PipeOperator::StringConcat,
1220        // `&&` as AND — a pure addition over ANSI's "unsupported".
1221        double_ampersand: DoubleAmpersand::LogicalAnd,
1222        // Recognize MySQL's `DIV`/`MOD`/`XOR`/`RLIKE`/`REGEXP` infix operators — additive
1223        // in operator position; they remain free identifiers elsewhere under rule 5.
1224        keyword_operators: KeywordOperators::MySql,
1225        // Bitwise XOR is `^` here (conflict-resolution rule 8): `^` is claimed as the
1226        // MySQL-style XOR operator (so `^` is XOR, not exponentiation), while `#` stays a line
1227        // comment (`line_comment_hash` on in `CommentSyntax::LENIENT`), so the PostgreSQL `#`
1228        // spelling is the sacrifice — the two claim the same `#` trigger
1229        // (`LexicalConflict::HashXorOperatorVersusHashComment`). Precedence follows STANDARD
1230        // (looser than additive), not MySQL's tight `^` rank; a parse-anything union documents
1231        // such precedence sacrifices, and both spellings still parse under the dialect that
1232        // owns each.
1233        caret_operator: CaretOperator::BitwiseXor,
1234        // `#` stays a line comment (above), so it is not the XOR operator here.
1235        hash_bitwise_xor: false,
1236        comment_syntax: CommentSyntax::LENIENT,
1237        mutation_syntax: MutationSyntax::LENIENT,
1238        statement_ddl_gates: StatementDdlGates::LENIENT,
1239        create_table_clause_syntax: CreateTableClauseSyntax::LENIENT,
1240        column_definition_syntax: ColumnDefinitionSyntax::LENIENT,
1241        constraint_syntax: ConstraintSyntax::LENIENT,
1242        index_alter_syntax: IndexAlterSyntax::LENIENT,
1243        existence_guards: ExistenceGuards::LENIENT,
1244        select_syntax: SelectSyntax::LENIENT,
1245        query_tail_syntax: QueryTailSyntax::LENIENT,
1246        grouping_syntax: GroupingSyntax::LENIENT,
1247        utility_syntax: UtilitySyntax::LENIENT,
1248        show_syntax: ShowSyntax::LENIENT,
1249        maintenance_syntax: MaintenanceSyntax::LENIENT,
1250        access_control_syntax: AccessControlSyntax::LENIENT,
1251        type_name_syntax: TypeNameSyntax::LENIENT,
1252        // Render the portable ANSI canonical spellings: `LENIENT` is a parse-anything
1253        // tooling union, not a dialect identity, so it has no PostgreSQL-specific
1254        // output spelling to emit.
1255        target_spelling: TargetSpelling::Ansi,
1256    };
1257}
1258
1259/// The permissive tooling union; prefer [`FeatureSet::LENIENT`] for struct update.
1260pub const LENIENT: FeatureSet = FeatureSet::LENIENT;
1261
1262// Compile-time proof that the union resolves every contested tokenizer trigger to a
1263// single claimant: if a future edit reintroduces a conflict (say, turns
1264// `double_quoted_strings` back on while `"` still quotes identifiers), the build fails
1265// here rather than silently mis-lexing.
1266const _: () = assert!(FeatureSet::LENIENT.is_lexically_consistent());
1267// The two sibling self-consistency registries are ratcheted the same way, so the
1268// parse-entry `debug_assert!` folds all three to dead code for this permissive union: no
1269// refinement flag rides an unset base, and every multi-claimant head it unions resolves by
1270// a documented lookahead/dispatch split rather than an unresolved grammar conflict.
1271const _: () = assert!(FeatureSet::LENIENT.has_satisfied_feature_dependencies());
1272const _: () = assert!(FeatureSet::LENIENT.has_no_grammar_conflict());
1273
1274#[cfg(test)]
1275mod tests {
1276    use super::super::{
1277        Casing, CommentSyntax, ExpressionSyntax, FeatureDelta, FeatureSet, IdentifierQuote,
1278        LexicalConflict, NumericLiteralSyntax, OperatorSyntax, ParameterSyntax, PipeOperator,
1279        SessionVariableSyntax, StringLiteralSyntax, TableExpressionSyntax,
1280    };
1281
1282    #[test]
1283    fn lenient_resolves_every_contested_trigger_to_one_claimant() {
1284        // The executable form of the documented conflict-resolution rules: each shared
1285        // trigger has exactly one claimant, so the union is self-consistent.
1286        assert_eq!(FeatureSet::LENIENT.lexical_conflict(), None);
1287        assert!(FeatureSet::LENIENT.is_lexically_consistent());
1288    }
1289
1290    #[test]
1291    fn lenient_quotes_three_identifier_styles() {
1292        // The multi-quote union: `"`, backtick, and `[`-bracket all open identifiers.
1293        let opens: Vec<char> = FeatureSet::LENIENT
1294            .identifier_quotes
1295            .iter()
1296            .map(|quote| quote.open())
1297            .collect();
1298        assert_eq!(opens, ['"', '`', '[']);
1299        // The bracket style is the only asymmetric one (open `[`, close `]`).
1300        assert_eq!(
1301            FeatureSet::LENIENT.identifier_quotes[2],
1302            IdentifierQuote::Asymmetric {
1303                open: '[',
1304                close: ']'
1305            },
1306        );
1307    }
1308
1309    #[test]
1310    fn conflict_resolution_fields_match_the_documented_rules() {
1311        let lenient = FeatureSet::LENIENT;
1312        // Rule 1: `"` is an identifier, so it is not a string.
1313        assert!(!lenient.string_literals.double_quoted_strings);
1314        // Rule 2: `[` is an identifier, so the `[`-punctuation forms are off.
1315        assert!(!lenient.expression_syntax.subscript);
1316        assert!(!lenient.expression_syntax.array_constructor);
1317        // Rule 3: `$`+digit is a parameter, not money.
1318        assert!(!lenient.numeric_literals.money_literals);
1319        assert!(lenient.parameters.positional_dollar);
1320        // Rule 4: `||` is concatenation.
1321        assert_eq!(lenient.pipe_operator, PipeOperator::StringConcat);
1322        // Rule 6: identity preserves exact text.
1323        assert_eq!(lenient.identifier_casing, Casing::Preserve);
1324    }
1325
1326    #[test]
1327    fn lenient_enables_copy_utility_statement() {
1328        // COPY is an additive utility statement (no contested trigger), so the permissive
1329        // union turns it on — the ANSI baseline gates it off. Bind to locals so the const
1330        // field reads are not flagged by clippy's `assertions_on_constants`.
1331        let lenient = FeatureSet::LENIENT.utility_syntax;
1332        let ansi = FeatureSet::ANSI.utility_syntax;
1333        assert!(lenient.copy);
1334        assert!(!ansi.copy);
1335        assert_ne!(lenient, ansi);
1336    }
1337
1338    #[test]
1339    fn lexical_conflict_detects_each_contested_trigger() {
1340        // Rule 1 violated: `"` claimed by both a string and the identifier quote.
1341        let double_quote =
1342            FeatureSet::LENIENT.with(FeatureDelta::EMPTY.string_literals(StringLiteralSyntax {
1343                double_quoted_strings: true,
1344                ..StringLiteralSyntax::LENIENT
1345            }));
1346        assert_eq!(
1347            double_quote.lexical_conflict(),
1348            Some(LexicalConflict::DoubleQuoteStringVersusIdentifier),
1349        );
1350
1351        // Rule 2 violated: `[` claimed by both the identifier quote and subscript syntax.
1352        let bracket =
1353            FeatureSet::LENIENT.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
1354                subscript: true,
1355                ..ExpressionSyntax::LENIENT
1356            }));
1357        assert_eq!(
1358            bracket.lexical_conflict(),
1359            Some(LexicalConflict::BracketIdentifierVersusArraySyntax),
1360        );
1361
1362        // Rule 2's third claimant: the DuckDB `[…]` list literal contends for the same
1363        // `[` trigger, independently of subscript/array_constructor (both stay off).
1364        let bracket_list =
1365            FeatureSet::LENIENT.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
1366                collection_literals: true,
1367                ..ExpressionSyntax::LENIENT
1368            }));
1369        assert_eq!(
1370            bracket_list.lexical_conflict(),
1371            Some(LexicalConflict::BracketIdentifierVersusArraySyntax),
1372        );
1373
1374        // Rule 2's fourth claimant: the Redshift/Snowflake table-position PartiQL path
1375        // (`FROM src[0].a`) also enters on `[`, so it contends with the bracket identifier
1376        // quote independently of the expression-position `[` grammars (all stay off).
1377        let bracket_table_path = FeatureSet::LENIENT.with(FeatureDelta::EMPTY.table_expressions(
1378            TableExpressionSyntax {
1379                table_json_path: true,
1380                ..TableExpressionSyntax::LENIENT
1381            },
1382        ));
1383        assert_eq!(
1384            bracket_table_path.lexical_conflict(),
1385            Some(LexicalConflict::BracketIdentifierVersusArraySyntax),
1386        );
1387
1388        // Rule 3 violated: `$`+digit claimed by both money and a positional parameter.
1389        let money =
1390            FeatureSet::LENIENT.with(FeatureDelta::EMPTY.numeric_literals(NumericLiteralSyntax {
1391                money_literals: true,
1392                ..NumericLiteralSyntax::LENIENT
1393            }));
1394        assert_eq!(
1395            money.lexical_conflict(),
1396            Some(LexicalConflict::MoneyVersusPositionalDollar),
1397        );
1398
1399        // The strict ANSI baseline plus only a positional parameter is *not* a conflict
1400        // (money stays off), confirming the check is the pair, not either flag alone.
1401        let pg_param = FeatureSet::ANSI.with(FeatureDelta::EMPTY.parameters(ParameterSyntax {
1402            positional_dollar: true,
1403            ..ParameterSyntax::ANSI
1404        }));
1405        assert_eq!(pg_param.lexical_conflict(), None);
1406
1407        // `@name` claimed by both a named-at parameter and a user-variable read.
1408        // `LENIENT` keeps `named_at` on, so turning `user_variables` on contends.
1409        let at_name = FeatureSet::LENIENT.with(FeatureDelta::EMPTY.session_variables(
1410            SessionVariableSyntax {
1411                user_variables: true,
1412                ..SessionVariableSyntax::LENIENT
1413            },
1414        ));
1415        assert_eq!(
1416            at_name.lexical_conflict(),
1417            Some(LexicalConflict::AtNameParameterVersusUserVariable),
1418        );
1419
1420        // `:name` claimed by both a colon parameter and the `a[x:y]` bare-identifier
1421        // slice bound. PostgreSQL has `subscript` on (and does not quote with `[`), so
1422        // turning `named_colon` on there contends — a pairing only a custom delta forms.
1423        let colon_slice =
1424            FeatureSet::POSTGRES.with(FeatureDelta::EMPTY.parameters(ParameterSyntax {
1425                named_colon: true,
1426                ..ParameterSyntax::POSTGRES
1427            }));
1428        assert_eq!(
1429            colon_slice.lexical_conflict(),
1430            Some(LexicalConflict::ColonParameterVersusSliceBound),
1431        );
1432
1433        // The `:`+identifier trigger's third claimant: a collection literal's
1434        // `key: value` separator before a bare-identifier value (`{a: b}`) contends
1435        // with the colon parameter on its own — `subscript` stays off here, so this
1436        // detects the collection grammar, not the slice bound.
1437        let colon_collection = FeatureSet::ANSI.with(
1438            FeatureDelta::EMPTY
1439                .parameters(ParameterSyntax {
1440                    named_colon: true,
1441                    ..ParameterSyntax::ANSI
1442                })
1443                .expression_syntax(ExpressionSyntax {
1444                    collection_literals: true,
1445                    ..ExpressionSyntax::ANSI
1446                }),
1447        );
1448        assert_eq!(
1449            colon_collection.lexical_conflict(),
1450            Some(LexicalConflict::ColonParameterVersusSliceBound),
1451        );
1452
1453        // `<@` claimed by both PostgreSQL's containment operator and an abutting `@name`.
1454        // PostgreSQL has `containment_operators` on, so turning the `@name` parameter on
1455        // contends — the `a<@x` (meaning `a < @x`) reading is shadowed.
1456        let containment_at =
1457            FeatureSet::POSTGRES.with(FeatureDelta::EMPTY.parameters(ParameterSyntax {
1458                named_at: true,
1459                ..ParameterSyntax::POSTGRES
1460            }));
1461        assert_eq!(
1462            containment_at.lexical_conflict(),
1463            Some(LexicalConflict::ContainmentOperatorVersusAtName),
1464        );
1465
1466        // The bare `@` operator (the general operator surface) claimed by both
1467        // `custom_operators` and an abutting `@name` parameter. PostgreSQL has
1468        // `custom_operators` on; turning the `<@` containment off (so the earlier
1469        // `ContainmentOperatorVersusAtName` does not claim the `@` first) and `@name` on
1470        // leaves the bare-`@` operator contending with the sigil.
1471        let custom_at = FeatureSet::POSTGRES.with(
1472            FeatureDelta::EMPTY
1473                .operator_syntax(OperatorSyntax {
1474                    containment_operators: false,
1475                    ..OperatorSyntax::POSTGRES
1476                })
1477                .parameters(ParameterSyntax {
1478                    named_at: true,
1479                    ..ParameterSyntax::POSTGRES
1480                }),
1481        );
1482        assert_eq!(
1483            custom_at.lexical_conflict(),
1484            Some(LexicalConflict::CustomOperatorVersusAtName),
1485        );
1486
1487        // The `@@` operator (the general operator surface, with the `jsonb` family off so
1488        // `@@` is not that match operator) claimed by both `custom_operators` and MySQL's
1489        // `@@name` system variable. Turning the `jsonb` family off (so the earlier
1490        // `JsonbSearchOperatorVersusSystemVariable` does not claim `@@` first) and the system
1491        // variable on leaves the `@@` operator contending with the sigil.
1492        let custom_sysvar = FeatureSet::POSTGRES.with(
1493            FeatureDelta::EMPTY
1494                .operator_syntax(OperatorSyntax {
1495                    jsonb_operators: false,
1496                    ..OperatorSyntax::POSTGRES
1497                })
1498                .session_variables(SessionVariableSyntax {
1499                    system_variables: true,
1500                    ..FeatureSet::POSTGRES.session_variables
1501                }),
1502        );
1503        assert_eq!(
1504            custom_sysvar.lexical_conflict(),
1505            Some(LexicalConflict::CustomOperatorVersusSystemVariable),
1506        );
1507
1508        // The disjoint `@@` system-variable form (LENIENT's default) never contends
1509        // with the `@name` parameter — confirming only the same-trigger pair conflicts.
1510        assert_eq!(FeatureSet::LENIENT.lexical_conflict(), None);
1511
1512        // `#` claimed by both the PostgreSQL `#` XOR operator (`hash_bitwise_xor: true`) and a
1513        // `#` line comment: PostgreSQL sets `hash_bitwise_xor: true` with `line_comment_hash`
1514        // off, so turning the comment on contends — the comment shadows the operator in
1515        // `skip_trivia`.
1516        let hash_xor_and_comment =
1517            FeatureSet::POSTGRES.with(FeatureDelta::EMPTY.comment_syntax(CommentSyntax {
1518                line_comment_hash: true,
1519                ..CommentSyntax::ANSI
1520            }));
1521        assert_eq!(
1522            hash_xor_and_comment.lexical_conflict(),
1523            Some(LexicalConflict::HashXorOperatorVersusHashComment),
1524        );
1525
1526        // The mirror: LENIENT keeps `#` a comment and spells XOR `^` (`CaretOperator::BitwiseXor`),
1527        // so its `#` trigger is uncontended — flipping `#` to the XOR operator is the conflict.
1528        let lenient_hash_xor = FeatureSet::LENIENT.with(FeatureDelta::EMPTY.hash_bitwise_xor(true));
1529        assert_eq!(
1530            lenient_hash_xor.lexical_conflict(),
1531            Some(LexicalConflict::HashXorOperatorVersusHashComment),
1532        );
1533    }
1534}