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}