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