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