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