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