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}