Skip to main content

squonk_ast/dialect/
sqlite.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The SQLite dialect preset and its reserved-keyword sets.
5//!
6//! The module is self-contained for feature gating: a build without the `sqlite`
7//! cargo feature compiles none of this preset's data and never depends on a gated
8//! sibling preset.
9
10use super::keyword::Keyword;
11use super::{
12    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
13    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
14    DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
15    IdentifierQuote, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, KeywordOperators, KeywordSet,
16    MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
17    ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax, SQLITE_BYTE_CLASSES,
18    SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates, StringFuncForms,
19    StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
20    TransactionSyntax, TypeNameSyntax, UtilitySyntax, ViewSequenceClauseSyntax,
21};
22use crate::precedence::{
23    Assoc, BindingPower, BindingPowerTable, STANDARD_SET_OPERATION_BINDING_POWERS,
24};
25
26/// SQLite identifier quoting: the standard `"a"`, MySQL-style `` `a` ``, and T-SQL
27/// `[a]`. SQLite additionally *falls back* a double-quoted token to a string
28/// constant when it resolves to no identifier — a resolution-time misfeature our
29/// parse-time model cannot express and the accept/reject oracle cannot see (`"x"`
30/// is accepted by both engines). Modelling `"` as the identifier quote is the
31/// faithful, conflict-free choice: [`StringLiteralSyntax::double_quoted_strings`]
32/// stays off, so no [`LexicalConflict::DoubleQuoteStringVersusIdentifier`] arises,
33/// and the fallback is recorded as an excluded-with-reason semantic divergence, not
34/// parsed (the sweep's `Control` class).
35///
36/// [`StringLiteralSyntax::double_quoted_strings`]: super::StringLiteralSyntax::double_quoted_strings
37/// [`LexicalConflict::DoubleQuoteStringVersusIdentifier`]: super::LexicalConflict::DoubleQuoteStringVersusIdentifier
38pub const SQLITE_IDENTIFIER_QUOTES: &[IdentifierQuote] = &[
39    IdentifierQuote::Symmetric('"'),
40    IdentifierQuote::Symmetric('`'),
41    IdentifierQuote::Asymmetric {
42        open: '[',
43        close: ']',
44    },
45];
46
47// --- SQLite per-position reject sets (POSITION-AWARE) -------------------------
48//
49// SQLite's reserved set is far smaller than ANSI's: its tokenizer keyword table
50// (`parse.y`) puts most keywords in the `%fallback ID` list, so `END`/`DESC`/`ASC`/
51// `ANALYZE`/`REPLACE`/… serve as ordinary identifiers. But the remaining reservations
52// are genuinely *position-dependent* — SQLite's grammar admits some non-fallback
53// keywords as a name (`nm`) while rejecting them as a bare alias / type name (`ids`),
54// so the five positions do NOT share one set (measured, not assumed — an in-process
55// rusqlite 3.53.2 probe over every position; see the ticket transcript). Three word
56// classes drive the split (each already a `Keyword` variant; hand-composed like the
57// `POSTGRES_NON_GENERIC_FUNCTION_KEYWORDS` precedent in `ansi.rs`, not a generated
58// per-dialect bitset — SQLite's sets are small subsets the union already holds):
59//
60//   * STRUCTURAL — the core keywords reserved in EVERY position (`SELECT`/`FROM`/`WHERE`/
61//     …). Not in `%fallback ID`, and not a `JOIN_KW`, so no production admits them.
62//   * JOIN keywords (`CROSS`/`INNER`/`LEFT`/`NATURAL`/`OUTER`/`RIGHT`/`FULL`) — tokenized
63//     as `JOIN_KW`, which the grammar admits via `nm ::= JOIN_KW`. So they ARE valid as a
64//     table/column name (`CREATE TABLE cross(…)`, `FROM left`), a function name, and an
65//     `AS` label (`AS nm`) — but NOT as a bare alias or a type name, both of which are
66//     the narrower `ids ::= ID|STRING` class that excludes `JOIN_KW` (`FROM t cross` is
67//     the CROSS JOIN, `CAST(1 AS cross)` a syntax error). This is the higher-impact half
68//     (`SELECT 1 AS left` / `CREATE TABLE left(…)` are common valid SQLite we rejected).
69//   * NAME-reserved residuals (`ISNULL`/`NOTNULL`/`RETURNING`/`NOTHING`) — non-fallback
70//     keywords SQLite rejects as an identifier in every NAME position (probed: `SELECT
71//     isnull`, `AS returning`, `CREATE TABLE nothing(…)` all syntax-reject). `RETURNING`/
72//     `NOTHING` are also rejected as a bare alias; `ISNULL`/`NOTNULL` are the exception —
73//     SQLite reads `SELECT 1 isnull` as the postfix `IS NULL` operator, so it accepts
74//     there. We do not model that postfix operator, so admitting them as a bare alias
75//     matches SQLite's ACCEPT verdict (a different tree, same accept/reject) rather than
76//     over-rejecting the common `col isnull` null test.
77//
78// The operator keywords `GLOB`/`MATCH`/`REGEXP` are deliberately absent from every set:
79// they double as function/identifier names (the built-in `glob(pattern, string)`), and a
80// dangling `SELECT 1 glob` is rejected by the Pratt operator path, not the reserved set.
81
82/// The SQLite keywords reserved in every position (`STRUCTURAL`): not in the `%fallback
83/// ID` list and not a `JOIN_KW`, so no production admits them as an identifier.
84const SQLITE_STRUCTURAL_RESERVED: &[Keyword] = &[
85    Keyword::Add,
86    Keyword::All,
87    Keyword::Alter,
88    Keyword::And,
89    Keyword::As,
90    Keyword::Between,
91    Keyword::Case,
92    Keyword::Check,
93    Keyword::Collate,
94    Keyword::Commit,
95    Keyword::Constraint,
96    Keyword::Create,
97    Keyword::Default,
98    Keyword::Deferrable,
99    Keyword::Delete,
100    Keyword::Distinct,
101    Keyword::Drop,
102    Keyword::Else,
103    Keyword::Escape,
104    Keyword::Except,
105    Keyword::Exists,
106    Keyword::Foreign,
107    Keyword::From,
108    Keyword::Group,
109    Keyword::Having,
110    Keyword::In,
111    Keyword::Index,
112    Keyword::Insert,
113    Keyword::Intersect,
114    Keyword::Into,
115    Keyword::Is,
116    Keyword::Join,
117    Keyword::Limit,
118    Keyword::Not,
119    Keyword::Null,
120    Keyword::On,
121    Keyword::Or,
122    Keyword::Order,
123    Keyword::Primary,
124    Keyword::References,
125    Keyword::Select,
126    Keyword::Set,
127    Keyword::Table,
128    Keyword::Then,
129    Keyword::To,
130    Keyword::Transaction,
131    Keyword::Union,
132    Keyword::Unique,
133    Keyword::Update,
134    Keyword::Using,
135    Keyword::Values,
136    Keyword::When,
137    Keyword::Where,
138];
139
140/// The seven SQLite `JOIN_KW` keywords: admissible as a name (`nm ::= JOIN_KW`) but not
141/// as a bare alias / type name (`ids ::= ID|STRING`), and consumed by the join grammar in
142/// join position. Reserved only in the bare-alias and type-name sets below.
143const SQLITE_JOIN_KEYWORDS: &[Keyword] = &[
144    Keyword::Cross,
145    Keyword::Inner,
146    Keyword::Left,
147    Keyword::Natural,
148    Keyword::Outer,
149    Keyword::Right,
150    Keyword::Full,
151];
152
153/// SQLite residual keywords reserved in every NAME position AND as a bare alias
154/// (`RETURNING`/`NOTHING` — probed syntax-rejects in all positions including
155/// `SELECT 1 nothing`).
156const SQLITE_RESIDUAL_NAME_AND_BARE: &[Keyword] = &[Keyword::Returning, Keyword::Nothing];
157
158/// SQLite residual keywords reserved in every NAME position but ADMITTED as a bare alias
159/// (`ISNULL`/`NOTNULL` — SQLite reads `SELECT 1 isnull` as the postfix `IS NULL` operator
160/// and accepts; we do not model that operator, so admitting them as a bare alias matches
161/// the ACCEPT verdict instead of over-rejecting the common `col isnull` null test).
162const SQLITE_RESIDUAL_NAME_ONLY: &[Keyword] = &[Keyword::Isnull, Keyword::Notnull];
163
164/// SQLite `ColId` reject set (column/table name, `AS` correlation alias, qualifier) —
165/// SQLite's `nm` production: `STRUCTURAL ∪ {all four NAME-reserved residuals}`. The seven
166/// `JOIN_KW` keywords are ABSENT (`nm ::= JOIN_KW` admits them), so `CREATE TABLE left(…)`
167/// / `FROM cross` / `INSERT INTO right …` parse.
168pub const SQLITE_RESERVED_COLUMN_NAME: KeywordSet =
169    KeywordSet::from_keywords(SQLITE_STRUCTURAL_RESERVED)
170        .union(KeywordSet::from_keywords(SQLITE_RESIDUAL_NAME_AND_BARE))
171        .union(KeywordSet::from_keywords(SQLITE_RESIDUAL_NAME_ONLY));
172
173/// SQLite function-name reject set. SQLite draws no `type_func_name` carve-out, so the
174/// name set governs the function position too (JOIN keywords ride `nm` and are admitted as
175/// call heads — `left(…)` — while `isnull(…)` is a syntax reject like every other position).
176pub const SQLITE_RESERVED_FUNCTION_NAME: KeywordSet = SQLITE_RESERVED_COLUMN_NAME;
177
178/// SQLite (user-defined / affinity) type-name reject set — SQLite's `ids`-class type name
179/// (`typename ::= ids …`, excluding `JOIN_KW`): the name set PLUS the seven JOIN keywords,
180/// so `CAST(1 AS cross)` is the syntax error SQLite reports even though the affinity
181/// fallback would otherwise treat `cross` as a user type. Arbitrary affinity names
182/// (`BANANA`) still ride the user-defined fallback before this gate.
183pub const SQLITE_RESERVED_TYPE_NAME: KeywordSet =
184    SQLITE_RESERVED_COLUMN_NAME.union(KeywordSet::from_keywords(SQLITE_JOIN_KEYWORDS));
185
186/// SQLite bare-alias reject set — the `ids ::= ID|STRING` class for a bare (`AS`-less)
187/// alias: `STRUCTURAL ∪ JOIN keywords ∪ {RETURNING, NOTHING}`. The JOIN keywords are
188/// reserved here (so `FROM t cross JOIN u` keeps `cross` for the join grammar and
189/// `SELECT 1 cross` is a syntax error), but `ISNULL`/`NOTNULL` are ADMITTED (they parse as
190/// the postfix null-test operator in SQLite; see `SQLITE_RESIDUAL_NAME_ONLY`). This set
191/// also governs the bare (`AS`-less) *table* correlation alias via
192/// [`TableExpressionSyntax::bare_table_alias_is_bare_label`](super::TableExpressionSyntax::bare_table_alias_is_bare_label).
193pub const SQLITE_RESERVED_BARE_ALIAS: KeywordSet =
194    KeywordSet::from_keywords(SQLITE_STRUCTURAL_RESERVED)
195        .union(KeywordSet::from_keywords(SQLITE_JOIN_KEYWORDS))
196        .union(KeywordSet::from_keywords(SQLITE_RESIDUAL_NAME_AND_BARE));
197
198/// SQLite `ColLabel` reject set — the `AS`-alias (`AS nm`) and dotted-name-continuation
199/// position. PostgreSQL admits every keyword there ([`KeywordSet::EMPTY`]); SQLite's `AS`
200/// label is the same `nm` production as a column name, so it reuses the ColId set —
201/// admitting the JOIN keywords (`SELECT 1 AS left`) while rejecting `STRUCTURAL` and the
202/// NAME-reserved residuals (`SELECT 1 AS delete` / `AS returning` are parse errors).
203pub const SQLITE_RESERVED_AS_LABEL: KeywordSet = SQLITE_RESERVED_COLUMN_NAME;
204
205impl StringLiteralSyntax {
206    /// The `SQLITE` preset for string literal syntax.
207    pub const SQLITE: Self = Self {
208        escape_strings: false,
209        dollar_quoted_strings: false,
210        national_strings: false,
211        double_quoted_strings: false,
212        backslash_escapes: false,
213        unicode_strings: false,
214        bit_string_literals: false,
215        blob_literals: true,
216        charset_introducers: false,
217        // SQLite requires a newline in the separator between adjacent literals.
218        same_line_adjacent_concat: false,
219    };
220}
221
222impl NumericLiteralSyntax {
223    /// The `SQLITE` preset for numeric literal syntax.
224    pub const SQLITE: Self = Self {
225        hex_integers: true,
226        octal_integers: false,
227        binary_integers: false,
228        // `_` digit-group separators (SQLite 3.46+, oracle-probed on rusqlite 3.53.2):
229        // `1_000`/`0x1_F` accept, `1_`/`1__0`/`0x_1F` reject.
230        underscore_separators: true,
231        money_literals: false,
232        // SQLite's radix grammar is `0[xX]{hexdigit}(_?{hexdigit})*` — a `_` may sit
233        // between hex digits but not lead the body (`0x_1F` rejects, unlike PG).
234        radix_leading_underscore: false,
235        // SQLite lexes a numeric literal abutting identifier chars as one TK_ILLEGAL
236        // token (`1SETECT`, `2ES`, `0x1g`, `1e5x` all reject; oracle-probed). Enabling
237        // this requires `underscore_separators` above so `1_000_000` (SQLite 3.46+) stays
238        // one number rather than a newly-rejected `1` plus junk suffix.
239        reject_trailing_junk: true,
240    };
241}
242
243impl CommentSyntax {
244    /// The `SQLITE` preset for comment syntax.
245    ///
246    /// SQLite's block comments diverge from the ANSI baseline in two engine-measured ways
247    /// (rusqlite): they do **not** nest (`/* a /* b */` closes at the first `*/`, so the
248    /// whole input is one comment and accepts — a nesting scanner would read it as
249    /// unterminated), and an unterminated `/* …` running to end of input is silently closed
250    /// as trailing trivia rather than an error (`SELECT 1/* eof`, `\t\t/*\t\t` prepare). Line
251    /// comments (`--`) stay `\n`-terminated like the ANSI baseline (a bare `/*` at EOF is the
252    /// `/` slash operator, handled by the tokenizer — see the field doc).
253    pub const SQLITE: Self = Self {
254        nested_block_comments: false,
255        unterminated_block_comment_at_eof: true,
256        line_comment_hash: false,
257        line_comment_ends_at_carriage_return: false,
258        versioned_comments: None,
259    };
260}
261
262impl ParameterSyntax {
263    /// The `SQLITE` preset for parameter syntax.
264    pub const SQLITE: Self = Self {
265        positional_dollar: false,
266        positional_dollar_large: false,
267        anonymous_question: true,
268        named_colon: true,
269        named_at: true,
270        named_dollar: true,
271        // SQLite numbered `?NNN` positional parameters (`?1`, `?123`), range-checked to
272        // `1..=32766` when materialised (engine-measured on rusqlite).
273        numbered_question: true,
274    };
275}
276
277impl IdentifierSyntax {
278    /// The `SQLITE` preset for identifier syntax.
279    pub const SQLITE: Self = Self {
280        // SQLite's IdChar class admits every code point at or above U+0080.
281        non_ascii: super::NonAsciiIdentifierSyntax::Any,
282        // SQLite's IdChar set includes `$` as a *continuation* byte (`L$C3`, `a$b`, `t$x` are
283        // one identifier each; engine-measured on rusqlite). `$` never *starts* an identifier —
284        // a leading `$name` is the dollar-named placeholder (`named_dollar`) and a lone `$` is a
285        // stray byte — so this widens only the continue run and never contends with the sigil.
286        dollar_in_identifiers: true,
287        // SQLite reads a single-quoted `'name'` string as a name wherever the grammar wants a
288        // `nm` identifier. Corpus-admitted for the relation-target and `PRIMARY
289        // KEY`/`UNIQUE` column-name positions (see the field doc); each is position-driven
290        // and unambiguous.
291        string_literal_identifiers: true,
292        // DuckDB-only single-part Sconst table-name form; SQLite's broader multi-part
293        // string-identifier misfeature is [`string_literal_identifiers`] above.
294        string_literal_table_names: false,
295        // SQLite admits an empty quoted identifier in every quote style (`` `` ``, `[]`, `""`);
296        // engine-measured on rusqlite, unique among the shipped engines.
297        empty_quoted_identifiers: true,
298    };
299}
300
301impl TypeNameSyntax {
302    /// The `SQLITE` preset for type name syntax.
303    pub const SQLITE: Self = Self {
304        integer_display_width: true,
305        // SQLite's `typename` is a free `ids ...` token run: an arbitrary multi-word affinity
306        // name (`UNSIGNED BIG INT`, `LONG INTEGER`) with an optional two-argument modifier
307        // (`VARCHAR(123,456)`), terminated by a column-constraint keyword / comma / close
308        // paren (engine-probed on rusqlite/sqlite3 3.53.2 & 3.43.2). Typed variants still win
309        // where they can faithfully hold the input.
310        liberal_type_names: true,
311        string_type_modifiers: false,
312        extended_scalar_type_names: false,
313        enum_type: false,
314        set_type: false,
315        numeric_modifiers: false,
316        composite_types: false,
317        varchar_requires_length: false,
318        zoned_temporal_types: true,
319        empty_type_parens: false,
320        character_set_annotation: false,
321        signed_type_modifier: false,
322        nullable_type: false,
323        low_cardinality_type: false,
324        fixed_string_type: false,
325        datetime64_type: false,
326        nested_type: false,
327        bit_width_integer_names: false,
328        angle_bracket_types: false,
329    };
330}
331
332impl TableExpressionSyntax {
333    /// The `SQLITE` preset for table expression syntax.
334    pub const SQLITE: Self = Self {
335        table_alias_column_lists: false,
336        bare_table_alias_is_bare_label: true,
337        // SQLite's `INDEXED BY <index>` / `NOT INDEXED` index directive on a table reference.
338        indexed_by: true,
339        only: false,
340        table_sample: false,
341        parenthesized_joins: true,
342        join_using_alias: false,
343        index_hints: false,
344        table_hints: false,
345        partition_selection: false,
346        base_table_alias_column_lists: true,
347        string_literal_aliases: false,
348        aliased_parenthesized_join: true,
349        table_version: false,
350        table_json_path: false,
351        prefix_colon_alias: false,
352    };
353}
354
355impl JoinSyntax {
356    /// The `SQLITE` preset for join syntax.
357    pub const SQLITE: Self = Self {
358        stacked_join_qualifiers: false,
359        // SQLite admits `NATURAL` before any join type; `NATURAL CROSS JOIN` is a natural
360        // inner join (engine-probed on rusqlite 3.53.2: shared-column equijoin shape, not
361        // the cross product), normalized into the canonical Inner+Natural shape.
362        natural_cross_join: true,
363        full_outer_join: true,
364        straight_join: false,
365        asof_join: false,
366        positional_join: false,
367        semi_anti_join: false,
368        sided_semi_anti_join: false,
369        apply_join: false,
370        recursive_search_cycle: false,
371        recursive_union_rejects_order_limit: false,
372        recursive_using_key: false,
373    };
374}
375
376impl TableFactorSyntax {
377    /// The `SQLITE` preset for table factor syntax.
378    pub const SQLITE: Self = Self {
379        // SQLite's `table-or-subquery` grammar admits a generic `table-function-name (
380        // args )` factor (the `pragma_table_info('t')` / `json_each('[]')` table-valued
381        // functions). Table-valued-ness is resolved at bind time, not parse time:
382        // `FROM abs(1)` and `FROM nofn(1)` parse-accept and fail only at prepare with a
383        // *binding* reject ("no such table"), while `FROM SELECT` / `FROM 123` are genuine
384        // syntax errors (engine-probed via rusqlite 3.53.2). Our parse-only parser matches
385        // that grammar with the flag on; the binding reject is not a parser concern.
386        table_functions: true,
387        lateral: false,
388        rows_from: false,
389        unnest: false,
390        unnest_with_offset: false,
391        table_function_ordinality: false,
392        special_function_table_source: true,
393        pivot: false,
394        unpivot: false,
395        show_ref: false,
396        from_values: false,
397        json_table: false,
398        xml_table: false,
399        table_expr_factor: false,
400        pivot_value_sources: false,
401        match_recognize: false,
402        open_json: false,
403    };
404}
405
406impl ExpressionSyntax {
407    /// The `SQLITE` preset for expression syntax.
408    pub const SQLITE: Self = Self {
409        typecast_operator: false,
410        subscript: false,
411        // DuckDB's three-bound `[lower:upper:step]` slice is a dialect extension (and `[`
412        // is a bracket identifier quote here regardless).
413        slice_step: false,
414        // `expr COLLATE <name>` as an expression postfix: `ORDER BY a COLLATE nocase`,
415        // `WHERE a < 'x' COLLATE binary`, and — since a `CREATE INDEX` key is a full
416        // expression — `CREATE INDEX i ON t(a COLLATE nocase)`. SQLite ranks it above the
417        // comparison operators exactly as PostgreSQL does (the shared postfix binding power),
418        // so `a = b COLLATE c` binds `a = (b COLLATE c)`. The collation name is an ordinary
419        // identifier (`nocase`/`binary`/`rtrim`, or a quoted `"reverse sort"`), read by the
420        // shared object-name grammar; an undefined collation is a SQLite *binding* reject the
421        // parser cannot and does not screen.
422        collate: true,
423        at_time_zone: false,
424        semi_structured_access: false,
425        array_constructor: false,
426        multidim_array_literals: false,
427        collection_literals: false,
428        row_constructor: false,
429        struct_constructor: false,
430        field_selection: false,
431        field_wildcard: false,
432        typed_string_literals: false,
433        // SQLite has no prefix-typed literal at all (`typed_string_literals` off), so the
434        // interval literal is never reached; kept off for uniformity.
435        typed_interval_literal: false,
436        // DuckDB's relaxed interval spellings are a dialect extension.
437        relaxed_interval_syntax: false,
438        mysql_interval_operator: false,
439        // DuckDB's `#n` positional column reference is a dialect extension.
440        positional_column: false,
441        lambda_keyword: false,
442    };
443}
444
445impl PredicateSyntax {
446    /// The `SQLITE` preset for predicate syntax.
447    pub const SQLITE: Self = Self {
448        // SQLite accepts an empty `IN ()` list (`x IN ()` is false, `x NOT IN ()` true);
449        // engine-measured via rusqlite. Otherwise SQLite's predicate surface is the ANSI
450        // baseline (standard `LIKE`, no `ILIKE`/`SIMILAR TO`/`OVERLAPS`/`NORMALIZED`).
451        empty_in_list: true,
452        // SQLite accepts the two-word `<expr> NOT NULL` postfix (a synonym for `IS NOT NULL`)
453        // alongside the one-word `NOTNULL`/`ISNULL`; both engine-measured via rusqlite 3.53.2
454        // (`SELECT 1 WHERE 1 NOT NULL` -> 1).
455        null_test_two_word_postfix: true,
456        is_distinct_from: true,
457        like: true,
458        ilike: false,
459        similar_to: false,
460        overlaps_period_predicate: false,
461        unparenthesized_in_list: false,
462        pattern_match_quantifier: false,
463        between_symmetric: false,
464        is_normalized: false,
465    };
466}
467
468impl OperatorSyntax {
469    /// The `SQLITE` preset for operator syntax.
470    pub const SQLITE: Self = Self {
471        operator_construct: false,
472        containment_operators: false,
473        json_arrow_operators: true,
474        // SQLite spells `?` as the anonymous placeholder and has none of the PostgreSQL
475        // `jsonb` operators, so this stays off (it would contend for the `?` trigger).
476        jsonb_operators: false,
477        double_equals: true,
478        // DuckDB-only `//` spelling; SQLite has no integer-division operator.
479        integer_divide_slash: false,
480        starts_with_operator: false,
481        is_general_equality: true,
482        // No truth-value predicate: SQLite's general `IS` folds `IS TRUE`/`IS FALSE` onto
483        // the boolean literal and reads `IS UNKNOWN` as equality against an identifier
484        // `unknown`, rejecting it unless bound (engine-measured via rusqlite). Off keeps
485        // that reading; on would over-accept `IS UNKNOWN`.
486        truth_value_tests: false,
487        // `<=>` is MySQL-only.
488        null_safe_equals: false,
489        // The single-arrow lambda is DuckDB-only: SQLite's `->` (on above) is always
490        // the JSON accessor, so `x -> x + 1` stays a `JsonGet` binary op here.
491        lambda_expressions: false,
492        // SQLite accepts the bitwise `| & ~ << >>` operators (engine-measured via rusqlite):
493        // SQLite has no bitwise XOR, so `bitwise_xor` stays off on the preset below.
494        bitwise_operators: true,
495        quantified_comparisons: false,
496        quantified_comparison_lists: false,
497        // SQLite has no quantified comparison at all, so the any-operator extension is
498        // vacuously off.
499        quantified_arbitrary_operator: false,
500        // SQLite has no general `Op`-class operator surface (its `^` has no infix meaning,
501        // via `caret_operator` on the preset below).
502        custom_operators: false,
503        null_test_postfix: true,
504        // SQLite has no postfix operator surface — a trailing symbolic operator rejects.
505        postfix_operators: false,
506    };
507}
508
509impl CallSyntax {
510    /// The `SQLITE` preset for call syntax.
511    pub const SQLITE: Self = Self {
512        named_argument: false,
513        // `<=>` and the UTC_* niladic functions are MySQL-only.
514        utc_special_functions: false,
515        columns_expression: false,
516        extract_from_syntax: false,
517        try_cast: false,
518        // SQLite's flexible typing accepts any affinity name as a cast target.
519        restricted_cast_targets: false,
520        // DuckDB-specific call tails; off for SQLite.
521        extract_string_field: false,
522        method_chaining: false,
523        sqljson_constructors_require_argument: false,
524        // SQLite has no SQL/JSON standard expression functions; keep the names ordinary.
525        sqljson_expression_functions: false,
526        // SQLite has no SQL/XML expression functions; keep the `xml*` names ordinary.
527        xml_expression_functions: false,
528        variadic_argument: false,
529        // `merge_action()` is a PostgreSQL-only support function.
530        merge_action_function: false,
531        convert_function: false,
532    };
533}
534
535impl StringFuncForms {
536    /// The `SQLITE` preset for string func forms.
537    pub const SQLITE: Self = Self {
538        // SQLite has none of the keyword string special forms (probed on the bundled
539        // engine: `SUBSTRING(x FROM 2)` / `TRIM(LEADING …)` / `OVERLAY(… PLACING …)`
540        // are all syntax errors; its `substring`/`substr`/`trim` are ordinary
541        // functions, and `position(a, b)` fails only at binding), so the whole
542        // family stays off and the heads keep their plain-call readings.
543        substring_from_for: false,
544        substring_leading_for: false,
545        substring_similar: false,
546        substring_plain_call_requires_2_or_3_args: false,
547        substr_from_for: false,
548        position_in: false,
549        position_asymmetric_operands: false,
550        overlay_placing: false,
551        overlay_requires_placing: false,
552        trim_from: false,
553        trim_list_syntax: false,
554        // `COLLATION FOR (<expr>)` is a PostgreSQL-only common-subexpr.
555        collation_for_expression: false,
556        // The `CEIL TO <field>` keyword form is sqlparser-rs-parity surface only —
557        // no probed oracle engine's grammar admits it.
558        ceil_to_field: false,
559        // The `FLOOR TO <field>` keyword form is sqlparser-rs-parity surface only —
560        // no probed oracle engine's grammar admits it.
561        floor_to_field: false,
562        match_against: false,
563    };
564}
565
566impl AggregateCallSyntax {
567    /// The `SQLITE` preset for aggregate call syntax.
568    pub const SQLITE: Self = Self {
569        group_concat_separator: false,
570        within_group: false,
571        // SQLite *does* have the `FILTER (WHERE …)` aggregate filter (since 3.30.0,
572        // engine-measured-accepted via rusqlite), so it stays on — unlike MySQL.
573        aggregate_filter: true,
574        // SQLite requires `FILTER (WHERE …)` (the keyword-less body is a syntax error).
575        filter_optional_where: false,
576        // SQLite admits an aggregate's argument forms regardless of a space before the `(`;
577        // the significant-space rule is MySQL's `IGNORE_SPACE`-off tokenizer only.
578        aggregate_args_require_adjacent_paren: false,
579        null_treatment: false,
580        // MySQL-only built-in aggregate/window arity restrictions; SQLite admits an empty
581        // aggregate call and `OVER` on any function.
582        aggregate_calls_reject_empty_arguments: false,
583        over_requires_windowable_function: false,
584        window_function_tail: false,
585        standalone_argument_order_by: false,
586    };
587}
588
589impl MutationSyntax {
590    /// The `SQLITE` preset for mutation syntax.
591    pub const SQLITE: Self = Self {
592        insert_ignore: false,
593        insert_overwrite: false,
594        returning: true,
595        on_conflict: true,
596        on_duplicate_key_update: false,
597        multi_column_assignment: false,
598        update_tuple_value_row_arity: false,
599        where_current_of: false,
600        merge: false,
601        replace_into: true,
602        insert_set: false,
603        // The MySQL `UPDATE`/`DELETE ... ORDER BY ... LIMIT` tails are a MySQL surface;
604        // SQLite's own (compile-time `SQLITE_ENABLE_UPDATE_DELETE_LIMIT`) form is off by
605        // default and out of this preset's scope.
606        update_delete_tails: false,
607        joined_update_delete: false,
608        // SQLite's `INSERT OR <action>` / `UPDATE OR <action>` conflict-resolution prefix.
609        or_conflict_action: true,
610        insert_column_matching: false,
611        // SQLite has no `DELETE ... USING` multi-relation delete.
612        delete_using: false,
613        // SQLite (3.33+) admits `UPDATE … SET … FROM <tables>`.
614        update_from: true,
615        // SQLite has no `DELETE … USING`, so the target-alias gate is moot; a leading
616        // `WITH` before `INSERT` is admitted.
617        delete_using_target_alias: true,
618        cte_before_insert: true,
619        // SQLite has no `MERGE`, so the leading-`WITH` gate is moot; off.
620        cte_before_merge: false,
621        // SQLite CTE bodies are select statements only — a DML body is a syntax error
622        // (probed via rusqlite prepare).
623        data_modifying_ctes: false,
624        // SQLite has no `MERGE`, so its residual-grammar gates are all moot; off.
625        merge_when_not_matched_by: false,
626        merge_insert_default_values: false,
627        merge_insert_overriding: false,
628        merge_insert_multirow: false,
629        merge_update_set_star: false,
630        merge_insert_star_by_name: false,
631        merge_error_action: false,
632        update_set_qualified_column: true,
633    };
634}
635
636impl StatementDdlGates {
637    /// The `SQLITE` preset for statement ddl gates.
638    pub const SQLITE: Self = Self {
639        colocation_groups: false,
640        // SQLite's `CREATE TRIGGER … BEGIN … END` compound-statement body.
641        create_trigger: true,
642        // SQLite has no `CREATE MACRO` (its functions are C-registered).
643        create_macro: false,
644        create_secret: false,
645        create_type: false,
646        // SQLite's `CREATE VIRTUAL TABLE <name> USING <module>(<args>)` — the module owns the
647        // opaque argument grammar; the parser only splits the args on top-level commas.
648        create_virtual_table: true,
649        // SQLite has no sequence generators (it uses AUTOINCREMENT rowids); `CREATE SEQUENCE`
650        // rejects — the `SEQUENCE` keyword falls through to the `CREATE TABLE` expectation.
651        create_sequence: false,
652        extension_ddl: false,
653        transform_ddl: false,
654        alter_system: false,
655        // MySQL's tablespace / logfile-group storage DDL is not a SQLite statement.
656        tablespace_ddl: false,
657        logfile_group_ddl: false,
658        // SQLite lacks schema objects, CREATE DATABASE, materialized views, stored
659        // routines, and the OR REPLACE modifier (all engine-measured-rejected).
660        schemas: false,
661        // SQLite has no schema objects at all, so the embedded-element form is off too.
662        schema_elements: false,
663        databases: false,
664        // SQLite has no `DROP DATABASE`/`DROP SCHEMA` (databases are files, reached via ATTACH).
665        drop_database: false,
666        materialized_views: false,
667        // SQLite spells session-local views `CREATE TEMP VIEW`.
668        routines: false,
669        or_replace: false,
670        create_or_replace_table: false,
671        // `CREATE RECURSIVE VIEW` is a DuckDB form; SQLite leaves `RECURSIVE`
672        // unconsumed before the expected `VIEW`.
673        // SQLite has no stored programs, so no compound-statement routine body.
674        compound_statements: false,
675        alter_database: false,
676        alter_database_options: false,
677        server_definition: false,
678        alter_instance: false,
679        spatial_reference_system: false,
680        resource_group: false,
681        alter_sequence: false,
682        alter_object_set_schema: false,
683    };
684}
685impl ViewSequenceClauseSyntax {
686    /// View/sequence clause surface for the `SQLITE` preset.
687    pub const SQLITE: Self = Self {
688        materialized_view_to: false,
689        create_sequence_cache: false,
690        temporary_views: true,
691        recursive_views: false,
692        view_definition_options: false,
693    };
694}
695
696impl CreateTableClauseSyntax {
697    /// The `SQLITE` preset for create table clause syntax.
698    pub const SQLITE: Self = Self {
699        table_options: false,
700        // SQLite accepts the trailing `WITHOUT ROWID` table option (a rowid-less table).
701        without_rowid_table_option: true,
702        // SQLite accepts the trailing `STRICT` table option (strict column-type enforcement).
703        strict_table_option: true,
704        storage_parameters: false,
705        on_commit: false,
706        create_table_as_with_data: true,
707        create_table_as_execute: false,
708        // SQLite has no table partitioning.
709        declarative_partitioning: false,
710        // SQLite has no table inheritance nor the PostgreSQL LIKE source-table element, and no
711        // statement-level `LIKE src` clone — SQLite reads a bare `LIKE` in element position as a
712        // keyword-named column instead, a behaviour this off flag preserves.
713        table_inheritance: false,
714        like_source_table: false,
715        statement_level_table_like: false,
716        unlogged_tables: false,
717        table_access_method: false,
718        without_oids: false,
719        typed_tables: false,
720    };
721}
722
723impl ColumnDefinitionSyntax {
724    /// The `SQLITE` preset for column definition syntax.
725    pub const SQLITE: Self = Self {
726        generated_column_shorthand: true,
727        // SQLite accepts a column-level `ON CONFLICT <resolution>` on an inline
728        // `NOT NULL`/`UNIQUE`/`PRIMARY KEY`/`CHECK` constraint.
729        column_conflict_resolution_clause: true,
730        // SQLite accepts a column with no declared type (`CREATE TABLE t (a, b)`).
731        typeless_column_definitions: true,
732        // The DuckDB generated-column narrowing is redundant under SQLite's wider typeless
733        // rule above (any column may drop its type), so it stays off — no need to also fire
734        // the narrow gate.
735        typeless_generated_columns: false,
736        // SQLite accepts the joined `AUTOINCREMENT` attribute on an inline `PRIMARY KEY` column
737        // (its own one-word keyword, distinct from MySQL's underscored `AUTO_INCREMENT`).
738        joined_autoincrement_attribute: true,
739        // SQLite accepts an `ASC`/`DESC` order qualifier on an inline `PRIMARY KEY` column
740        // (`a INTEGER PRIMARY KEY DESC`).
741        inline_primary_key_ordering: true,
742        // SQLite makes `COLLATE` an ordinary nameable column constraint, so it accepts a
743        // `CONSTRAINT <name>` prefix on a column COLLATE clause.
744        named_column_collate_constraint: true,
745        // SQLite has no IDENTITY column, WITH (storage params), ON COMMIT action, or
746        // extended ALTER surface (its ALTER is RENAME/ADD/DROP COLUMN only).
747        identity_columns: false,
748        compact_identity_columns: false,
749        // SQLite accepts a bare expression default and a `CONSTRAINT <name>` on any inline
750        // column constraint.
751        default_expression_requires_parens: false,
752        column_default_requires_b_expr: false,
753        // SQLite spells a per-column `COLLATE <name>` (a single bare identifier). The remaining
754        // residue surfaces — UNLOGGED, column STORAGE/COMPRESSION, the USING access method,
755        // WITHOUT OIDS, typed `OF <type>` tables — are PostgreSQL-only and absent here.
756        column_collation: true,
757        column_storage: false,
758    };
759}
760
761impl ConstraintSyntax {
762    /// The `SQLITE` preset for constraint syntax.
763    pub const SQLITE: Self = Self {
764        deferrable_constraints: true,
765        named_inline_non_check_constraints: true,
766        // SQLite accepts a trailing bodyless `CONSTRAINT <name>` — the constraint element
767        // after the name is optional — chaining freely with bodied constraints and, in the
768        // table-constraint list, needing no separating comma.
769        bare_constraint_name: true,
770        exclusion_constraints: false,
771        constraint_no_inherit_not_valid: false,
772        index_constraint_parameters: false,
773        // SQLite's indexed-column spelling in PRIMARY KEY / UNIQUE constraint position:
774        // `column-name [COLLATE <collation>] [ASC|DESC]` (engine-measured accepts, exprs and
775        // NULLS FIRST/LAST rejected).
776        constraint_column_collate_order: true,
777        referential_action_cascade_set: true,
778        check_constraint_subqueries: false,
779    };
780}
781
782impl IndexAlterSyntax {
783    /// The `SQLITE` preset for index alter syntax.
784    pub const SQLITE: Self = Self {
785        rename_constraint: false,
786        alter_table_set_options: false,
787        drop_primary_key: false,
788        alter_column_add_identity: false,
789        index_storage_parameters: false,
790        drop_behavior: false,
791        // SQLite's `DROP INDEX` is the shared name-list drop, not the MySQL `ON <table>` form.
792        index_drop_on_table: false,
793        index_concurrently: false,
794        index_using_method: false,
795        partial_index: true,
796        // SQLite supports `CREATE INDEX IF NOT EXISTS` and per-key `NULLS FIRST`/`LAST`
797        // (3.30+); it has no stored routines, so `routine_arg_types` is moot (off).
798        index_if_not_exists: true,
799        index_nulls_order: true,
800        alter_table_extended: false,
801        // SQLite's narrow `ALTER TABLE` never reaches these (it is not
802        // `alter_table_extended`), so the guard/type-change gates are inert — held off to
803        // keep the preset clean under `FeatureSet::feature_dependencies` (both ride
804        // `alter_table_extended`). It does parse `DEFERRABLE` constraints and (like the ANSI
805        // baseline) a `WITH [NO] DATA` clause.
806        alter_nested_column_paths: false,
807        alter_existence_guards: false,
808        alter_column_set_data_type: false,
809        routine_arg_types: false,
810        routine_arg_defaults: false,
811        routine_arg_modes: false,
812        // No stored routines, so the routine `LANGUAGE` operand is moot (off).
813        routine_language_string: false,
814        alter_table_multiple_actions: false,
815    };
816}
817
818impl ExistenceGuards {
819    /// The `SQLITE` preset for existence guards.
820    pub const SQLITE: Self = Self {
821        if_exists: true,
822        view_if_not_exists: true,
823        create_database_if_not_exists: false,
824    };
825}
826
827impl SelectSyntax {
828    /// The `SQLITE` preset for select syntax.
829    pub const SQLITE: Self = Self {
830        distinct_on: false,
831        select_into: false,
832        empty_target_list: false,
833        // SQLite has no `QUALIFY` clause (a DuckDB extension).
834        qualify: false,
835        // SQLite accepts a string literal as a projection alias after `AS` (`SELECT v AS
836        // 'x'`) — engine-measured via rusqlite 3.53.2, where `'x'` becomes the result-column
837        // name. Reuses the MySQL/DuckDB round-trip machinery (the alias renders back
838        // single-quoted, which SQLite re-accepts). The *bare* (`AS`-less) form rides the
839        // separate `bare_alias_string_literals` axis below (DuckDB accepts only the `AS` form,
840        // so the two split).
841        alias_string_literals: true,
842        // SQLite also accepts the *bare* string alias (`SELECT v 'x'`; engine-measured) —
843        // SQLite has no adjacent-string concatenation, so a string after an expression is
844        // unambiguously the alias.
845        bare_alias_string_literals: true,
846        // SQLite has no `UNION [ALL] BY NAME` name-matched set operation (a DuckDB
847        // extension); `BY` after a set operator is a syntax error there.
848        union_by_name: false,
849        wildcard_modifiers: false,
850        wildcard_replace: false,
851        intersect_all: false,
852        except_all: false,
853        // SQLite's `table.*` result-column is a non-aliasable production; a trailing alias
854        // rejects (measured Reject on rusqlite with the table provisioned).
855        qualified_wildcard_alias: false,
856        // FROM-first SELECT is a DuckDB extension; SQLite rejects a statement-position
857        // `FROM`.
858        from_first: false,
859        explicit_table: false,
860        // SQLite rejects a ragged VALUES constructor at parse — engine-measured via
861        // rusqlite `prepare`: "all VALUES must have the same number of terms" — so the
862        // preset enforces equal row arity, matching DuckDB (the shared parse-time gate
863        // `Parser::reject_ragged_values_rows`, fired in every VALUES position).
864        values_rows_require_equal_arity: true,
865        // SQLite has no parenthesized compound operand (a `select-core` is `SELECT`/
866        // `VALUES`, never `( … )`); a leading `(` in statement / set-op / CTE-body /
867        // CTAS / INSERT-source position is a syntax error. The FROM table-or-subquery
868        // grouping and expression scalar-subquery keep their parenthesized query — a
869        // complete standalone primary — through the parser's grouping context.
870        parenthesized_query_operands: false,
871        // SQLite spells the query-position VALUES constructor with bare `(…)` rows.
872        values_row_constructor: true,
873        // SQLite draws no `ColId`/`ColLabel` split, so its projection `AS` alias already
874        // rejects reserved words via the non-empty `reserved_as_label` set — no reroute.
875        as_alias_rejects_reserved: false,
876        // A trailing comma in a list is a DuckDB tolerance; SQLite rejects it.
877        trailing_comma: false,
878        // The prefix colon alias is a DuckDB extension; a `:` at a select-item /
879        // table-factor head is a parse error in SQLite.
880        prefix_colon_alias: false,
881        // Hive/Spark `LATERAL VIEW` is not SQLite; a post-FROM `LATERAL` is a parse
882        // error there.
883        lateral_view_clause: false,
884        // The Oracle-style `START WITH`/`CONNECT BY` hierarchical query clause is not
885        // SQLite; a post-WHERE `CONNECT BY`/`START WITH` is a parse error there.
886        connect_by_clause: false,
887    };
888}
889
890impl QueryTailSyntax {
891    /// The `SQLITE` preset for query tail syntax.
892    pub const SQLITE: Self = Self {
893        fetch_first: false,
894        limit_offset_comma: true,
895        // SQLite has no `FOR UPDATE`/`FOR SHARE` row-locking clause.
896        locking_clauses: false,
897        // No locking clause at all, so the PostgreSQL strength/stacking refinements are
898        // moot here.
899        key_lock_strengths: false,
900        stacked_locking_clauses: false,
901        using_sample: false,
902        // SQLite requires LIMIT before OFFSET; a bare leading OFFSET is a syntax error.
903        leading_offset: false,
904        limit_expressions: true,
905        limit_percent: false,
906        with_ties_requires_order_by: false,
907        // BigQuery/ZetaSQL `|>` pipe syntax is not SQLite; off here. A `|>` after a query is
908        // a parse error, and the token never lexes with the gate off.
909        pipe_syntax: false,
910        // ClickHouse `LIMIT n BY …` is not SQLite; a `BY` after `LIMIT` is a parse error.
911        limit_by_clause: false,
912        // ClickHouse `SETTINGS …` is not SQLite; a trailing `SETTINGS` is a parse error.
913        settings_clause: false,
914        // ClickHouse `FORMAT …` is not SQLite; a trailing `FORMAT` is a parse error.
915        format_clause: false,
916        // MSSQL `FOR XML`/`FOR JSON` is not SQLite; a trailing `FOR XML`/`FOR JSON` is a
917        // parse error.
918        for_xml_json_clause: false,
919    };
920}
921
922impl GroupingSyntax {
923    /// The `SQLITE` preset for grouping syntax.
924    pub const SQLITE: Self = Self {
925        grouping_sets: false,
926        with_rollup: false,
927        order_by_using: false,
928        // `GROUP BY ALL` / `ORDER BY ALL` are DuckDB clause modes; SQLite reserves
929        // `ALL` (`parse.y` keeps it out of the `%fallback ID` list), so either
930        // spelling is a syntax error there.
931        group_by_all: false,
932        group_by_set_quantifier: false,
933        order_by_all: false,
934    };
935}
936
937impl UtilitySyntax {
938    /// The `SQLITE` preset for utility syntax.
939    pub const SQLITE: Self = Self {
940        copy: false,
941        // `COPY INTO` is Snowflake bulk load/unload; SQLite has no such statement.
942        copy_into: false,
943        stage_references: false,
944        comment_on: false,
945        comment_if_exists: false,
946        pragma: true,
947        attach: true,
948        // `KILL` and the MySQL `DESCRIBE`/`DESC` overloads are MySQL-only; SQLite's own
949        // `EXPLAIN [QUERY PLAN]` keeps the ungated query-plan grammar.
950        kill: false,
951        // MySQL's `HANDLER` cursor family is not a SQLite statement.
952        handler_statements: false,
953        // MySQL's `INSTALL`/`UNINSTALL` `PLUGIN`/`COMPONENT` family is not a SQLite statement.
954        plugin_component_statements: false,
955        // MySQL's server-administration families are not SQLite statements.
956        shutdown: false,
957        restart: false,
958        clone: false,
959        import_table: false,
960        help_statement: false,
961        binlog: false,
962        // MySQL's `CACHE INDEX` / `LOAD INDEX INTO CACHE` key-cache pair is not a SQLite
963        // statement.
964        key_cache_statements: false,
965        // The `USE` catalog-switch statement is DuckDB/MySQL-only; SQLite has none.
966        use_statement: false,
967        // Moot: `use_statement` is off, so the name-arity refinement is unreachable.
968        use_qualified_name: false,
969        // Moot: `use_statement` is off, so the string-name refinement is unreachable.
970        use_string_literal_name: false,
971        // The DuckDB prepared-statement lifecycle and `CALL` are not SQLite statements.
972        prepared_statements: false,
973        // Moot: the typed parameter list widens `PREPARE`, which is already off.
974        prepare_typed_parameters: false,
975        // MySQL's `PREPARE ... FROM` lifecycle is not a SQLite statement either.
976        prepared_statements_from: false,
977        call: false,
978        // `call` is off (no SQLite `CALL`), so its MySQL bare-name widening is moot and off.
979        call_bare_name: false,
980        load_extension: false,
981        load_bare_name: false,
982        load_data: false,
983        reset_scope: false,
984        detach_if_exists: false,
985        // `DO` is the PostgreSQL anonymous code block; SQLite has no such statement.
986        do_statement: false,
987        // MySQL's `DO <expr-list>` statement; SQLite has none.
988        do_expression_list: false,
989        // MySQL's `LOCK/UNLOCK TABLES` and `LOCK/UNLOCK INSTANCE`; SQLite has neither
990        // (its locking is implicit in transaction modes), so both stay undispatched.
991        lock_tables: false,
992        lock_instance: false,
993        // SQLite's `BEGIN {DEFERRED|IMMEDIATE|EXCLUSIVE}` transaction-mode modifier
994        // (engine-measured on rusqlite 3.53.2: all three accept, `BEGIN CONCURRENT`
995        // rejects, doubling the modifier rejects).
996        // MySQL's `XA` distributed-transaction family is MySQL-only; SQLite has no `XA`
997        // statement, so the leading `XA` keyword is not dispatched.
998        // The standalone `RENAME TABLE`/`RENAME USER` statements are MySQL-only; SQLite
999        // renames via `ALTER TABLE … RENAME TO`, so the leading `RENAME` is not dispatched.
1000        rename_statement: false,
1001        signal_diagnostics: false,
1002        // SQLite has no `EXPORT`/`IMPORT DATABASE` (its dump surface is the `.dump` shell
1003        // command, not SQL), so the leading keywords stay undispatched.
1004        export_import_database: false,
1005        // SQLite has no `UPDATE EXTENSIONS` statement, so the `EXTENSIONS` lookahead is never
1006        // taken and every `UPDATE` reaches the DML parser.
1007        update_extensions: false,
1008        // MySQL's `FLUSH` / `PURGE BINARY LOGS` server-administration statements — SQLite has
1009        // neither, so both leading-keyword gates stay off.
1010        flush: false,
1011        purge_binary_logs: false,
1012        replication_statements: false,
1013    };
1014}
1015impl TransactionSyntax {
1016    /// Transaction-control surface for the `SQLITE` preset (split from UtilitySyntax).
1017    pub const SQLITE: Self = Self {
1018        start_transaction: false,
1019        start_transaction_block_optional: false,
1020        transaction_work_keyword: false,
1021        begin_transaction_keyword: true,
1022        commit_transaction_keyword: true,
1023        rollback_transaction_keyword: true,
1024        transaction_name: true,
1025        begin_transaction_modes: false,
1026        transaction_savepoints: true,
1027        set_transaction: false,
1028        transaction_isolation_mode: false,
1029        transaction_access_mode: false,
1030        transaction_deferrable_mode: false,
1031        start_transaction_isolation_mode: false,
1032        start_transaction_deferrable_mode: false,
1033        start_transaction_consistent_snapshot: false,
1034        transaction_multiple_modes: false,
1035        transaction_modes_require_commas: false,
1036        transaction_modes_reject_duplicates: false,
1037        abort_transaction_alias: false,
1038        end_transaction_alias: true,
1039        transaction_release: false,
1040        transaction_chain: false,
1041        release_savepoint_keyword_optional: true,
1042        begin_transaction_mode: true,
1043        xa_transactions: false,
1044    };
1045}
1046
1047impl ShowSyntax {
1048    /// The `SQLITE` preset for show syntax.
1049    pub const SQLITE: Self = Self {
1050        describe: false,
1051        describe_summarize: false,
1052        // SQLite has no SET/RESET/SHOW session statements and no GRANT/REVOKE.
1053        session_statements: false,
1054        set_value_reserved_words: KeywordSet::EMPTY,
1055        set_value_on_keyword: false,
1056        set_value_null_keyword: false,
1057        show_tables: false,
1058        show_columns: false,
1059        show_create_table: false,
1060        show_functions: false,
1061        show_routine_status: false,
1062        show_verbose: false,
1063        show_admin: false,
1064    };
1065}
1066
1067impl MaintenanceSyntax {
1068    /// The `SQLITE` preset for maintenance syntax.
1069    pub const SQLITE: Self = Self {
1070        vacuum: true,
1071        // DuckDB's `VACUUM [ANALYZE] <table> (<cols>)` grammar is DuckDB-only; SQLite's
1072        // `VACUUM` rides `vacuum` and admits `[<schema>] INTO <expr>` instead.
1073        vacuum_analyze: false,
1074        reindex: true,
1075        analyze: true,
1076        // DuckDB's `ANALYZE <table> (<cols>)` column list is DuckDB-only; SQLite's
1077        // `ANALYZE` takes a bare name with no column list.
1078        analyze_columns: false,
1079        // `CHECKPOINT` and `LOAD` are PostgreSQL/DuckDB statements (SQLite's checkpoint is
1080        // the `PRAGMA wal_checkpoint` form, already covered by `pragma`), and the DuckDB
1081        // `RESET`-scope / `DETACH … IF EXISTS` extensions are DuckDB-only. SQLite's own
1082        // `DETACH [DATABASE] name` (via `attach`) has no `IF EXISTS` guard.
1083        checkpoint: false,
1084        checkpoint_database: false,
1085        // The MySQL admin-table verbs are MySQL-only; SQLite's `ANALYZE` is the bare
1086        // leading-`analyze` form, not `ANALYZE TABLE`.
1087        table_maintenance: false,
1088    };
1089}
1090
1091impl AccessControlSyntax {
1092    /// The `SQLITE` preset for access control syntax.
1093    pub const SQLITE: Self = Self {
1094        alter_role_rename: false,
1095        access_control: false,
1096        // Moot: SQLite has no permission system, so `access_control` is already off.
1097        access_control_extended_objects: false,
1098        // SQLite has no accounts or roles.
1099        user_role_management: false,
1100        // Moot: SQLite has no permission system, so `access_control` is already off.
1101        access_control_account_grants: false,
1102    };
1103}
1104
1105/// SQLite binding powers, explicitly enumerated with left-associative comparisons and
1106/// the tight unary bitwise-NOT rank.
1107///
1108/// **Comparison associativity.** The comparison row is `Assoc::Left`, the same delta
1109/// MySQL applies: SQLite parses `1 < 2 < 3` as `(1 < 2) < 3` (the 0/1 result feeding the
1110/// outer comparison) where ANSI/PostgreSQL reject the chain. Rewriting the whole
1111/// `comparison` row from one representative operator (`Eq`) moves every comparison
1112/// variant — including the `==` spelling and the `GLOB`/`MATCH`/`REGEXP` keyword
1113/// operators — together; only associativity changes.
1114///
1115/// **Prefix `~` rank.** SQLite binds unary `~` tightly (its precedence table puts `~` with
1116/// the unary sign, above every binary operator), so `~ 1 + 1` groups `(~ 1) + 1` —
1117/// engine-measured, and the *opposite* of PostgreSQL/DuckDB's loose placement. The binary
1118/// bitwise operators keep STANDARD's shared rank (engine-measured: `1 | 2 & 2` is
1119/// `(1 | 2) & 2`, all four at one level between additive and comparison).
1120pub const SQLITE_BINDING_POWERS: BindingPowerTable = BindingPowerTable {
1121    or: BindingPower {
1122        left: 10,
1123        right: 11,
1124        assoc: Assoc::Left,
1125    },
1126    xor: BindingPower {
1127        left: 15,
1128        right: 16,
1129        assoc: Assoc::Left,
1130    },
1131    and: BindingPower {
1132        left: 20,
1133        right: 21,
1134        assoc: Assoc::Left,
1135    },
1136    comparison: BindingPower {
1137        left: 40,
1138        right: 41,
1139        assoc: Assoc::Left,
1140    },
1141    range_predicate_override: None,
1142    is_predicate_override: None,
1143    double_equals: BindingPower {
1144        left: 40,
1145        right: 41,
1146        assoc: Assoc::Left,
1147    },
1148    additive: BindingPower {
1149        left: 50,
1150        right: 51,
1151        assoc: Assoc::Left,
1152    },
1153    multiplicative: BindingPower {
1154        left: 60,
1155        right: 61,
1156        assoc: Assoc::Left,
1157    },
1158    exponent: BindingPower {
1159        left: 65,
1160        right: 66,
1161        assoc: Assoc::Left,
1162    },
1163    string_concat: BindingPower {
1164        left: 45,
1165        right: 46,
1166        assoc: Assoc::Left,
1167    },
1168    any_operator: BindingPower {
1169        left: 45,
1170        right: 46,
1171        assoc: Assoc::Left,
1172    },
1173    json_get: BindingPower {
1174        left: 45,
1175        right: 46,
1176        assoc: Assoc::Left,
1177    },
1178    bitwise_or: BindingPower {
1179        left: 45,
1180        right: 46,
1181        assoc: Assoc::Left,
1182    },
1183    bitwise_and: BindingPower {
1184        left: 45,
1185        right: 46,
1186        assoc: Assoc::Left,
1187    },
1188    bitwise_shift: BindingPower {
1189        left: 45,
1190        right: 46,
1191        assoc: Assoc::Left,
1192    },
1193    bitwise_xor: BindingPower {
1194        left: 45,
1195        right: 46,
1196        assoc: Assoc::Left,
1197    },
1198    prefix_not: 30,
1199    prefix_sign: 80,
1200    // SQLite's `~` binds like the unary sign, not PostgreSQL/DuckDB's between-arithmetic
1201    // rank.
1202    prefix_bitwise_not: 80,
1203    at_time_zone: BindingPower {
1204        left: 70,
1205        right: 71,
1206        assoc: Assoc::Left,
1207    },
1208    collate: BindingPower {
1209        left: 74,
1210        right: 75,
1211        assoc: Assoc::Left,
1212    },
1213    subscript: BindingPower {
1214        left: 84,
1215        right: 85,
1216        assoc: Assoc::Left,
1217    },
1218    typecast: BindingPower {
1219        left: 88,
1220        right: 89,
1221        assoc: Assoc::Left,
1222    },
1223    field_selection: BindingPower {
1224        left: 92,
1225        right: 93,
1226        assoc: Assoc::Left,
1227    },
1228};
1229
1230impl FeatureSet {
1231    /// SQLite as dialect data, including the position-aware reserved sets that its
1232    /// `%fallback` keyword model requires.
1233    pub const SQLITE: Self = Self {
1234        // SQLite compares identifiers case-insensitively (ASCII) while preserving the
1235        // written text; `Lower` models that identity (fold lower, render exact).
1236        identifier_casing: Casing::Lower,
1237        // `"a"`, `` `a` ``, and `[a]`. `"` quotes identifiers, so `double_quoted_strings`
1238        // is off (see `StringLiteralSyntax::SQLITE`) — the DQS fallback is excluded.
1239        identifier_quotes: SQLITE_IDENTIFIER_QUOTES,
1240        // SQLite sorts NULLs first under ascending order (NULL ranks lowest).
1241        default_null_ordering: NullOrdering::NullsFirst,
1242        // SQLite's reserved set is far smaller than ANSI's (its `%fallback` frees
1243        // `END`/`DESC`/`ASC`/`ANALYZE`/…), so it gets its own hand-composed per-position
1244        // sets rather than reusing the PostgreSQL-derived ANSI ones.
1245        reserved_column_name: SQLITE_RESERVED_COLUMN_NAME,
1246        reserved_function_name: SQLITE_RESERVED_FUNCTION_NAME,
1247        reserved_type_name: SQLITE_RESERVED_TYPE_NAME,
1248        reserved_bare_alias: SQLITE_RESERVED_BARE_ALIAS,
1249        // SQLite rejects reserved words in ColLabel position too (`SELECT 1 AS delete`,
1250        // `x.update`, `schema.case`) — no ColId/ColLabel split, unlike PostgreSQL.
1251        reserved_as_label: SQLITE_RESERVED_AS_LABEL,
1252        // SQLite relation names are `schema.table` (two parts); a database is the schema
1253        // namespace, so there is no catalog qualifier — `a.b.c` in table/index position
1254        // is a syntax error.
1255        catalog_qualified_names: false,
1256        // Backtick/bracket quotes, `?`/`:`/`@`/`$` placeholders, and `0x` integers all
1257        // dispatch from the standard byte classes gated by the knobs below — plus the
1258        // vertical tab as a whitespace-run *continuation* (`SQLITE_BYTE_CLASSES`):
1259        // `rusqlite`-measured, a `\v` rides an open whitespace run (`"\x20\x0b"` accepts)
1260        // but cannot start one (lone `"\x0b"` rejects), the sole byte-class divergence.
1261        byte_classes: SQLITE_BYTE_CLASSES,
1262        // SQLite's comparison family is left-associative like MySQL's, not `STANDARD`'s
1263        // `NonAssoc` (`SELECT 1 < 2 < 3` is legal SQLite meaning `(1 < 2) < 3`), so it
1264        // needs its own table (`SQLITE_BINDING_POWERS`).
1265        binding_powers: SQLITE_BINDING_POWERS,
1266        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
1267        string_literals: StringLiteralSyntax::SQLITE,
1268        numeric_literals: NumericLiteralSyntax::SQLITE,
1269        parameters: ParameterSyntax::SQLITE,
1270        // SQLite has no `@name`/`@@sysvar` session variables — its `@name` is a bind
1271        // parameter (`ParameterSyntax::SQLITE`), not a variable — so these stay off.
1272        session_variables: SessionVariableSyntax::ANSI,
1273        // SQLite identifiers are letters/digits/`_` only (`$` leads a placeholder, not an
1274        // identifier byte) but a `'name'` string is admitted in the relation-target and
1275        // `PRIMARY KEY`/`UNIQUE` column-name positions.
1276        identifier_syntax: IdentifierSyntax::SQLITE,
1277        // SQLite's join grammar is the ANSI surface (no `LATERAL`/`STRAIGHT_JOIN`/…)
1278        // minus the FROM table-alias column list, which SQLite lacks.
1279        table_expressions: TableExpressionSyntax::SQLITE,
1280        join_syntax: JoinSyntax::SQLITE,
1281        table_factor_syntax: TableFactorSyntax::SQLITE,
1282        expression_syntax: ExpressionSyntax::SQLITE,
1283        operator_syntax: OperatorSyntax::SQLITE,
1284        call_syntax: CallSyntax::SQLITE,
1285        string_func_forms: StringFuncForms::SQLITE,
1286        aggregate_call_syntax: AggregateCallSyntax::SQLITE,
1287        // SQLite has the standard `LIKE` predicate but neither `ILIKE` nor `SIMILAR TO`;
1288        // it diverges from ANSI only by accepting an empty `IN ()` list.
1289        predicate_syntax: PredicateSyntax::SQLITE,
1290        // SQLite `||` concatenates (the standard meaning), and `&&` is not an operator.
1291        pipe_operator: PipeOperator::StringConcat,
1292        double_ampersand: DoubleAmpersand::Unsupported,
1293        keyword_operators: KeywordOperators::Sqlite,
1294        // SQLite has no bitwise XOR operator (engine-measured: both `#` and `^` reject), and
1295        // `^` has no infix meaning at all.
1296        caret_operator: CaretOperator::Unsupported,
1297        hash_bitwise_xor: false,
1298        // SQLite comments are `--` and `/* … */`; no `#` line comment, block comments do not
1299        // nest, and an unterminated `/* …` at EOF is silently closed (`CommentSyntax::SQLITE`).
1300        comment_syntax: CommentSyntax::SQLITE,
1301        mutation_syntax: MutationSyntax::SQLITE,
1302        statement_ddl_gates: StatementDdlGates::SQLITE,
1303        view_sequence_clause_syntax: ViewSequenceClauseSyntax::SQLITE,
1304        create_table_clause_syntax: CreateTableClauseSyntax::SQLITE,
1305        column_definition_syntax: ColumnDefinitionSyntax::SQLITE,
1306        constraint_syntax: ConstraintSyntax::SQLITE,
1307        index_alter_syntax: IndexAlterSyntax::SQLITE,
1308        existence_guards: ExistenceGuards::SQLITE,
1309        select_syntax: SelectSyntax::SQLITE,
1310        query_tail_syntax: QueryTailSyntax::SQLITE,
1311        grouping_syntax: GroupingSyntax::SQLITE,
1312        // SQLite's `PRAGMA` and `ATTACH`/`DETACH` config statements and the
1313        // `VACUUM`/`REINDEX`/`ANALYZE` maintenance trio are dispatched; the PostgreSQL
1314        // `COPY`/`COMMENT ON` flags stay off (SQLite has neither).
1315        utility_syntax: UtilitySyntax::SQLITE,
1316        transaction_syntax: TransactionSyntax::SQLITE,
1317        show_syntax: ShowSyntax::SQLITE,
1318        maintenance_syntax: MaintenanceSyntax::SQLITE,
1319        access_control_syntax: AccessControlSyntax::SQLITE,
1320        // SQLite's flexible typing accepts an arbitrary affinity type name through the
1321        // user-defined-type fallback, so it needs no extended type vocabulary — save the
1322        // one built-in decoration affinity absorbs, integer display widths (`INT(11)`).
1323        type_name_syntax: TypeNameSyntax::SQLITE,
1324        // No SQLite-specific Tier-1 output spelling yet: a target-dialect render falls
1325        // back to the portable ANSI canonical spellings.
1326        target_spelling: TargetSpelling::Ansi,
1327    };
1328}
1329
1330/// Prefer [`FeatureSet::SQLITE`] for struct update.
1331pub const SQLITE: FeatureSet = FeatureSet::SQLITE;
1332
1333// Compile-time proof the SQLite preset claims no shared tokenizer trigger twice —
1334// notably that `"` quotes identifiers while `double_quoted_strings` stays off (no
1335// `DoubleQuoteStringVersusIdentifier`), and that `named_dollar` never meets a
1336// contending `dollar_quoted_strings`. The ratchet fails the build if a future edit
1337// adds a conflict, rather than silently shadowing a meaning (uniform with the ANSI
1338// and MySQL asserts).
1339const _: () = assert!(FeatureSet::SQLITE.is_lexically_consistent());
1340// The two sibling self-consistency registries are ratcheted the same way, so the
1341// parse-entry `debug_assert!` folds all three to dead code for this preset: no refinement
1342// flag rides an unset base, and no two features contend for one parser-position head.
1343const _: () = assert!(FeatureSet::SQLITE.has_satisfied_feature_dependencies());
1344const _: () = assert!(FeatureSet::SQLITE.has_no_grammar_conflict());
1345
1346#[cfg(test)]
1347mod tests {
1348    use super::super::{
1349        LexicalConflict, RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME,
1350        RESERVED_TYPE_NAME,
1351    };
1352    use super::*;
1353    use crate::ast::{BinaryOperator, EqualsSpelling, RegexpSpelling};
1354    use crate::precedence::STANDARD_BINDING_POWERS;
1355
1356    #[test]
1357    fn sqlite_reserved_set_is_smaller_than_ansi_freeing_sqlite_identifiers() {
1358        // The inventory evidence: SQLite's `%fallback` frees words the ANSI/PostgreSQL
1359        // model reserves, so `DESC`/`ASC`/`END`/`ANALYZE` serve as bare identifiers
1360        // (`CREATE TABLE z (end INT)`, `SELECT 'a' AS desc`, …). Each is reserved in
1361        // some ANSI position and free in every SQLite one.
1362        for keyword in [Keyword::Desc, Keyword::Asc, Keyword::End, Keyword::Analyze] {
1363            assert!(
1364                !SQLITE_RESERVED_COLUMN_NAME.contains(keyword),
1365                "{keyword:?} must be a free SQLite identifier",
1366            );
1367            assert!(!SQLITE_RESERVED_FUNCTION_NAME.contains(keyword));
1368            assert!(!SQLITE_RESERVED_TYPE_NAME.contains(keyword));
1369            assert!(!SQLITE_RESERVED_BARE_ALIAS.contains(keyword));
1370        }
1371        // At least one of them is genuinely reserved under ANSI, so the sets diverge.
1372        assert!(
1373            RESERVED_COLUMN_NAME.contains(Keyword::Desc)
1374                || RESERVED_BARE_ALIAS.contains(Keyword::Desc),
1375            "DESC should be reserved somewhere under the ANSI/PostgreSQL model",
1376        );
1377
1378        // The core structural keywords stay reserved in every position, so ordinary
1379        // SQL still parses (`SELECT`/`FROM`/`WHERE` cannot be bare identifiers).
1380        for keyword in [
1381            Keyword::Select,
1382            Keyword::From,
1383            Keyword::Where,
1384            Keyword::Join,
1385        ] {
1386            assert!(SQLITE_RESERVED_COLUMN_NAME.contains(keyword));
1387        }
1388
1389        // The keyword operators double as identifier / function names in SQLite (the
1390        // built-in `glob(pattern, string)`), so they are deliberately *not* reserved.
1391        for keyword in [Keyword::Glob, Keyword::Match, Keyword::Regexp] {
1392            assert!(!SQLITE_RESERVED_FUNCTION_NAME.contains(keyword));
1393        }
1394        // The four ANSI sets are referenced so their `use` is load-bearing.
1395        let _ = (RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME);
1396    }
1397
1398    #[test]
1399    fn sqlite_join_keywords_are_reserved_by_position_not_uniformly() {
1400        // The seven `JOIN_KW` keywords: admissible as a name (`nm ::= JOIN_KW`) — column /
1401        // table name, function name, `AS` label — but reserved as a bare alias and a type
1402        // name (`ids ::= ID|STRING`). Probed cell-by-cell against rusqlite 3.53.2 (the
1403        // conformance `sqlite_position_aware_reserved_matches_the_engine` oracle test).
1404        for keyword in [
1405            Keyword::Cross,
1406            Keyword::Inner,
1407            Keyword::Left,
1408            Keyword::Natural,
1409            Keyword::Outer,
1410            Keyword::Right,
1411            Keyword::Full,
1412        ] {
1413            // Admitted as a name (`CREATE TABLE left(…)`, `FROM cross`, `SELECT 1 AS left`).
1414            assert!(
1415                !SQLITE_RESERVED_COLUMN_NAME.contains(keyword),
1416                "{keyword:?} must be admissible as a SQLite table/column name",
1417            );
1418            assert!(!SQLITE_RESERVED_FUNCTION_NAME.contains(keyword));
1419            assert!(!SQLITE_RESERVED_AS_LABEL.contains(keyword));
1420            // Reserved as a bare alias (so `FROM t cross JOIN u` keeps the join) and a type
1421            // name (`CAST(1 AS cross)` is a syntax error).
1422            assert!(
1423                SQLITE_RESERVED_BARE_ALIAS.contains(keyword),
1424                "{keyword:?} must be reserved as a SQLite bare alias (the JOIN guard)",
1425            );
1426            assert!(SQLITE_RESERVED_TYPE_NAME.contains(keyword));
1427        }
1428    }
1429
1430    #[test]
1431    fn sqlite_name_reserved_residuals_are_reserved_as_a_name_but_isnull_notnull_bare_admit() {
1432        // `RETURNING`/`NOTHING`/`ISNULL`/`NOTNULL` are reserved in every NAME position
1433        // (probed: `SELECT isnull`, `AS returning`, `CREATE TABLE nothing(…)` syntax-reject)
1434        // — the four AS-label over-acceptances the sibling sweep found.
1435        for keyword in [
1436            Keyword::Returning,
1437            Keyword::Nothing,
1438            Keyword::Isnull,
1439            Keyword::Notnull,
1440        ] {
1441            assert!(
1442                SQLITE_RESERVED_COLUMN_NAME.contains(keyword),
1443                "{keyword:?} must be reserved as a SQLite name",
1444            );
1445            assert!(SQLITE_RESERVED_AS_LABEL.contains(keyword));
1446            assert!(SQLITE_RESERVED_FUNCTION_NAME.contains(keyword));
1447            assert!(SQLITE_RESERVED_TYPE_NAME.contains(keyword));
1448        }
1449        // `RETURNING`/`NOTHING` are also reserved as a bare alias (`SELECT 1 nothing` rejects)…
1450        assert!(SQLITE_RESERVED_BARE_ALIAS.contains(Keyword::Returning));
1451        assert!(SQLITE_RESERVED_BARE_ALIAS.contains(Keyword::Nothing));
1452        // …but `ISNULL`/`NOTNULL` are ADMITTED as a bare alias: SQLite reads `SELECT 1
1453        // isnull` as the postfix null-test operator (which we do not model), so admitting
1454        // them there matches its ACCEPT verdict rather than over-rejecting.
1455        assert!(!SQLITE_RESERVED_BARE_ALIAS.contains(Keyword::Isnull));
1456        assert!(!SQLITE_RESERVED_BARE_ALIAS.contains(Keyword::Notnull));
1457    }
1458
1459    #[test]
1460    fn sqlite_preset_is_lexically_consistent_with_double_quote_resolved_to_identifier() {
1461        // The load-bearing decision: `"` quotes identifiers and `double_quoted_strings`
1462        // stays off, so the preset carries no lexical conflict.
1463        assert!(FeatureSet::SQLITE.is_lexically_consistent());
1464        assert_eq!(FeatureSet::SQLITE.lexical_conflict(), None);
1465
1466        // Flipping `double_quoted_strings` on — while `"` still quotes identifiers —
1467        // is exactly the conflict the decision avoids: the two claim the same `"`
1468        // trigger. Proven by construction so the decision cannot silently rot.
1469        let with_dqs = FeatureSet::SQLITE.with(super::super::FeatureDelta::EMPTY.string_literals(
1470            StringLiteralSyntax {
1471                double_quoted_strings: true,
1472                ..StringLiteralSyntax::SQLITE
1473            },
1474        ));
1475        assert_eq!(
1476            with_dqs.lexical_conflict(),
1477            Some(LexicalConflict::DoubleQuoteStringVersusIdentifier),
1478        );
1479    }
1480
1481    #[test]
1482    fn sqlite_named_dollar_parameter_never_meets_dollar_quoting() {
1483        // `$name` is on and `$tag$…$tag$` dollar-quoting is off in the preset (see
1484        // `ParameterSyntax::SQLITE` / `StringLiteralSyntax::SQLITE`), so the shared
1485        // `$`+identifier-start trigger is uncontended. Turning dollar-quoting on is the
1486        // tracked conflict — SQLite has no dollar-quoting, so no shipped preset does.
1487        let with_dollar_quote = FeatureSet::SQLITE.with(
1488            super::super::FeatureDelta::EMPTY.string_literals(StringLiteralSyntax {
1489                dollar_quoted_strings: true,
1490                ..StringLiteralSyntax::SQLITE
1491            }),
1492        );
1493        assert_eq!(
1494            with_dollar_quote.lexical_conflict(),
1495            Some(LexicalConflict::NamedDollarParameterVersusDollarQuotedString),
1496        );
1497    }
1498
1499    #[test]
1500    fn sqlite_keyword_operators_map_glob_match_regexp() {
1501        // `GLOB`/`MATCH` get their own operator keys; `REGEXP` folds onto the shared
1502        // regex operator with the `Regexp` spelling tag (the MySQL `RLIKE`/`REGEXP`
1503        // round-trip pattern). Every other keyword is inert (ends the expression).
1504        assert_eq!(
1505            KeywordOperators::Sqlite.binary_operator(Keyword::Glob),
1506            Some(BinaryOperator::Glob),
1507        );
1508        assert_eq!(
1509            KeywordOperators::Sqlite.binary_operator(Keyword::Match),
1510            Some(BinaryOperator::Match),
1511        );
1512        assert_eq!(
1513            KeywordOperators::Sqlite.binary_operator(Keyword::Regexp),
1514            Some(BinaryOperator::Regexp(RegexpSpelling::Regexp)),
1515        );
1516        assert_eq!(
1517            KeywordOperators::Sqlite.binary_operator(Keyword::Div),
1518            None,
1519            "DIV is MySQL's, not SQLite's",
1520        );
1521    }
1522
1523    #[test]
1524    fn sqlite_comparison_row_is_left_associative_carrying_the_new_operators() {
1525        // The binding-power deltas from STANDARD: the comparison row goes `NonAssoc` ->
1526        // `Left` (so `1 < 2 < 3` / `1 == 2 == 3` chain left-associatively), and prefix `~`
1527        // takes the tight unary-sign rank (SQLite binds `~` above every binary operator, the
1528        // opposite of PostgreSQL/DuckDB's loose placement — engine-measured `~ 1 + 1` is
1529        // `(~ 1) + 1`).
1530        let mut expected = STANDARD_BINDING_POWERS;
1531        expected.comparison.assoc = Assoc::Left;
1532        // `==` rides the comparison row with `=` (SQLite treats them identically), so the
1533        // `double_equals` field moves to `Left` with `comparison` — DuckDB is the only
1534        // dialect that splits `==` off the comparisons.
1535        expected.double_equals.assoc = Assoc::Left;
1536        expected.prefix_bitwise_not = expected.prefix_sign;
1537        assert_eq!(SQLITE_BINDING_POWERS, expected);
1538
1539        // The bitwise binaries keep STANDARD's shared rank (one level between additive and
1540        // comparison, all left-associative): SQLite groups `1 | 2 & 2` as `(1 | 2) & 2`.
1541        for op in [
1542            BinaryOperator::BitwiseOr,
1543            BinaryOperator::BitwiseAnd,
1544            BinaryOperator::BitwiseShiftLeft,
1545            BinaryOperator::BitwiseShiftRight,
1546        ] {
1547            assert_eq!(
1548                SQLITE_BINDING_POWERS.binary(&op),
1549                STANDARD_BINDING_POWERS.binary(&op),
1550            );
1551        }
1552
1553        // Both `Eq` spellings and the `GLOB`/`MATCH`/`REGEXP` operators fold onto the
1554        // comparison row, so they ride the associativity delta together with `=`.
1555        for op in [
1556            BinaryOperator::Eq(EqualsSpelling::Single),
1557            BinaryOperator::Eq(EqualsSpelling::Double),
1558            BinaryOperator::Glob,
1559            BinaryOperator::Match,
1560            BinaryOperator::Regexp(RegexpSpelling::Regexp),
1561        ] {
1562            let bp = SQLITE_BINDING_POWERS.binary(&op);
1563            assert_eq!(bp.assoc, Assoc::Left, "{op:?} rides the comparison row");
1564            assert_eq!(bp.left, 40, "{op:?} keeps the STANDARD left rank");
1565            assert_eq!(bp.right, 41, "{op:?} keeps the STANDARD right rank");
1566        }
1567    }
1568}