squonk_ast/dialect/mod.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! Const dialect data shared by the parser and renderer.
5//!
6//! Custom dialects are built from explicit deltas over a base preset:
7//!
8//! ```
9//! # use squonk_ast::dialect::{Casing, FeatureDelta, FeatureSet};
10//! let custom = FeatureSet::ANSI.with(
11//! FeatureDelta::EMPTY.identifier_casing(Casing::Preserve),
12//! );
13//! assert_eq!(custom.identifier_quotes, FeatureSet::ANSI.identifier_quotes);
14//! ```
15//!
16//! # Flag naming: widening vs narrowing
17//!
18//! Every boolean feature flag names the direction ON moves acceptance:
19//!
20//! - **Widening** (ON accepts more): a noun phrase of the accepted surface, e.g.
21//! `distinct_on`, `limit_percent`, `identity_columns`.
22//! - **Narrowing / enforcement** (ON rejects more): `<subject>_requires_<x>` or
23//! `<subject>_rejects_<x>`, e.g. `with_ties_requires_order_by`,
24//! `values_rows_require_equal_arity`, `as_alias_rejects_reserved`.
25//!
26//! Deliberate exceptions:
27//!
28//! - [`StringFuncForms::position_asymmetric_operands`] *swaps* grammar rather than widening
29//! or narrowing it — ON tightens the needle operand and widens the haystack — so neither
30//! direction word fits and it keeps a descriptive noun phrase.
31//! - [`CallSyntax::restricted_cast_targets`] is grandfathered: the participle already reads
32//! as a narrowing, and the name is referenced widely enough that a rename to the
33//! `requires`/`rejects` form would churn more than it clarifies.
34//! - [`QueryTailSyntax::with_ties_requires_order_by`] is named for its load-bearing
35//! `ORDER BY` guard; investigation shows it always co-varies with PostgreSQL's paired
36//! `SKIP LOCKED` + `WITH TIES` reject (only Postgres enables the flag), so both guards
37//! ride one knob rather than a second `rejects_skip_locked` field that would never
38//! diverge in shipped presets.
39//!
40//! # Struct header docs: category + doctrine, never member lists
41//!
42//! A sub-struct's header states the *category* of surface it gates and the *doctrine* its
43//! flags follow (widening/narrowing per the rule above, engine-probed on/off, leading-keyword
44//! dispatch, …), plus pointers to the relevant registries — never an enumeration of the
45//! specific flags/statements/clauses it contains. An enumeration is stale-by-growth: every
46//! field added past the ones the header happened to name turns the list into a misleading
47//! subset. A statement of the struct's MECE boundary against its sibling axes is *not* an
48//! enumeration, and is encouraged.
49
50pub mod closed_delta;
51pub mod keyword;
52pub mod lex_class;
53
54// Each shipped dialect owns its preset, reserved sets, and byte-class choices in
55// its own module; the shared `FeatureSet`/`KeywordSet`/`ByteClasses` machinery in
56// this module stays dialect-agnostic. `ansi` is the always-compiled baseline every
57// dialect derives from; `postgres`/`mysql`/`sqlite`/`duckdb`/`lenient` are each gated
58// behind their cargo feature so an excluded dialect's preset data is genuinely not
59// compiled.
60mod ansi;
61#[cfg(feature = "bigquery")]
62mod bigquery;
63#[cfg(feature = "clickhouse")]
64mod clickhouse;
65#[cfg(feature = "databricks")]
66mod databricks;
67#[cfg(feature = "duckdb")]
68mod duckdb;
69#[cfg(feature = "hive")]
70mod hive;
71#[cfg(feature = "lenient")]
72mod lenient;
73#[cfg(feature = "mssql")]
74mod mssql;
75#[cfg(feature = "mysql")]
76mod mysql;
77#[cfg(feature = "postgres")]
78mod postgres;
79#[cfg(feature = "quiltdb")]
80mod quiltdb;
81#[cfg(feature = "redshift")]
82mod redshift;
83#[cfg(feature = "snowflake")]
84mod snowflake;
85#[cfg(feature = "sqlite")]
86mod sqlite;
87
88pub use ansi::{
89 ANSI, RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME,
90 RESERVED_SET_VALUE_WORDS, RESERVED_TYPE_NAME, STANDARD_IDENTIFIER_QUOTES,
91};
92#[cfg(feature = "bigquery")]
93pub use bigquery::{BIGQUERY, BIGQUERY_IDENTIFIER_QUOTES};
94#[cfg(feature = "clickhouse")]
95pub use clickhouse::{CLICKHOUSE, CLICKHOUSE_IDENTIFIER_QUOTES};
96#[cfg(feature = "databricks")]
97pub use databricks::{
98 DATABRICKS, DATABRICKS_IDENTIFIER_QUOTES, DATABRICKS_QUALIFY_RESERVATION,
99 DATABRICKS_RESERVED_BARE_ALIAS, DATABRICKS_RESERVED_COLUMN_NAME,
100 DATABRICKS_RESERVED_FUNCTION_NAME, DATABRICKS_RESERVED_TYPE_NAME,
101};
102#[cfg(feature = "duckdb")]
103pub use duckdb::{DUCKDB, DUCKDB_BINDING_POWERS};
104#[cfg(feature = "hive")]
105pub use hive::{HIVE, HIVE_IDENTIFIER_QUOTES};
106pub use keyword::{Keyword, KeywordSet, lookup_keyword};
107#[cfg(feature = "lenient")]
108pub use lenient::{LENIENT, LENIENT_IDENTIFIER_QUOTES};
109pub use lex_class::{
110 ByteClassTable, ByteClasses, DUCKDB_BYTE_CLASSES, MYSQL_BYTE_CLASSES, POSTGRES_BYTE_CLASSES,
111 SQLITE_BYTE_CLASSES, STANDARD_BYTE_CLASSES,
112};
113#[cfg(feature = "mssql")]
114pub use mssql::{MSSQL, MSSQL_IDENTIFIER_QUOTES};
115#[cfg(feature = "mysql")]
116pub use mysql::{
117 MYSQL, MYSQL_IDENTIFIER_QUOTES, MYSQL_RESERVED_BARE_ALIAS, MYSQL_RESERVED_COLUMN_NAME,
118 MYSQL_RESERVED_FUNCTION_NAME, MYSQL_RESERVED_TYPE_NAME, MYSQL_WINDOW_FUNCTION_KEYWORDS,
119};
120#[cfg(feature = "postgres")]
121pub use postgres::POSTGRES;
122#[cfg(feature = "quiltdb")]
123pub use quiltdb::QUILTDB;
124#[cfg(feature = "redshift")]
125pub use redshift::REDSHIFT;
126#[cfg(feature = "snowflake")]
127pub use snowflake::{
128 SNOWFLAKE, SNOWFLAKE_QUALIFY_RESERVATION, SNOWFLAKE_RESERVED_BARE_ALIAS,
129 SNOWFLAKE_RESERVED_COLUMN_NAME, SNOWFLAKE_RESERVED_FUNCTION_NAME, SNOWFLAKE_RESERVED_TYPE_NAME,
130 SNOWFLAKE_TABLE_OPERATOR_RESERVATION,
131};
132#[cfg(feature = "sqlite")]
133pub use sqlite::{
134 SQLITE, SQLITE_IDENTIFIER_QUOTES, SQLITE_RESERVED_BARE_ALIAS, SQLITE_RESERVED_COLUMN_NAME,
135 SQLITE_RESERVED_FUNCTION_NAME, SQLITE_RESERVED_TYPE_NAME,
136};
137
138use std::borrow::Cow;
139
140use crate::ast::{
141 BinaryOperator, BitwiseXorSpelling, IntegerDivideSpelling, ModuloSpelling, RegexpSpelling,
142 SetOperator, UnaryOperator,
143};
144use crate::precedence::{BindingPower, BindingPowerTable, SetOperationBindingPowerTable};
145
146/// How unquoted identifiers fold for identity in consumers such as planners.
147///
148/// The parser still interns the exact source text; this value describes later
149/// identity behaviour without losing render fidelity.
150///
151/// One tri-state models a single identity fold shared by *every* unquoted
152/// identifier — table, column, and alias alike. That single-fold model is a
153/// deliberate, parity-matching choice, not a gap: `datafusion-sqlparser-rs`, the
154/// drop-in target, does not key case sensitivity by identifier kind either, so one
155/// knob already meets parity and per-kind folding would be strictly more expressive
156/// than parity requires. It also keeps the dialect model minimal.
157/// [`Self::Lower`] approximates the MySQL/T-SQL column rule — "case-preserving
158/// storage, case-insensitive comparison" — closely: fold lower for identity while
159/// the interned text still renders exactly as written (folding is layered onto
160/// the exact form, never baked into the parse).
161///
162/// # Known limitation: per-identifier-kind sensitivity
163///
164/// MySQL and T-SQL are case-insensitive for columns/aliases, but table and database
165/// name sensitivity is a *deployment* setting (MySQL `lower_case_table_names`, the
166/// T-SQL server/database collation). A case-insensitive column beside a
167/// case-sensitive table cannot be expressed by one fold, and that deployment-config
168/// knob is out of model; [`Self::Lower`] is the closest single fit and is what both
169/// dialects use. Per-identifier-kind casing is a deliberate future extension, gated
170/// on committing to a case-sensitive-table dialect (T-SQL): only then does the
171/// table-vs-column split change parse/identity results, so until such a dialect
172/// ships the added expressiveness would be unused surface (an M3 decision).
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub enum Casing {
175 /// Fold unquoted identifiers to upper case (Oracle/DB2 style).
176 Upper,
177 /// Fold unquoted identifiers to lower case (PostgreSQL style).
178 Lower,
179 /// Preserve unquoted identifier case as written (MySQL/SQL Server style).
180 Preserve,
181}
182
183impl Casing {
184 /// Fold an unquoted identifier for dialect identity comparison.
185 ///
186 /// M1 performs ASCII folding only. Non-ASCII bytes are preserved, matching
187 /// the tokenizer's permissive Unicode stance until a later precision pass
188 /// introduces full Unicode identifier semantics.
189 pub fn fold_identifier<'a>(&self, identifier: &'a str) -> Cow<'a, str> {
190 match self {
191 Self::Upper => fold_identifier_upper(identifier),
192 Self::Lower => fold_identifier_lower(identifier),
193 Self::Preserve => Cow::Borrowed(identifier),
194 }
195 }
196}
197
198/// Default sort position for null values when a query omits explicit ordering.
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
200pub enum NullOrdering {
201 /// Sort null values before non-null values by default.
202 NullsFirst,
203 /// Sort null values after non-null values by default.
204 NullsLast,
205}
206
207impl NullOrdering {
208 /// Whether this default places nulls before non-null values.
209 pub const fn nulls_first(&self) -> bool {
210 match self {
211 Self::NullsFirst => true,
212 Self::NullsLast => false,
213 }
214 }
215}
216
217/// Which dialect's canonical surface spelling a target-dialect render emits.
218///
219/// When [`RenderSpelling::TargetDialect`] rendering normalizes a construct whose
220/// spelling diverges across dialects — today the divergent type names (`NUMERIC` vs
221/// `DECIMAL`, `TIMESTAMPTZ` vs `TIMESTAMP WITH TIME ZONE`, `BYTEA` vs `BLOB`, …) — it
222/// emits this family's spelling. It is the dialect *data* the renderer reads instead
223/// of recognizing a preset by identity: each preset declares its family here, so
224/// PostgreSQL-vs-ANSI output spelling is a `FeatureSet` field, not a compile-time
225/// `FeatureSet::POSTGRES` comparison gated on the `postgres` cargo feature.
226/// `PreserveSource` rendering ignores it and keeps the AST's own syntax tag.
227///
228/// [`RenderSpelling::TargetDialect`]: crate::render::RenderSpelling::TargetDialect
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum TargetSpelling {
231 /// The ANSI/SQL-standard canonical spelling — the portable baseline every dialect
232 /// without its own Tier-1 spelling table renders to.
233 Ansi,
234 /// PostgreSQL's canonical spelling (`NUMERIC`, `TIMESTAMPTZ`, `BYTEA`, `VARCHAR`,
235 /// the explicit zone-suffix forms).
236 Postgres,
237}
238
239/// What the `||` operator token *means* in a dialect.
240///
241/// ANSI and PostgreSQL concatenate; MySQL/MariaDB without `PIPES_AS_CONCAT` treat
242/// `||` as a synonym for logical `OR` (sqlglot's `DPIPE_IS_STRING_CONCAT`). This
243/// is dialect *meaning* data, not tree shape: both spellings map to a
244/// single canonical [`BinaryOperator`], and the precedence follows automatically
245/// from that operator — `OR` binds looser than concatenation, so no separate
246/// binding-power override is needed.
247#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248pub enum PipeOperator {
249 /// `||` concatenates strings ([`BinaryOperator::StringConcat`]).
250 StringConcat,
251 /// `||` is logical OR ([`BinaryOperator::Or`]).
252 LogicalOr,
253}
254
255impl PipeOperator {
256 /// The canonical binary operator `||` parses to under this dialect.
257 pub const fn binary_operator(self) -> BinaryOperator {
258 match self {
259 Self::StringConcat => BinaryOperator::StringConcat,
260 Self::LogicalOr => BinaryOperator::Or,
261 }
262 }
263}
264
265/// What the `&&` operator token *means* in a dialect — the operator-meaning analog
266/// of [`PipeOperator`].
267///
268/// MySQL/MariaDB treat `&&` as a synonym for logical `AND`; DuckDB (and PostgreSQL)
269/// spell array/range/geometry overlap with `&&`; ANSI has no `&&` scalar operator. This
270/// is dialect *meaning* data: the `&&` lexeme always tokenizes, and this decides whether
271/// it is an infix operator and which canonical [`BinaryOperator`] it maps to. The chosen
272/// operator's binding power follows automatically, so there is no separate precedence
273/// override.
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub enum DoubleAmpersand {
276 /// `&&` is not a scalar infix operator (ANSI). It still lexes, but the parser rejects
277 /// it in expression position rather than mis-binding.
278 Unsupported,
279 /// `&&` is logical AND ([`BinaryOperator::And`], MySQL/MariaDB).
280 LogicalAnd,
281 /// `&&` is the overlap operator ([`BinaryOperator::Overlap`]) — array/range overlap
282 /// (PostgreSQL) and bounding-box overlap over geometries (DuckDB). Binds at the "any
283 /// other operator" precedence.
284 Overlaps,
285}
286
287impl DoubleAmpersand {
288 /// The canonical binary operator `&&` parses to, or `None` when the dialect
289 /// does not treat `&&` as a scalar infix operator.
290 pub const fn binary_operator(self) -> Option<BinaryOperator> {
291 match self {
292 Self::Unsupported => None,
293 Self::LogicalAnd => Some(BinaryOperator::And),
294 Self::Overlaps => Some(BinaryOperator::Overlap),
295 }
296 }
297}
298
299/// Whether a dialect treats MySQL's reserved-keyword infix operators — `DIV`, `MOD`,
300/// `XOR`, `RLIKE`, `REGEXP` — as operators.
301///
302/// MySQL spells these infix operators as keywords rather than symbols; ANSI and
303/// PostgreSQL have none of them (the words are ordinary identifiers there). Which
304/// keywords act as operators is therefore an explicit dialect-data decision,
305/// the keyword analogue of [`PipeOperator`]/[`DoubleAmpersand`]: this
306/// maps a reserved keyword token to the canonical [`BinaryOperator`] it folds onto.
307/// Precedence is *not* stored here — each operator's binding power is owned by the
308/// [`BindingPowerTable`] (`DIV`/`MOD` multiplicative, `XOR` between `AND` and `OR`,
309/// `RLIKE`/`REGEXP` comparison), so meaning and precedence cannot drift.
310///
311/// The `MySql` variant is named for a dialect, not the capability it grants — the
312/// lone place a `FeatureSet` value is dialect-named — and that is deliberate. A
313/// variant here denotes one dialect's *exact* keyword-operator set (MySQL's
314/// `DIV`/`MOD`/`XOR`/`RLIKE`/`REGEXP` mapping), which is a specification, not a
315/// composable capability: a future dialect with a different set gets its own
316/// variant rather than reusing this one, so the dialect name *is* the spec. A
317/// capability name like `DivModXorRegexp` would only restate that mapping, less
318/// precisely and with no room for the next dialect's variant.
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
320pub enum KeywordOperators {
321 /// No keyword infix operators (ANSI/PostgreSQL): `DIV`/`MOD`/`XOR`/`RLIKE`/`REGEXP`
322 /// are ordinary identifiers, so in infix position they end the expression.
323 Unsupported,
324 /// The MySQL keyword infix operators: `DIV`, `MOD`, `XOR`, `RLIKE`, `REGEXP`.
325 MySql,
326 /// The SQLite keyword infix operators: `GLOB`, `MATCH`, `REGEXP`. Like
327 /// [`MySql`](Self::MySql), the variant is named for the dialect whose *exact*
328 /// keyword-operator set it denotes (a specification, not a composable capability),
329 /// per the dialect-named-variant rationale documented on this enum. `GLOB` is a
330 /// built-in the differential oracle can verify; `MATCH`/`REGEXP` are grammar hooks
331 /// backed by application-defined functions the bundled engine does not register,
332 /// so they parse but a bare `prepare` rejects them — grammar-only siblings.
333 Sqlite,
334 /// DuckDB's keyword infix set is just `GLOB` (engine-probed on 1.5.4: `MATCH` /
335 /// `REGEXP` are not keyword infix operators there). Named for the dialect's exact
336 /// set, same rationale as [`Sqlite`](Self::Sqlite).
337 DuckDb,
338}
339
340impl KeywordOperators {
341 /// The canonical binary operator `keyword` maps to as an infix operator under
342 /// this dialect, or `None` when `keyword` is not a keyword operator here.
343 ///
344 /// The `MOD` and `RLIKE`/`REGEXP` keywords fold onto the shared modulo/regex
345 /// operators with a surface tag so the exact spelling round-trips;
346 /// `DIV` and `XOR` are their own operator keys.
347 pub const fn binary_operator(self, keyword: Keyword) -> Option<BinaryOperator> {
348 match self {
349 Self::Unsupported => None,
350 Self::MySql => match keyword {
351 Keyword::Div => Some(BinaryOperator::IntegerDivide(IntegerDivideSpelling::Div)),
352 Keyword::Mod => Some(BinaryOperator::Modulo(ModuloSpelling::Mod)),
353 Keyword::Xor => Some(BinaryOperator::Xor),
354 Keyword::Rlike => Some(BinaryOperator::Regexp(RegexpSpelling::Rlike)),
355 Keyword::Regexp => Some(BinaryOperator::Regexp(RegexpSpelling::Regexp)),
356 _ => None,
357 },
358 // SQLite's `GLOB`/`MATCH` get their own operator keys; `REGEXP` folds onto
359 // the shared regex operator with the `Regexp` spelling tag (same round-trip
360 // pattern MySQL uses for `RLIKE`/`REGEXP`).
361 Self::Sqlite => match keyword {
362 Keyword::Glob => Some(BinaryOperator::Glob),
363 Keyword::Match => Some(BinaryOperator::Match),
364 Keyword::Regexp => Some(BinaryOperator::Regexp(RegexpSpelling::Regexp)),
365 _ => None,
366 },
367 Self::DuckDb => match keyword {
368 Keyword::Glob => Some(BinaryOperator::Glob),
369 _ => None,
370 },
371 }
372 }
373}
374
375/// What the always-lexed `^` operator token *means* in a dialect — the operator-meaning
376/// analog of [`PipeOperator`]/[`DoubleAmpersand`].
377///
378/// The `^` byte is a single-character self token that always tokenizes; this decides what
379/// an infix `^` binds to. PostgreSQL/DuckDB read it as arithmetic exponentiation, MySQL as
380/// bitwise XOR, and ANSI/SQLite give it no infix meaning at all. The three readings are
381/// mutually exclusive per dialect — one byte, one meaning — so a single meaning-enum makes
382/// the invalid "both power and XOR" state unrepresentable (conflict-exempt: the both-state is
383/// unrepresentable by construction, so no [`GrammarConflict`] registry variant governs the
384/// exclusion). It folds together what used to
385/// be an `exponent_operator` bool and a `Caret`-XOR spelling that prose alone kept apart.
386/// The precedence follows from the mapped [`BinaryOperator`] via the dialect's
387/// [`BindingPowerTable`] (exponent at its own tier tighter than `*`; `Caret` XOR at the
388/// bitwise-XOR rank), so there is no separate override here.
389///
390/// Bitwise XOR's *other* spelling `#` (PostgreSQL) rides a different byte on its own axis,
391/// [`FeatureSet::hash_bitwise_xor`]; `^` and `#` never share this enum.
392#[derive(Clone, Copy, Debug, PartialEq, Eq)]
393pub enum CaretOperator {
394 /// `^` is not an infix operator (ANSI/SQLite): it still lexes, but the parser ends the
395 /// expression rather than binding it.
396 Unsupported,
397 /// `^` is arithmetic exponentiation ([`BinaryOperator::Exponent`], PostgreSQL/DuckDB) —
398 /// its own precedence tier, tighter than `*`.
399 Exponent,
400 /// `^` is bitwise XOR ([`BinaryOperator::BitwiseXor`] under the
401 /// [`Caret`](BitwiseXorSpelling::Caret) spelling, MySQL).
402 BitwiseXor,
403}
404
405impl CaretOperator {
406 /// The canonical binary operator `^` maps to under this dialect, or `None` when the
407 /// dialect gives `^` no infix meaning.
408 pub const fn binary_operator(self) -> Option<BinaryOperator> {
409 match self {
410 Self::Unsupported => None,
411 Self::Exponent => Some(BinaryOperator::Exponent),
412 Self::BitwiseXor => Some(BinaryOperator::BitwiseXor(BitwiseXorSpelling::Caret)),
413 }
414 }
415}
416
417/// Dialect-owned comment syntax extensions.
418///
419/// Standard `--` line comments and `/* … */` block comments are always part of
420/// the baseline. These flags cover dialect-specific comment forms — and the
421/// dialect-specific *shape* of the baseline forms — whose recognition is an
422/// explicit dialect data decision.
423#[derive(Clone, Copy, Debug, PartialEq, Eq)]
424pub struct CommentSyntax {
425 /// Treat `#` as a line-comment introducer (MySQL). One of several coexisting
426 /// `#` claimants (see [`FeatureSet`] module docs, "Shared byte-trigger ownership:
427 /// `#`"); trivia phase shadows identifier / XOR / positional readings when on.
428 /// Do not also mark `#` an identifier-start in byte classes (T-SQL `#temp`), or the
429 /// comment branch wins and `#temp` never lexes as a word
430 /// ([`LexicalConflict::HashCommentVersusHashIdentifier`](crate::dialect::LexicalConflict)).
431 pub line_comment_hash: bool,
432 /// Whether a bare carriage return (`\r`, `0x0d`) ends a `--`/`#` line comment, on
433 /// top of the newline (`\n`) that always ends one. PostgreSQL and DuckDB terminate a
434 /// line comment at *either* `\n` or `\r` — their flex scanner's comment body is
435 /// `[^\n\r]*` (engine-verified: `SELECT 1 -- c\rFROM` reads `FROM` as a live token and
436 /// rejects) — while SQLite and MySQL end it at `\n` alone, treating a `\r` as ordinary
437 /// comment content (the same input is one comment to end-of-line, accepted). All four
438 /// engines fold `\r` as whitespace *outside* a comment, so this flag governs only
439 /// whether `\r` *ends* a comment, never how it lexes elsewhere. The terminating byte is
440 /// left for the whitespace scan either way (`\r` is in `CLASS_WHITESPACE` for every
441 /// preset), so it never joins the comment's trivia span — matching PG, whose `[^\n\r]*`
442 /// body excludes the `\r`. `0x0b`/`0x0c` (vertical tab / form feed) sit in the flex
443 /// `space` set but not its newline set, so they are never terminators here.
444 pub line_comment_ends_at_carriage_return: bool,
445 /// Whether `/* … */` block comments nest: an inner `/*` raises a depth an
446 /// inner `*/` must lower before the comment can close. PostgreSQL nests;
447 /// MySQL ends every block comment at the first `*/` (engine-verified against
448 /// mysql:8 — `SELECT /* a /* b */ 1` parses as `SELECT 1` there, while a
449 /// nesting scanner reads it as an unterminated comment). The permissive
450 /// nesting superset predates this flag, so every preset except
451 /// MySQL keeps it on.
452 pub nested_block_comments: bool,
453 /// MySQL versioned comments (`/*! … */`, `/*!NNNNN … */`) as *conditional
454 /// inclusion*: the engine executes the body, so it is not a comment. `None`
455 /// (every non-MySQL dialect) keeps the whole construct an ordinary block
456 /// comment. `Some(bound)` models a server whose `MYSQL_VERSION_ID` is
457 /// `bound`: the body lexes as live tokens when the version is absent or
458 /// `<= bound`, and the region is discarded wholesale when the version
459 /// exceeds it — exactly the engine's include/skip gate.
460 ///
461 /// Engine-verified semantics (probed against mysql:8, 8.4.10): the version
462 /// is the digit run immediately abutting the `!` (a space breaks it) —
463 /// exactly five or exactly six digits form a version; 0–4 digits are not a
464 /// version (the digits stay body tokens and the region is included
465 /// unconditionally); from a run of ≥7 the first five are the version.
466 /// Regions do not nest (a flag, not a depth): a passing inner `/*!NNNNN`
467 /// marker is a no-op, a failing one discards only up to the next `*/`, and
468 /// the first region-level `*/` closes the region. A `*/` inside a string
469 /// literal of an *included* body does not close it (the body is lexed
470 /// normally), while a *discarded* body is raw bytes — not string-aware.
471 pub versioned_comments: Option<u32>,
472 /// Whether an unterminated `/* … ` block comment running to end of input is silently
473 /// closed (valid trailing trivia) rather than the pre-existing hard
474 /// `UnterminatedBlockComment` error. SQLite's tokenizer swallows a `/*` whose body runs
475 /// off the end as a `TK_SPACE` (engine-measured on rusqlite: `SELECT 1/* eof` and a
476 /// whitespace-wrapped `\t\t/*\t\t` both prepare), while every other engine rejects it.
477 ///
478 /// The one exception, replicated here: SQLite treats a *bare* `/*` sitting exactly at
479 /// end of input (no byte after the `*`, `z[2]==0`) as the `/` slash operator, not a
480 /// comment — so `/*` alone and `SELECT 1 /*` still reject on both. The scanner honours
481 /// this by opening a silently-EOF-closed comment only when a byte follows the `/*`. On
482 /// for SQLite / Lenient, off elsewhere.
483 pub unterminated_block_comment_at_eof: bool,
484}
485
486/// One accepted identifier-quoting delimiter style.
487///
488/// SQL identifier quotes escape an embedded close delimiter by *doubling* it
489/// (`"a""b"`, `[a]]b]`, and the backtick analogue), so the escape rule is implied by the kind —
490/// there is no separate escape character. A dialect accepts a *set* of these
491/// ([`FeatureSet::identifier_quotes`]); the tokenizer emits a single `QuotedIdent`
492/// token spanning the delimiters, and stripping/unescaping into an `Ident` is a
493/// later stage that reads the style back from the span's opening byte.
494#[derive(Clone, Copy, Debug, PartialEq, Eq)]
495pub enum IdentifierQuote {
496 /// Open and close are the same delimiter (`"`, `` ` ``).
497 Symmetric(char),
498 /// Distinct open/close (T-SQL `[a]`); only the close needs doubling to escape.
499 Asymmetric {
500 /// The opening delimiter character.
501 open: char,
502 /// The closing delimiter character.
503 close: char,
504 },
505}
506
507impl IdentifierQuote {
508 /// The opening delimiter.
509 pub const fn open(self) -> char {
510 match self {
511 Self::Symmetric(delim) => delim,
512 Self::Asymmetric { open, .. } => open,
513 }
514 }
515
516 /// The closing delimiter (doubled to escape an embedded close).
517 pub const fn close(self) -> char {
518 match self {
519 Self::Symmetric(delim) => delim,
520 Self::Asymmetric { close, .. } => close,
521 }
522 }
523}
524
525/// Dialect-owned string literal syntax extensions.
526///
527/// Standard single-quoted strings are always part of the baseline. These flags
528/// cover dialect-specific forms whose recognition must be an explicit dialect
529/// data decision, not a parser-side type check. Each is a lexical
530/// gate: it changes which token the scanner emits, never how a value is
531/// materialized (deferred).
532#[derive(Clone, Copy, Debug, PartialEq, Eq)]
533pub struct StringLiteralSyntax {
534 /// Accept PostgreSQL `E'...'` escape string constants.
535 pub escape_strings: bool,
536 /// Accept PostgreSQL `$tag$...$tag$` dollar-quoted string constants.
537 pub dollar_quoted_strings: bool,
538 /// Accept `N'...'` national-character string constants (T-SQL; PostgreSQL
539 /// sugar). Lexes as a `String` token spanning the `N` prefix.
540 pub national_strings: bool,
541 /// Lex `"..."` as a string constant rather than a quoted identifier (MySQL
542 /// without `ANSI_QUOTES`). Takes precedence over identifier quoting for `"`,
543 /// so a preset enabling this must not also list `"` in `identifier_quotes`.
544 pub double_quoted_strings: bool,
545 /// Honour C-style backslash escapes inside `'...'` (and double-quoted) strings
546 /// for termination (MySQL default; not T-SQL), so `'a\'b'` is one token. The
547 /// escape is recognised lexically only; value materialization stays deferred.
548 ///
549 /// This bool collapses what PostgreSQL historically made a *tri-state*: legacy
550 /// `standard_conforming_strings = off` made a plain `'…'` backslash-aware too — a
551 /// third mode distinct from both the modern-off default and MySQL's always-on. The
552 /// bool covers every *shipped* preset (modern PostgreSQL off, MySQL on); the
553 /// version-varying third mode belongs to the deferred per-release version presets
554 /// (`prod-dialect-release-version-presets`), which own version-varying knobs.
555 pub backslash_escapes: bool,
556 /// Accept `U&'...'` Unicode-escape string constants (SQL standard, PostgreSQL).
557 pub unicode_strings: bool,
558 /// Accept `B'...'` / `X'...'` bit-string constants (SQL standard, PostgreSQL).
559 /// Lexes as a `String` token spanning the `B`/`X` prefix; the binary-vs-hex
560 /// radix and the digit validation are recovered later, so a
561 /// malformed body like `X'1FG'` still lexes (PostgreSQL defers the check too).
562 pub bit_string_literals: bool,
563 /// Accept SQLite/MySQL `x'53514C'` / `X'53514c'` hexadecimal byte-string literals
564 /// (SQLite's BLOB literal; MySQL's hexadecimal literal). Only the `x`/`X` hex marker
565 /// — the `B'…'` binary form is [`bit_string_literals`](Self::bit_string_literals),
566 /// not this — and the quote must abut the marker (like `E'`/`B'`).
567 ///
568 /// Unlike a PostgreSQL/DuckDB deferred bit-string, the body is validated **eagerly at
569 /// lex time**: it must be an *even* number of ASCII hex digits (each pair is one
570 /// byte), so an odd-length (`x'ABC'`, `x'0'`) or non-hex (`x'XY'`) body is a
571 /// tokenize-time syntax error — the rule both engines enforce (probed: SQLite
572 /// "unrecognized token", MySQL `ER_PARSE_ERROR`). The empty body `x''` is a valid
573 /// zero-byte blob. It lexes as a `String` token spanning the marker and classifies as
574 /// a hex [`BitString`](crate::ast::LiteralKind::BitString) — the same canonical
575 /// hex-digit-string shape as `X'…'`, differing only in this eager lex-time bound; the
576 /// spelling round-trips from the span and a consumer reads the bytes via
577 /// [`as_bit_text`](crate::ast::Literal::as_bit_text).
578 ///
579 /// Disjoint from `bit_string_literals` on the shared `x`/`X` marker by scan
580 /// precedence: where both are on (MySQL), the eager hex arm claims `x`/`X` and the
581 /// deferred bit arm keeps `B`/`b`; where only `bit_string_literals` is on
582 /// (PostgreSQL) `X'…'` stays the deferred bit-string (odd-length allowed).
583 pub blob_literals: bool,
584 /// Accept MySQL `_charset'...'` character-set introducers — an `_`-prefixed
585 /// charset name abutting a string constant (`_utf8mb4'x'`, `_latin1'x'`). Like
586 /// the `N'...'` national prefix this lexes as one `String` token spanning the
587 /// introducer; the charset name is a surface tag that rides the span and is
588 /// recovered on demand, and the value materialises with the
589 /// introducer stripped. The abutting `'` is required — a bare `_name` with no
590 /// quote stays an ordinary identifier — so with this off `_utf8'x'` lexes as the
591 /// identifier `_utf8` then a string (the ANSI/PostgreSQL behaviour).
592 pub charset_introducers: bool,
593 /// Concatenate adjacent string literals separated by whitespace with *no* newline
594 /// (`'a' 'b'` on one line → `'ab'`), MySQL's rule. The SQL standard (and the default
595 /// here) requires a newline in the separator, so `'a' 'b'` same-line is
596 /// otherwise an adjacency error. Not a lexical gate — each literal still tokenizes
597 /// separately; the parser reads this at the continuation-gap classification point and
598 /// the AST materializer walks the folded span the same way it walks the newline form.
599 /// A comment in the gap still blocks concatenation under either rule.
600 pub same_line_adjacent_concat: bool,
601}
602
603/// Dialect-owned numeric literal syntax extensions.
604///
605/// Standard decimal/scientific numbers are always part of the baseline. These
606/// flags cover non-ANSI numeric forms whose recognition is an explicit dialect
607/// data decision. Each is lexical: it widens which numbers the scanner
608/// accepts as one token; value materialization stays deferred. The
609/// radix and separator forms below are PostgreSQL 14+ additions (also in T-SQL /
610/// MySQL for hex), so a release-pinned PostgreSQL preset gates them by version.
611#[derive(Clone, Copy, Debug, PartialEq, Eq)]
612pub struct NumericLiteralSyntax {
613 /// Accept `0x..` hexadecimal integer literals (T-SQL, MySQL, PostgreSQL 14+).
614 pub hex_integers: bool,
615 /// Accept `0o..` octal integer literals (PostgreSQL 14+).
616 pub octal_integers: bool,
617 /// Accept `0b..` binary integer literals (MySQL, PostgreSQL 14+).
618 pub binary_integers: bool,
619 /// Accept `_` digit-group separators between digits (PostgreSQL 14+), e.g.
620 /// `1_500_000`. Recognised lexically only when a digit follows the `_`. When
621 /// [`reject_trailing_junk`](Self::reject_trailing_junk) is off the placement is
622 /// loose (a `_` is a separator wherever a digit follows it); when it is on the
623 /// placement is strict (a decimal `_` must sit *between* two digits, matching PG's
624 /// `{decdigit}(_?{decdigit})*`), so a leading-in-fraction/trailing/doubled `_`
625 /// stops the number and surfaces as trailing junk. Whether a `_` may additionally
626 /// *lead* a radix body (`0x_1F`) is a separate axis
627 /// ([`radix_leading_underscore`](Self::radix_leading_underscore)), because PG and
628 /// SQLite disagree on it.
629 pub underscore_separators: bool,
630 /// Accept a leading `_` immediately after a radix marker, before the first radix
631 /// digit (`0x_1F`, `0b_101`) — PostgreSQL's `0[xX](_?{hexdigit})+` grammar, which
632 /// admits the underscore ahead of the first digit. Requires
633 /// [`underscore_separators`](Self::underscore_separators); an interior radix `_`
634 /// (`0x1_F`) rides that flag alone and is unaffected by this one.
635 ///
636 /// Its own axis rather than a rider on [`reject_trailing_junk`](Self::reject_trailing_junk)
637 /// because the engines that reject trailing junk still split on it: PostgreSQL accepts
638 /// `0x_1F` (probed) but SQLite rejects it — SQLite's radix grammar is
639 /// `0[xX]{hexdigit}(_?{hexdigit})*`, requiring a digit before the first `_`. With this
640 /// off a leading-underscore radix body does not open, so `0x_1F` falls back to the
641 /// bare `0` plus a `x_1F` word/alias (loose dialects) or a trailing-junk reject
642 /// (strict ones), exactly as before this axis existed.
643 pub radix_leading_underscore: bool,
644 /// Reject an identifier-start character abutting a numeric literal — PostgreSQL's
645 /// "trailing junk after numeric literal" scanner error (`123abc`, `1x`, `0.0e`,
646 /// `100_`), plus the bad-radix (`0x`, `0b0x`) and misplaced-separator (`100__000`,
647 /// `1_000._5`) forms that decompose to it. A number is a maximal-munch lexeme, so
648 /// with this on anything identifier-ish immediately following one is a lexer error,
649 /// not a new token; it also switches [`underscore_separators`] to strict placement.
650 ///
651 /// Dialect-gated because the reject is not universal: PostgreSQL and SQLite reject
652 /// these (both probed against their engines), but **DuckDB accepts them all** (it
653 /// re-reads `123abc` as `123` aliased) and MySQL only rejects the integer/radix forms
654 /// — so a dialect that lexes its numerics loosely (DuckDB/MySQL) leaves this off and
655 /// the trailing text falls through to the ordinary word/alias scan, exactly as before.
656 ///
657 /// [`underscore_separators`]: Self::underscore_separators
658 pub reject_trailing_junk: bool,
659 /// Accept `$1234.56` / `$.5` T-SQL money literals (the `$` currency sigil prefixes
660 /// a decimal). Off in ANSI/PostgreSQL: PostgreSQL spells `$` as a positional
661 /// parameter (`$1`) or a dollar-quote (`$tag$`), never money, so the three
662 /// `$`-prefix lexer forms are dialect-disjoint. Lexes as a `Number` token spanning
663 /// the `$`; the money type rides the literal kind and the sigil is stripped at the
664 /// accessor, like the `B`/`X` bit-string prefix.
665 pub money_literals: bool,
666}
667
668/// Dialect-owned prepared-statement parameter placeholder syntax.
669///
670/// SQL has no single standard placeholder spelling, so which forms a dialect
671/// accepts is explicit dialect data, not a parser-side type check. Each
672/// flag is a lexical gate: it decides whether the scanner recognises that sigil as
673/// a parameter [`TokenKind`](crate::ast) rather than a stray byte.
674#[derive(Clone, Copy, Debug, PartialEq, Eq)]
675pub struct ParameterSyntax {
676 /// Accept PostgreSQL positional `$1`, `$2`, … (`$` + ASCII digits). Disjoint
677 /// from dollar-quoting: `$` + digit is a parameter, `$` + tag-start/`$` opens a
678 /// dollar-quoted string, so both forms can be enabled together.
679 pub positional_dollar: bool,
680 /// Preserve a `$` positional parameter whose digit run exceeds `u32`. PostgreSQL's
681 /// scanner accepts these spellings and narrows them into its signed `ParamRef.number`;
682 /// dialects that range-check their parameter indices leave this off.
683 pub positional_dollar_large: bool,
684 /// Accept anonymous positional `?` placeholders (ODBC/JDBC).
685 pub anonymous_question: bool,
686 /// Accept colon-named `:name` placeholders (Oracle, SQLite, JDBC/psycopg). The
687 /// sigil must abut an identifier-start byte, which is what keeps `:name` free of
688 /// the two other `:` meanings: `::` stays the typecast (its second `:` is not an
689 /// identifier byte) and a lone `:` before a non-identifier byte stays the
690 /// array-slice separator. (A dialect that *also* sliced arrays with bare
691 /// identifier bounds — `a[x:y]` — or wrote semi-structured paths as `a:b` would lex
692 /// `:y`/`:b` as a parameter; that pairing is the tracked
693 /// [`LexicalConflict::ColonParameterVersusSliceBound`], since no real dialect combines
694 /// `:name` with those spellings.)
695 pub named_colon: bool,
696 /// Accept at-sign-named `@name` placeholders (T-SQL parameters/local variables).
697 /// The sigil must abut an identifier-start byte, so the system-variable `@@name`
698 /// form is left unclaimed for `prod-token-identifier-prefix-tokens` (the second
699 /// `@` is not an identifier byte, so this never grabs `@@`).
700 ///
701 /// Mutually exclusive with [`SessionVariableSyntax::user_variables`]: both claim
702 /// the `@name` trigger (one as a placeholder, one as a user-variable read), so a
703 /// feature set enabling both is a [`LexicalConflict`]. `@name`-as-parameter
704 /// (T-SQL) and `@name`-as-user-variable (MySQL) are the same surface with
705 /// different meaning, so a dialect picks one.
706 pub named_at: bool,
707 /// Accept dollar-named `$name` placeholders (SQLite). The sigil must abut an
708 /// identifier-start byte — `$` + a *digit* stays the PostgreSQL positional
709 /// `$1` ([`positional_dollar`](Self::positional_dollar)) and `$` + a
710 /// non-identifier byte a stray/money form — so this is follow-set-disjoint from
711 /// both `$`-digit forms and can be enabled alongside them.
712 ///
713 /// Contends only with [`StringLiteralSyntax::dollar_quoted_strings`]: a
714 /// `$tag$…$tag$` opener also leads with `$` + a tag-start byte (the same class as
715 /// identifier-start), so enabling both is a
716 /// [`LexicalConflict::NamedDollarParameterVersusDollarQuotedString`]. SQLite has
717 /// no dollar-quoting, so the two never meet in a shipped preset.
718 pub named_dollar: bool,
719 /// Accept SQLite numbered `?NNN` positional parameters (`?1`, `?123`) — the `?` sigil
720 /// abutting an ASCII-digit run — on top of the bare anonymous `?`
721 /// ([`anonymous_question`](Self::anonymous_question)). Follow-set-disjoint from the
722 /// anonymous form by the digit (a `?` with no digit stays anonymous), exactly as
723 /// PostgreSQL's `$1` splits from `$name`. The number is a maximal digit run, so a
724 /// trailing identifier is a separate token (`?1abc` is `?1` then the alias `abc`;
725 /// engine-measured). SQLite range-restricts the index to `1..=32766`
726 /// (`SQLITE_MAX_VARIABLE_NUMBER`): `?0`, `?32767`, and an overflowing run are parse
727 /// rejects ("variable number must be between ?1 and ?32766"; probed), enforced when the
728 /// numbered token is materialised. On for SQLite / Lenient, off elsewhere.
729 pub numbered_question: bool,
730}
731
732/// Dialect-owned MySQL session-variable syntax.
733///
734/// MySQL exposes user-defined `@name` variables and server `@@[scope.]name` system
735/// variables as *value expressions* — distinct from a prepared-statement placeholder
736/// ([`ParameterSyntax`]), which is a hole bound at execute time. Each flag is a
737/// lexical gate: it decides whether the scanner recognises that sigil as a variable
738/// token ([`Expr::SessionVariable`](crate::ast::Expr::SessionVariable)) rather than a
739/// stray byte. The two forms are independent — a dialect can accept `@@sysvar` without
740/// `@uservar` — and are lookahead-disjoint (`@@` needs a second `@`, `@name` an
741/// identifier-start), so they never contend with each other.
742///
743/// [`Expr::SessionVariable`]: crate::ast
744#[derive(Clone, Copy, Debug, PartialEq, Eq)]
745pub struct SessionVariableSyntax {
746 /// Accept `@name` user-defined session variables (MySQL). The sigil must abut an
747 /// identifier-start byte. Mutually exclusive with
748 /// [`ParameterSyntax::named_at`]: both claim `@name`, so enabling both is a
749 /// [`LexicalConflict::AtNameParameterVersusUserVariable`].
750 pub user_variables: bool,
751 /// Accept `@@name` / `@@global.name` / `@@session.name` system variables (MySQL).
752 /// The `@@` must abut an identifier-start byte; a scoped form folds the optional
753 /// `global.`/`session.` prefix into the same token. Never contends with `named_at`
754 /// or `user_variables` — the second `@` keeps `@@` lookahead-disjoint from the
755 /// single-`@` forms.
756 pub system_variables: bool,
757 /// Accept the MySQL `SET` **variable-assignment** statement grammar and its `:=`
758 /// assignment operator (MySQL).
759 ///
760 /// Two facets of one behaviour that ship together in MySQL and nowhere else:
761 /// * *Parser* — a `SET` becomes a comma-separated list of heterogeneous assignments
762 /// ([`SessionStatement::SetVariables`](crate::ast::SessionStatement::SetVariables)):
763 /// `[GLOBAL|SESSION|LOCAL|PERSIST|PERSIST_ONLY] <var> {= | :=} <expr>`, the
764 /// `@@[scope.]<var>` spellings, user variables `@v {= | :=} <expr>`, and
765 /// `SET {CHARACTER SET | CHARSET} …`. Each value is a *full expression*, unlike the
766 /// generic PostgreSQL `SET`'s restricted literal/bareword value list, so the two
767 /// grammars are distinct statements rather than one widened form.
768 /// * *Lexer* — `:=` lexes to the `ColonEquals` operator token (MySQL's `SET_VAR`), the
769 /// same token PostgreSQL's deprecated named-argument separator produces; the two never
770 /// coexist in a shipped preset, so the shared token is unambiguous per dialect.
771 ///
772 /// The two facets sit on independent axes, which is why this flag is a documented
773 /// exemption from the [`feature_dependencies`](FeatureSet::feature_dependencies) registry
774 /// rather than a [`FeatureDependencyViolation`] variant. The *parser* facet is unreachable
775 /// without [`show_syntax.session_statements`](ShowSyntax::session_statements) — the leader
776 /// that dispatches every `SET`/`RESET`/`SHOW`, so with it off no `SET` form parses at all
777 /// (measured: `SET x = 1` is rejected as an unknown statement leader) — a genuine
778 /// cross-axis grammar dependency. The *lexer* facet, however, fires whether or not that
779 /// leader is on: with `session_statements` off, `:=` still munches to one `ColonEquals`
780 /// token (measured), so the flag is **not** inert without its parser-side base. A registry
781 /// whose contract is inertness cannot own a flag that keeps changing tokenization while
782 /// its base is off, so the dependency is recorded here instead. See the exemption note on
783 /// [`feature_dependencies`](FeatureSet::feature_dependencies).
784 ///
785 /// This is a *route* flag on the `SET` head — when on it dispatches the MySQL grammar before
786 /// the standard `SET TIME ZONE` / `SET SESSION AUTHORIZATION` forms are read (see the parser's
787 /// `parse_set`) — but unlike the
788 /// [`access_control_account_grants`](AccessControlSyntax::access_control_account_grants) route
789 /// it carries *no* [`GrammarConflict`] variant: the grammar it displaces is unconditional base
790 /// grammar with no rival feature flag, so there is no independently-expressible both-on state,
791 /// and MySQL (a shipped preset) already exercises the route deterministically. Registering it
792 /// would require promoting the standard `SET` config grammar to its own flag or converting this
793 /// field to an enum axis — see the [`GrammarConflict`] enum doc's route-flag discussion.
794 ///
795 /// **Shared `:=` claim** with [`CallSyntax::named_argument`] (PostgreSQL named args): both
796 /// enable `:=` lexing; shipped presets never arm both, so the token stays unambiguous.
797 pub variable_assignment: bool,
798}
799
800/// Policy for non-ASCII code points in an *unquoted* identifier.
801#[derive(Clone, Copy, Debug, PartialEq, Eq)]
802pub enum NonAsciiIdentifierSyntax {
803 /// Only Unicode letters may start an identifier; Unicode letters and numbers may
804 /// continue it.
805 UnicodeAlphanumeric,
806 /// Every non-ASCII code point may start or continue an identifier.
807 Any,
808}
809
810/// Dialect-owned policy for which characters form an *unquoted* identifier.
811///
812/// The identifier-start and identifier-continue classes are an explicit,
813/// Unicode-aware policy, not an ad-hoc byte rule. ANSI starts with a Unicode *letter*
814/// (`char::is_alphabetic`) or `_`, and continues with a letter, a Unicode *digit*
815/// (`char::is_alphanumeric`), `_`, or — where [`dollar_in_identifiers`] is set — `$`.
816/// PostgreSQL, MySQL, and SQLite instead admit every non-ASCII code point. Quoted
817/// identifiers bypass the policy entirely (any character may be
818/// quoted), and no identifier *normalization* (NFC/NFKC) is performed — characters
819/// are compared as written; case folding for identity is the separate
820/// [`identifier_casing`] concern.
821///
822/// Only the dialect-*variable* part lives here. The ASCII letter/digit/`_` classes
823/// are the shared byte-class table (so a dialect can still add ASCII identifier bytes
824/// like T-SQL `#`/`@` through [`byte_classes`]); [`non_ascii`](IdentifierSyntax::non_ascii)
825/// selects the non-ASCII rule.
826///
827/// [`dollar_in_identifiers`]: IdentifierSyntax::dollar_in_identifiers
828/// [`identifier_casing`]: FeatureSet::identifier_casing
829/// [`byte_classes`]: FeatureSet::byte_classes
830#[derive(Clone, Copy, Debug, PartialEq, Eq)]
831pub struct IdentifierSyntax {
832 /// Select the non-ASCII start/continue policy. ASCII characters remain governed by
833 /// [`FeatureSet::byte_classes`].
834 pub non_ascii: NonAsciiIdentifierSyntax,
835 /// Accept `$` as an identifier-*continue* character (`foo$bar`), a PostgreSQL /
836 /// Oracle extension that strict ANSI forbids. `$` never *starts* an identifier (a
837 /// leading `$` is a parameter or dollar-quote), and it is never a dollar-quote
838 /// *tag* character (there `$` is the delimiter).
839 pub dollar_in_identifiers: bool,
840 /// Accept a single-quoted string literal in identifier positions *beyond* aliases —
841 /// SQLite's misfeature where a `'name'` string is read as a name wherever the grammar
842 /// wants a `nm` (identifier). Corpus-admitted for the two positions the SQLite
843 /// test-suite surfaces: a DML/DDL *relation-target* name (`DELETE FROM 'table1'`, and
844 /// so `CREATE TABLE 'name'` / `DROP TABLE 'n'` / `INSERT INTO 'n'` through the shared
845 /// target path) and a `PRIMARY KEY`/`UNIQUE` *table-constraint column-name* list
846 /// (`PRIMARY KEY('a')`, `UNIQUE('b')`). Each admitted position is *position-driven*:
847 /// a bare string there is never a valid literal in standard SQL, so reading it as the
848 /// name is unambiguous — no lexical or grammar conflict (the tokenizer still lexes
849 /// `'x'` to a `String`; a single parser position reads it, shadowing no rival feature).
850 ///
851 /// SQLite's full leniency is broader (a string is also admitted as a column-def name,
852 /// a `CAST` type name, a qualified column-ref qualifier, a `CREATE VIEW`/`TRIGGER`
853 /// target, …); those positions carry no corpus gap and stay out of scope. A string
854 /// *function* name is a SQLite syntax error (`SELECT 'f'(1)`), so the widening is
855 /// deliberately confined to the two name positions rather than folded into the shared
856 /// object-name grammar. The folded name records [`QuoteStyle::Single`] (or `Double`
857 /// under a no-`ANSI_QUOTES` mode) so the quotes round-trip, reusing the projection
858 /// alias's string round-trip. On for SQLite / Lenient, off elsewhere — every other
859 /// dialect syntax-rejects the form, so the over-acceptance risk is zero (flag off).
860 ///
861 /// [`QuoteStyle::Single`]: crate::ast::QuoteStyle::Single
862 pub string_literal_identifiers: bool,
863 /// Accept a single-part Sconst spelling of a relation / table name — DuckDB's
864 /// `FROM 't'` / `FROM ''` / `FROM E't'` / `FROM $$t$$` (engine-measured on
865 /// libduckdb 1.5.4). The string is a *single-part* name only: a dotted string name
866 /// (`FROM 'a'.'b'`) is a parser reject, matching DuckDB. Distinct from
867 /// [`string_literal_identifiers`](Self::string_literal_identifiers) (SQLite's broader
868 /// multi-part `'schema'.'table'` misfeature on relation *targets*). On for DuckDB and
869 /// the permissive superset; off elsewhere.
870 pub string_literal_table_names: bool,
871 /// Accept a *zero-length* delimited (quoted) identifier — the empty backtick
872 /// `` `` ``, the empty bracket `[]`, and the empty double-quote `""` — which SQL's
873 /// `<delimited identifier body>` and PostgreSQL/MySQL both forbid at scan time. SQLite
874 /// alone among the shipped engines admits an empty quoted identifier in every quote
875 /// style (engine-measured on rusqlite: `` SELECT `` ``, `SELECT []`, and `SELECT ""`
876 /// all prepare — the `""` via SQLite's double-quote-to-string fallback, the others as
877 /// zero-length names). When off, the tokenizer rejects a zero-length quoted identifier
878 /// the moment the close abuts the open (the pre-existing, universal
879 /// `ZeroLengthDelimitedIdentifier` scan reject). On for SQLite / Lenient, off elsewhere.
880 pub empty_quoted_identifiers: bool,
881}
882
883/// Dialect-owned table-expression syntax extensions.
884#[derive(Clone, Copy, Debug, PartialEq, Eq)]
885pub struct TableExpressionSyntax {
886 /// Accept PostgreSQL `ONLY table` / `ONLY (table)` inheritance suppression.
887 pub only: bool,
888 /// Accept `TABLESAMPLE method(args...) [REPEATABLE (...)]`.
889 pub table_sample: bool,
890 /// Accept joined tables as parenthesized table factors (`FROM (a JOIN b) JOIN c`).
891 /// On in every shipped preset — the standard join grouping — but stays gateable, so
892 /// a stricter dialect that forbids parenthesizing a join leaves it off and the form
893 /// then surfaces as a clean parse error.
894 pub parenthesized_joins: bool,
895 /// Accept `alias(column, ...)` derived-column lists after table factors. On in every
896 /// shipped preset (the SQL-standard correlation-name column list), but stays gateable
897 /// so a dialect without the form can reject it as trailing input.
898 pub table_alias_column_lists: bool,
899 /// Accept PostgreSQL `JOIN ... USING (...) AS alias`.
900 pub join_using_alias: bool,
901 /// Accept MySQL index hints (`{USE|FORCE|IGNORE} {INDEX|KEY} [FOR …] (…)`) as a
902 /// table-factor tail after the alias. MySQL-only, so it rides
903 /// [`TableFactor::Table::index_hints`](crate::ast::TableFactor); when off the hint
904 /// keywords are left to the identifier grammar and the construct is a clean parse
905 /// divergence. On for MySQL / Lenient, off elsewhere.
906 pub index_hints: bool,
907 /// Accept MSSQL / T-SQL `WITH ( <hint>, … )` table hints (`WITH (NOLOCK)`,
908 /// `WITH (INDEX(ix), FORCESEEK)`) as a table-factor tail after the tablesample
909 /// clause. T-SQL-only, so it rides
910 /// [`TableFactor::Table::table_hints`](crate::ast::TableFactor); when off the
911 /// trailing `WITH` is left unconsumed, so under ANSI/PostgreSQL — where `WITH`
912 /// introduces only a leading CTE clause at statement start — the construct is a
913 /// clean parse divergence. A separate axis from [`index_hints`](TableExpressionSyntax::index_hints):
914 /// a different dialect (T-SQL vs MySQL) and a different grammar position. On for
915 /// MSSQL / Lenient, off elsewhere.
916 pub table_hints: bool,
917 /// Accept MySQL explicit partition selection (`PARTITION (p0, p1)`) as a
918 /// table-factor tail between the table name and the alias. MySQL-only, so it rides
919 /// [`TableFactor::Table::partition`](crate::ast::TableFactor); when off the
920 /// `PARTITION` keyword is left unconsumed and the construct is a clean parse
921 /// divergence. On for MySQL / Lenient, off elsewhere.
922 pub partition_selection: bool,
923 /// Accept a column-list alias (`AS y(a, b)`) on a *base table* factor
924 /// (`FROM t AS y(a, b)`). On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL admits a
925 /// column-list alias only on a *derived* table / subquery / table function
926 /// (`FROM (SELECT …) AS c(x)` parses on mysql:8, only bind-failing) and rejects one on
927 /// a base table (`FROM t AS y(a, b)` is an `ER_PARSE_ERROR` on mysql:8), so it is off
928 /// there; the `(` after the base-table alias name is then a clean parse error. The
929 /// broader [`table_alias_column_lists`](TableExpressionSyntax::table_alias_column_lists) gate governs
930 /// whether the dialect admits column-list aliases *at all* (for the derived/function
931 /// positions); this one further restricts the base-table position — the base-vs-derived
932 /// split MySQL draws.
933 pub base_table_alias_column_lists: bool,
934 /// Accept a single-quoted string literal in table-alias position — both the
935 /// correlation name after an explicit `AS` (`FROM integers AS 't'`) and each entry
936 /// of the alias column list (`FROM integers AS 't'('k')` / `FROM integers t('k')`),
937 /// reusing the projection alias's string-literal round-trip. DuckDB admits this only
938 /// after `AS`: a bare `FROM integers 't'` is an engine reject (probed on 1.5.4),
939 /// preserved by the alias site's leading-string guard.
940 ///
941 /// Deliberately *separate* from [`SelectSyntax::alias_string_literals`], which gates
942 /// the string spelling in *projection* position: the two profiles diverge. MySQL
943 /// accepts a string *column* alias (`SELECT 1 AS 'x'`) but rejects a string *table*
944 /// alias (`FROM t AS 't'` — engine-measured-rejected on mysql:8), so folding both
945 /// onto one flag would make MySQL over-accept the table form. On for DuckDb /
946 /// Lenient, off elsewhere (including MySQL).
947 pub string_literal_aliases: bool,
948 /// Accept a correlation alias on a *parenthesized joined table*
949 /// (`FROM (a CROSS JOIN b) AS x`). On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL
950 /// admits a parenthesized join ([`parenthesized_joins`](TableExpressionSyntax::parenthesized_joins)) but
951 /// rejects an alias on it (`(a CROSS JOIN b) AS x` is an `ER_PARSE_ERROR` on mysql:8,
952 /// while the bare `(a CROSS JOIN b)` and a derived-table `(SELECT …) AS x` both parse),
953 /// so it is off there and the trailing alias surfaces as a clean parse error. Only the
954 /// *joined-table* parenthesization is governed; a derived subquery's alias rides the
955 /// always-accepted derived-table path.
956 pub aliased_parenthesized_join: bool,
957 /// Treat a bare (`AS`-less) *table* correlation alias as a `BareColLabel` — routed to
958 /// [`FeatureSet::reserved_bare_alias`] — instead of the default `ColId`
959 /// ([`FeatureSet::reserved_column_name`]). Off for every dialect except SQLite, where
960 /// the bare alias is the narrow `ids ::= ID|STRING` grammar class (not the `nm` name
961 /// class): the seven `JOIN_KW` keywords are admissible as a *table name* (`FROM cross`)
962 /// yet reserved as a *bare alias*, so `FROM t cross JOIN u` must keep `cross` for the
963 /// join grammar rather than read it as `t`'s alias. Routing the bare-table-alias gate
964 /// to the bare-alias set (which reserves the JOIN keywords) while the table-name gate
965 /// stays on the permissive `ColId` set is what makes the two positions diverge — the
966 /// table-alias twin of [`SelectSyntax::as_alias_rejects_reserved`]. The explicit `AS`
967 /// table alias keeps the `ColId` set (SQLite's `AS nm` admits the JOIN keywords).
968 pub bare_table_alias_is_bare_label: bool,
969 /// Accept a table version / time-travel modifier on a base table, written between the
970 /// table name and the alias: BigQuery/MSSQL `FOR SYSTEM_TIME …`, MSSQL's five temporal
971 /// forms (`AS OF`, `FROM … TO`, `BETWEEN … AND`, `CONTAINED IN`, `ALL`), and
972 /// Databricks/Delta `VERSION`/`TIMESTAMP AS OF`. Rides
973 /// [`TableFactor::Table::version`](crate::ast::TableFactor); when off the clause keyword
974 /// is left unconsumed, so a query-level `FOR` (row locking, MSSQL `FOR XML`) still parses
975 /// — the two `FOR` surfaces are position-partitioned, this one at the table factor and
976 /// the query-level ones after the whole `FROM`/`WHERE`. On for BigQuery / MSSQL /
977 /// Databricks / Lenient, off elsewhere.
978 pub table_version: bool,
979 /// Accept a PartiQL / SUPER JSON path navigating into a semi-structured column at the
980 /// table-source position, attached directly to the table name (`FROM src[0].a`,
981 /// `FROM src[0].a[1].b`). Redshift's SUPER navigation and Snowflake's PartiQL access
982 /// (sqlparser-rs's `supports_partiql`). Rides
983 /// [`TableFactor::Table::json_path`](crate::ast::TableFactor); the path is entered only
984 /// by a `[` immediately after the name (a bracket index root, then `.key` / `[index]`
985 /// suffixes), so a dotted `FROM src.a.b` stays a compound relation name. When off the
986 /// `[` is left unconsumed and the construct is a clean parse divergence. Because the
987 /// entry trigger is the `[` tokenizer trigger, this shares the
988 /// [`BracketIdentifierVersusArraySyntax`](crate::dialect::LexicalConflict) hazard: a
989 /// dialect with a `[` identifier quote cannot also enable it. On for Snowflake /
990 /// Redshift, off elsewhere — including Lenient, whose `[` bracket identifier quote claims
991 /// the trigger (the same reason Lenient keeps `subscript` / `collection_literals` off).
992 pub table_json_path: bool,
993 /// Accept a SQLite `INDEXED BY <index>` / `NOT INDEXED` index directive on a base table,
994 /// written after the table name and its optional alias (`FROM t AS e INDEXED BY ix`,
995 /// `FROM t NOT INDEXED`). Rides
996 /// [`TableFactor::Table::indexed_by`](crate::ast::TableFactor). When on, a bare
997 /// `INDEXED` at the base-table alias position is declined as a correlation alias so the
998 /// directive is reachable (the `CONNECT BY` clause-decline precedent), which is what
999 /// makes SQLite reject a bare `FROM t indexed` while still admitting `indexed` as an
1000 /// identifier everywhere else (`SELECT indexed`, `t AS indexed`, `indexed INT`). A
1001 /// separate axis from MySQL [`index_hints`](TableExpressionSyntax::index_hints): a
1002 /// different dialect, grammar, and cardinality. On for SQLite, off elsewhere — including
1003 /// Lenient, whose maximal-accept goal keeps a bare `FROM t indexed` an ordinary alias
1004 /// (the directive-versus-bare-alias readings are mutually exclusive given the keyword's
1005 /// one-position semantics, and Lenient prefers the more permissive bare-alias reading).
1006 pub indexed_by: bool,
1007 /// Accept DuckDB's **table-factor** prefix colon alias — `<alias> : <relation>` at a
1008 /// `FROM` head (`FROM b : a` → relation `a` aliased `b`). Projection-position twin is
1009 /// [`SelectSyntax::prefix_colon_alias`]. Same pure-`AS` sugar and
1010 /// [`GrammarConflict::PrefixColonAliasVersusSemiStructuredAccess`] hazard when either
1011 /// position is on with `semi_structured_access`. On for DuckDB / Lenient; off elsewhere.
1012 pub prefix_colon_alias: bool,
1013}
1014
1015/// Dialect-owned join-operator and recursive-query relation-composition syntax
1016/// accepted by the parser.
1017///
1018/// The join-family flags — the join operators and their side/qualifier variants beyond
1019/// the always-available inner/left/right/cross joins — together with the recursive-query
1020/// structural clauses that compose relations. Split out of [`TableExpressionSyntax`] at
1021/// its 16-field line as the relation-composition axis, distinct from the table-factor and
1022/// factor-tail/alias cores. Each flag is a grammar gate: when off the keyword is left to
1023/// the identifier grammar or surfaces as a clean parse error.
1024#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1025pub struct JoinSyntax {
1026 /// Accept *stacked* join qualifiers — the PostgreSQL right-nesting where two joins
1027 /// precede their `ON`/`USING` constraints and the nearest constraint closes the
1028 /// innermost join: `a JOIN b JOIN c ON p ON q` reads as `a JOIN (b JOIN c ON p) ON q`
1029 /// (PostgreSQL `table_ref: … | joined_table` right-recursion). On for ANSI/PostgreSQL/
1030 /// MySQL/DuckDB/Lenient. SQLite's `join-clause` is flat — each `join-operator` takes
1031 /// exactly one immediately-following constraint — so it is off; the right operand is
1032 /// not extended past the first constraint, and a second stacked `ON`/`USING`
1033 /// (`… USING (id) USING (id)`) is then left unconsumed and surfaces as the syntax
1034 /// error SQLite reports (engine-measured via rusqlite). The common `a JOIN b ON x
1035 /// JOIN c ON y` — each constraint right after its own join — needs no nesting and is
1036 /// unaffected either way.
1037 pub stacked_join_qualifiers: bool,
1038 /// Accept the `FULL [OUTER] JOIN` bilateral outer join. On for ANSI/PostgreSQL/SQLite/
1039 /// DuckDB/Lenient. MySQL has no `FULL` join (it offers only `LEFT`/`RIGHT` outer joins),
1040 /// so it is off there: the `FULL` join-side keyword is not consumed, and an
1041 /// already-aliased factor followed by `FULL [OUTER] JOIN` (`a x FULL OUTER JOIN b`) is
1042 /// then a clean parse error — exactly what MySQL reports (engine-measured-rejected on
1043 /// mysql:8). Because `FULL` is a non-reserved word in MySQL, a *bare* `a full JOIN b`
1044 /// still reads `full` as the factor's alias (grabbed before the join loop), matching the
1045 /// engine — this flag only governs the join-side reading, never the alias.
1046 pub full_outer_join: bool,
1047 /// Accept `NATURAL CROSS JOIN` — `NATURAL` prefixing the `CROSS` join keyword.
1048 /// SQLite's `join-operator` validates its up-to-three keywords post-hoc and admits
1049 /// `NATURAL` before any join type, `CROSS` included; PostgreSQL and DuckDB both
1050 /// parse-reject it (engine-probed). Because `CROSS JOIN` is the optimizer-hint
1051 /// spelling of `INNER JOIN` and `NATURAL` supplies the shared-column constraint,
1052 /// `NATURAL CROSS JOIN` is semantically a natural inner join — engine-probed on
1053 /// rusqlite: it yields the shared-column equijoin's row/column shape, not the cross
1054 /// product — so the parser normalizes it into the canonical
1055 /// [`JoinOperator::Inner`](crate::ast::JoinOperator) + [`JoinConstraint::Natural`](crate::ast::JoinConstraint)
1056 /// shape (the `NATURAL INNER` precedent, where the redundant side keyword is likewise
1057 /// dropped), rendering back as `NATURAL JOIN`. No new AST field is needed: the
1058 /// round-trip oracle compares structure, not spelling, so the elided `CROSS` word
1059 /// re-parses to the same AST. On for SQLite / Lenient, off elsewhere; when off,
1060 /// `NATURAL CROSS` fails the `NATURAL` arm's mandatory `JOIN` and rejects unchanged.
1061 pub natural_cross_join: bool,
1062 /// Accept MySQL's `STRAIGHT_JOIN` join-order hint, in both of its surfaces: the
1063 /// join operator `a STRAIGHT_JOIN b [ON ...]` and the `SELECT STRAIGHT_JOIN ...`
1064 /// modifier. One flag gates both grammar points because they are a single dialect
1065 /// unit — MySQL always admits the modifier wherever it admits the join keyword
1066 /// (mirroring how [`CreateTableClauseSyntax::table_options`] gates the trailing options
1067 /// and the `AUTO_INCREMENT` attribute together). Both fold into the
1068 /// canonical inner-join shape / `Select` flag with a surface tag, never a new node.
1069 /// When off, `STRAIGHT_JOIN` is left to the identifier grammar (a non-reserved word
1070 /// under ANSI/PostgreSQL), so the MySQL construct is a clean parse divergence.
1071 pub straight_join: bool,
1072 /// Accept DuckDB's `ASOF [INNER|LEFT|RIGHT|FULL [OUTER]] JOIN` inexact-match
1073 /// temporal join ([`JoinOperator::AsOf`](crate::ast::JoinOperator)). When off,
1074 /// `ASOF` is left to the identifier grammar (a non-reserved word under
1075 /// ANSI/PostgreSQL) — and because the next word is `JOIN`, the text still parses
1076 /// there, as an aliased *plain* join (a different-tree reading, unlike
1077 /// `STRAIGHT_JOIN`'s leftover-input reject). The flag alone is not enough for the
1078 /// DuckDB meaning on a bare table factor — the word must also be in
1079 /// [`FeatureSet::reserved_column_name`] or the factor's alias swallows it first
1080 /// (the DuckDb preset reserves it; `LENIENT` keeps the ANSI reserved model, so
1081 /// there the join parses only after an explicit alias). On for DuckDb / Lenient,
1082 /// off elsewhere.
1083 pub asof_join: bool,
1084 /// Accept DuckDB's `POSITIONAL JOIN` row-position pairing join
1085 /// ([`JoinOperator::Positional`](crate::ast::JoinOperator)), which takes no
1086 /// `ON`/`USING` constraint and no side keyword. The same reserved-word
1087 /// interaction as [`asof_join`](JoinSyntax::asof_join) applies. On for DuckDb /
1088 /// Lenient, off elsewhere.
1089 pub positional_join: bool,
1090 /// Accept DuckDB's `SEMI JOIN` / `ANTI JOIN` semi-/anti-join operators
1091 /// ([`JoinOperator::Semi`](crate::ast::JoinOperator)/[`Anti`](crate::ast::JoinOperator::Anti)),
1092 /// including their `NATURAL` and `ASOF` compositions (`NATURAL SEMI JOIN`,
1093 /// `ASOF ANTI JOIN`). One flag gates both because `SEMI` and `ANTI` are the same
1094 /// grammar production (a `join_type` keyword taking an `ON`/`USING` constraint) that
1095 /// DuckDB only ever ships together — the paired-flag doctrine (the
1096 /// [`straight_join`](JoinSyntax::straight_join) precedent), unlike the distinct-grammar
1097 /// [`asof_join`](JoinSyntax::asof_join)/[`positional_join`](JoinSyntax::positional_join) pair.
1098 /// The same reserved-word interaction as [`asof_join`](JoinSyntax::asof_join) applies: the
1099 /// DuckDb preset reserves both words as a `ColId`/bare-alias so a bare
1100 /// `l SEMI JOIN r` reads the join rather than aliasing `l`; `LENIENT` keeps the ANSI
1101 /// reserved model, so there the join parses only after an explicit alias. On for
1102 /// DuckDb / Lenient, off elsewhere.
1103 pub semi_anti_join: bool,
1104 /// Accept the Spark/Hive/Databricks *sided* semi-/anti-join spelling
1105 /// (`{LEFT|RIGHT} {SEMI|ANTI} JOIN`), recorded as the
1106 /// [`SemiAntiSide`](crate::ast::SemiAntiSide) axis on the same
1107 /// [`JoinOperator::Semi`](crate::ast::JoinOperator)/[`Anti`](crate::ast::JoinOperator::Anti)
1108 /// operators as DuckDB's side-less form. A *separate* gate from
1109 /// [`semi_anti_join`](JoinSyntax::semi_anti_join) because it is a different engine family:
1110 /// DuckDB parse-rejects `LEFT SEMI JOIN` (engine-probed), so folding the sided
1111 /// spelling into `semi_anti_join` would over-accept it under the DuckDb preset. The
1112 /// leading `LEFT`/`RIGHT` keyword is already a reserved join side, so — unlike the
1113 /// keyword-led [`asof_join`](JoinSyntax::asof_join) pair — no reserved-word interplay is
1114 /// needed: the preceding factor's alias can never swallow it, and a plain
1115 /// `LEFT [OUTER] JOIN` is disambiguated by the following `SEMI`/`ANTI` keyword. One
1116 /// flag gates the whole sided family (both sides × `SEMI`/`ANTI`) as one grammar
1117 /// production (the [`straight_join`](JoinSyntax::straight_join)/[`semi_anti_join`](JoinSyntax::semi_anti_join)
1118 /// paired-flag doctrine). The sided form always takes an `ON`/`USING` constraint and
1119 /// never composes with `NATURAL`/`ASOF`. On for the Databricks and Hive presets (whose
1120 /// engine family documents the spelling — Hive originated `LEFT SEMI JOIN`) and for
1121 /// Lenient (the permissive superset); off elsewhere, where the other engines
1122 /// parse-reject the sided spelling. The atomic flag admits the `RIGHT`-sided and `ANTI`
1123 /// spellings those presets do not all document — a known conservative-direction
1124 /// over-acceptance a future side-refinement would tighten.
1125 pub sided_semi_anti_join: bool,
1126 /// Accept MSSQL's `CROSS APPLY` / `OUTER APPLY` join operators
1127 /// ([`JoinOperator::Apply`](crate::ast::JoinOperator)) — a lateral-correlated join
1128 /// over a right table factor (derived table or table-valued function), taking no
1129 /// `ON`/`USING` constraint. One flag gates the whole `APPLY` family (both the
1130 /// `CROSS` and `OUTER` flavours) because they are a single grammar production
1131 /// differing only by that keyword (the [`straight_join`](JoinSyntax::straight_join)
1132 /// paired-flag doctrine; the [`ApplyKind`](crate::ast::ApplyKind) axis carries the
1133 /// flavour). No reserved-word interplay is needed — the leading `CROSS`/`OUTER`
1134 /// keyword already anchors the operator, so the preceding factor's alias can never
1135 /// swallow it (unlike the keyword-led [`asof_join`](JoinSyntax::asof_join) pair). On for
1136 /// the MSSQL preset and Lenient (the permissive superset), off elsewhere: the other
1137 /// engines parse-reject `APPLY` in join position.
1138 pub apply_join: bool,
1139 /// Accept the SQL:2023 recursive-query `SEARCH { DEPTH | BREADTH } FIRST BY … SET …`
1140 /// and `CYCLE … SET … [TO … DEFAULT …] USING …` clauses on a CTE
1141 /// ([`Cte::search`](crate::ast::Cte)/[`cycle`](crate::ast::Cte)), written after the
1142 /// CTE body's `)`. One flag gates both clauses because PostgreSQL ships them as a
1143 /// single recursive-query unit (the [`straight_join`](JoinSyntax::straight_join)
1144 /// paired-flag doctrine) — no dialect admits one without the other. When off, the
1145 /// `SEARCH`/`CYCLE` keyword after a CTE body is left unconsumed and the statement is a
1146 /// clean parse error. On for PostgreSQL / Lenient; off for ANSI (the conservative
1147 /// baseline, like [`unnest`](TableFactorSyntax::unnest)), MySQL/SQLite (no such clauses), and
1148 /// DuckDB — which parse-rejects both (`syntax error at or near "SEARCH"`, probed on
1149 /// 1.5.4), so it overrides the PostgreSQL surface it otherwise inherits (the
1150 /// [`MutationSyntax::data_modifying_ctes`] split precedent).
1151 pub recursive_search_cycle: bool,
1152 /// Parse-reject a top-level `ORDER BY` / `LIMIT` / `OFFSET` on a recursive CTE whose
1153 /// body is a `UNION [ALL]` set operation (`WITH RECURSIVE t AS (SELECT … UNION ALL
1154 /// SELECT … FROM t ORDER BY …)`). DuckDB special-cases the recursive term's grammar:
1155 /// once `WITH RECURSIVE` fronts a `UNION`-bodied CTE the query is a *recursive query*,
1156 /// and a result-shaping modifier on it is a parse error (`Parser Error: ORDER BY in a
1157 /// recursive query is not allowed` / `LIMIT or OFFSET in a recursive query is not
1158 /// allowed`; probed on 1.5.4). "order_limit" names the whole modifier set — `ORDER BY`,
1159 /// `LIMIT`, and `OFFSET` — because DuckDB forbids them under one rule (no dialect admits
1160 /// one but not the others), so it is one behaviour, not three.
1161 ///
1162 /// Three boundaries are load-bearing and engine-probed (1.5.4), so the check mirrors
1163 /// them exactly rather than firing on the bare `RECURSIVE` keyword: the body must be a
1164 /// `UNION` set operation (a non-set-op recursive CTE, or an `INTERSECT`/`EXCEPT` body,
1165 /// keeps its modifiers — those are not recursive-eligible); the modifier must sit on the
1166 /// set operation itself, not a parenthesized arm or a nested subquery (`((SELECT …
1167 /// LIMIT 1) UNION ALL …)` and `… WHERE x < (SELECT … ORDER BY 1)` both accept); and
1168 /// self-reference is *not* required (DuckDB rejects even a `UNION` whose right arm never
1169 /// names the CTE — the check is syntactic). On for DuckDB only; every other preset
1170 /// parse-accepts the modifier (PostgreSQL admits it outright, the rest defer the
1171 /// restriction to binding), so it stays off there and the modifier rides the ordinary
1172 /// query tail.
1173 pub recursive_union_rejects_order_limit: bool,
1174 /// Accept DuckDB's `USING KEY (col, ...)` recursive-CTE key clause
1175 /// ([`Cte::using_key`](crate::ast::Cte)), written between the CTE column list and `AS`
1176 /// (`WITH RECURSIVE t(a, b) USING KEY (a) AS (…)`). DuckDB's keyed-recursion variant
1177 /// (stable since 1.3; probed accepting on 1.5.4): the recurring table becomes a
1178 /// key-indexed dictionary whose rows the recursive term overwrites in place. A distinct
1179 /// gate from [`recursive_search_cycle`](Self::recursive_search_cycle) — a different
1180 /// engine (DuckDB, not PostgreSQL) and a different grammar slot (before `AS`, not after
1181 /// the body's `)`), so neither implies the other. Positioned ahead of `AS`, the leading
1182 /// `USING` cannot be swallowed by any prior production (a CTE otherwise expects `AS`
1183 /// next), so enabling it shadows no existing spelling. On for DuckDB / Lenient (the
1184 /// permissive superset); off elsewhere, where the clause is a parse error.
1185 pub recursive_using_key: bool,
1186}
1187
1188/// Dialect-owned table-factor syntax accepted by the parser.
1189///
1190/// The `FROM`-item factor forms beyond a plain named table. Split out of
1191/// [`TableExpressionSyntax`] at its 16-field line as the table-factor axis, distinct from
1192/// the join and factor-tail/alias cores. Each flag is a grammar gate: when off the factor
1193/// keyword falls through to the named-table path or surfaces as a clean parse error.
1194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1195pub struct TableFactorSyntax {
1196 /// Accept `LATERAL` before derived tables and table functions.
1197 pub lateral: bool,
1198 /// Accept function calls as `FROM` items.
1199 pub table_functions: bool,
1200 /// Accept PostgreSQL `ROWS FROM (...)` table functions.
1201 pub rows_from: bool,
1202 /// Accept the first-class `UNNEST(<expr>[, <expr>…])` table factor
1203 /// ([`TableFactor::Unnest`](crate::ast::TableFactor)) — an array/collection
1204 /// expression expanded into a relation, with optional `WITH ORDINALITY`, a
1205 /// correlation alias, and (under [`unnest_with_offset`](TableFactorSyntax::unnest_with_offset))
1206 /// a BigQuery `WITH OFFSET` tail. On for PostgreSQL/DuckDB/Lenient (each admits
1207 /// `FROM unnest(…)`); off for ANSI/MySQL/SQLite, where `UNNEST(` is left to the
1208 /// named-table path and — with those presets' [`table_functions`](TableFactorSyntax::table_functions)
1209 /// also off — surfaces as the same clean parse error as any other function-in-FROM.
1210 /// Distinct from [`table_functions`](TableFactorSyntax::table_functions): a dialect can admit
1211 /// generic table functions yet route `UNNEST` to this dedicated node.
1212 pub unnest: bool,
1213 /// Accept the BigQuery/ZetaSQL `WITH OFFSET [AS <alias>]` tail on an
1214 /// [`unnest`](TableFactorSyntax::unnest) table factor — a 0-based ordinal column, the BigQuery
1215 /// counterpart of PostgreSQL's `WITH ORDINALITY`; the dependency is
1216 /// [`FeatureDependencyViolation::UnnestWithOffsetWithoutUnnest`]. On for the BigQuery
1217 /// preset alone — the first shipped dialect to enable it; off for every oracle-compared
1218 /// preset (PostgreSQL and DuckDB both parse-*reject* `WITH OFFSET`, engine-probed) and
1219 /// off for Lenient, and BigQuery carries no differential oracle. When off, a
1220 /// trailing `WITH OFFSET` is left unconsumed and the statement rejects.
1221 pub unnest_with_offset: bool,
1222 /// Accept a `WITH ORDINALITY` tail on a table-valued `FROM` source — the generic
1223 /// function factor, an [`unnest`](TableFactorSyntax::unnest) factor, and a
1224 /// [`rows_from`](TableFactorSyntax::rows_from) factor — adding a trailing ordinal column. On for
1225 /// PostgreSQL/DuckDB/Lenient; off for SQLite, whose grammar admits generic
1226 /// [`table_functions`](TableFactorSyntax::table_functions) but syntax-rejects `WITH ORDINALITY`
1227 /// (engine-probed: `FROM pragma_table_info('t') WITH ORDINALITY` errors at
1228 /// `ORDINALITY`). When off, the trailing `WITH ORDINALITY` is left unconsumed and the
1229 /// statement rejects. Distinct from [`unnest_with_offset`](TableFactorSyntax::unnest_with_offset),
1230 /// the BigQuery `WITH OFFSET` counterpart.
1231 pub table_function_ordinality: bool,
1232 /// Accept a bare SQL special value function (`current_date`, `current_timestamp`,
1233 /// `current_user`, …) as a `FROM` table source — PostgreSQL's `func_table`
1234 /// promotion of a `SQLValueFunction` ([`TableFactor::SpecialFunction`](crate::ast::TableFactor)),
1235 /// e.g. `SELECT * FROM current_date`. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient.
1236 /// MySQL has no such promotion — `current_date`/`current_timestamp` are reserved and a
1237 /// bare one in table position is an `ER_PARSE_ERROR` on mysql:8 — so it is off there,
1238 /// and the special-function keyword then falls through to the named-table path where the
1239 /// reserved-word gate rejects it (exactly as the alias position already does).
1240 pub special_function_table_source: bool,
1241 /// Accept DuckDB's `PIVOT` operator ([`Pivot`](crate::ast::Pivot)) in both of its
1242 /// surfaces: the `<source> PIVOT (<aggs> FOR <col> IN (<vals>))` table factor and
1243 /// the leading-keyword `PIVOT <source> ON … USING …` statement. One flag gates both
1244 /// grammar points because they are a single dialect unit — DuckDB always admits the
1245 /// statement wherever it admits the table factor (the [`straight_join`](JoinSyntax::straight_join)
1246 /// precedent). The flag alone is not enough on a bare table factor: `PIVOT` must
1247 /// also be in [`FeatureSet::reserved_bare_alias`] or the source's alias swallows it
1248 /// first (the DuckDb preset reserves it, class `reserved` like `QUALIFY`). On for
1249 /// DuckDb / Lenient, off elsewhere.
1250 pub pivot: bool,
1251 /// Accept DuckDB's `UNPIVOT` operator ([`Unpivot`](crate::ast::Unpivot)) in both its
1252 /// table-factor and leading-keyword-statement surfaces — the [`pivot`](TableFactorSyntax::pivot)
1253 /// counterpart, kept a separate flag because `PIVOT` and `UNPIVOT` are distinct
1254 /// operators (the [`asof_join`](JoinSyntax::asof_join)/[`positional_join`](JoinSyntax::positional_join)
1255 /// precedent). The same reserved-word interaction applies. On for DuckDb / Lenient,
1256 /// off elsewhere.
1257 pub unpivot: bool,
1258 /// Accept DuckDB's `DESCRIBE`/`SHOW`/`SUMMARIZE` utility as a parenthesized `FROM`
1259 /// table source — DuckDB's `SHOW_REF` table reference
1260 /// ([`TableFactor::ShowRef`](crate::ast::TableFactor)), e.g.
1261 /// `FROM (DESCRIBE SELECT …)`, `FROM (SHOW databases)`. One flag gates all three
1262 /// keywords because DuckDB models them as a single `SHOW_REF` production (the
1263 /// paired-flag doctrine). When off, the leading keyword is left to the query/join
1264 /// grammar inside the parentheses and the construct is a clean parse divergence
1265 /// (`FROM (DESCRIBE …)` reads `DESCRIBE` as neither a query start nor a joined
1266 /// table). Only the table-source position is admitted — DuckDB parse-rejects these
1267 /// at CTE-body position — so, unlike [`pivot`](TableFactorSyntax::pivot), there is no query-body
1268 /// or leading-keyword-statement surface. On for DuckDb / Lenient, off elsewhere.
1269 pub show_ref: bool,
1270 /// Accept DuckDB's bare `FROM VALUES (<row>, …) AS <alias>` row-list table factor —
1271 /// a `VALUES` constructor standing directly as a table factor *without* the wrapping
1272 /// parentheses the standard derived table requires
1273 /// ([`TableFactor::Derived`](crate::ast::TableFactor) tagged
1274 /// [`DerivedSpelling::BareValues`](crate::ast::DerivedSpelling)). DuckDB
1275 /// parse-requires a table alias here (a bare `FROM VALUES (1)` is a syntax error;
1276 /// `FROM VALUES (1) t` accepts — probed on 1.5.4), so the parser rejects a missing
1277 /// one. When off, `VALUES` in table-factor position is not a table name and the
1278 /// construct is a clean parse divergence (the reject the other dialects give). The
1279 /// parenthesized `FROM (VALUES …)` derived table is separate and always accepted. On
1280 /// for DuckDb / Lenient, off elsewhere.
1281 pub from_values: bool,
1282 /// Accept the SQL/JSON `JSON_TABLE(context, path [AS name] [PASSING …] COLUMNS (…)
1283 /// [… ON ERROR])` table factor ([`TableFactor::JsonTable`](crate::ast::TableFactor)). On
1284 /// for PostgreSQL/Lenient. Off elsewhere: DuckDB parse-rejects it, and MySQL's `JSON_TABLE`
1285 /// has a *different* grammar (kept off so this PG-shaped node never fires for it). When
1286 /// off, `JSON_TABLE(` falls to the ordinary function/name path; reached only when the
1287 /// keyword is immediately followed by `(`.
1288 pub json_table: bool,
1289 /// Accept the SQL/XML `XMLTABLE([XMLNAMESPACES(…),] row PASSING doc COLUMNS …)` table
1290 /// factor ([`TableFactor::XmlTable`](crate::ast::TableFactor)). On for PostgreSQL/Lenient,
1291 /// off elsewhere (DuckDB parse-rejects it; MySQL/SQLite/ANSI have no such form). When off,
1292 /// `XMLTABLE(` falls to the ordinary function/name path; reached only when the keyword is
1293 /// immediately followed by `(`.
1294 pub xml_table: bool,
1295 /// Accept `TABLE(<expr>)` as a first-class `FROM` table factor
1296 /// ([`TableFactor::TableExpr`](crate::ast::TableFactor)) — sqlparser-rs's
1297 /// `TableFunction`. Distinct from [`table_functions`](TableFactorSyntax::table_functions) (a
1298 /// *named* table function, `FROM f(1)`) and from the standalone `TABLE t` query
1299 /// form, which is not a `FROM`-position factor at all. Snowflake and Oracle are the
1300 /// engines that document this shape, but neither ships a differential oracle here,
1301 /// so over-acceptance is unmeasurable — the conservative-preset family rule keeps
1302 /// this off everywhere but Lenient (the permissive superset), with no oracle-backed
1303 /// preset enabling it. When off, `TABLE(` in table-factor position falls through to
1304 /// the named-table path, where the reserved `TABLE` keyword is not an admissible
1305 /// relation name and the construct is a clean parse error.
1306 pub table_expr_factor: bool,
1307 /// Accept the SQL-standard PIVOT table factor's extended value sources and default —
1308 /// the Snowflake/BigQuery/Oracle grammar layered on the shared
1309 /// `<source> PIVOT (<aggs> FOR <col> IN (…))` shape: the `IN (ANY [ORDER BY …])`
1310 /// wildcard and `IN (<subquery>)` value sources
1311 /// ([`PivotValueSource`](crate::ast::PivotValueSource)) and the Snowflake
1312 /// `DEFAULT ON NULL (<expr>)` tail ([`Pivot::default_on_null`](crate::ast::Pivot)).
1313 /// Also the reachability gate for the standard single-`FOR`-column table-factor
1314 /// PIVOT itself where the DuckDB [`pivot`](Self::pivot) flag is off — so with `pivot`
1315 /// off and this on the parser reads the table-factor PIVOT but *not* the DuckDB
1316 /// leading-keyword statement / query-body / `IN <enum>` forms, and stops the
1317 /// bare-chained multi-`FOR`-column list at one head (the standard admits exactly one).
1318 /// On for BigQuery / Snowflake / Lenient — none oracle-compared here, so
1319 /// over-acceptance is unmeasurable and the conservative-preset family rule keeps it
1320 /// off elsewhere. Like [`pivot`](Self::pivot), a *bare*-factor standard PIVOT is
1321 /// reachable only where `PIVOT` is in [`FeatureSet::reserved_bare_alias`]; where it is
1322 /// not (BigQuery/Snowflake today), the suffix fires after an explicit alias — the
1323 /// `pivot`/`ASOF` reservation-dependency precedent.
1324 ///
1325 /// It doubles as the reachability gate for the standard `UNPIVOT` table factor where
1326 /// the DuckDB [`unpivot`](Self::unpivot) flag is off: PIVOT and UNPIVOT co-travel in
1327 /// these engines' grammars (every dialect with the standard PIVOT table factor also
1328 /// has UNPIVOT), so one flag reaches both rather than a redundant sibling that no
1329 /// dialect would ever toggle apart. The `UNPIVOT` table factor grammar is fully
1330 /// shared — the `INCLUDE`/`EXCLUDE NULLS` marker, per-column aliases, and multi-column
1331 /// value/name lists are all DuckDB fields BigQuery/Snowflake reuse — so this gate adds
1332 /// no new UNPIVOT syntax, only reachability; the same explicit-alias reachability note
1333 /// applies to a bare-factor standard UNPIVOT.
1334 pub pivot_value_sources: bool,
1335 /// Accept the SQL:2016 `<source> MATCH_RECOGNIZE (…)` row-pattern-recognition table
1336 /// factor ([`MatchRecognize`](crate::ast::MatchRecognize)) — the Snowflake / Oracle
1337 /// row-pattern-matching clause, with its `PARTITION BY` / `ORDER BY` / `MEASURES` /
1338 /// `ONE|ALL ROWS PER MATCH` / `AFTER MATCH SKIP` / `PATTERN (…)` / `SUBSET` / `DEFINE`
1339 /// subclauses. On for Snowflake (documented; no differential oracle, so
1340 /// over-acceptance is unmeasurable and the conservative-preset family rule keeps it
1341 /// off elsewhere) and Lenient (the permissive superset). Oracle is not a shipped
1342 /// preset, so it does not enable this. Like [`pivot`](Self::pivot), a *bare*-factor
1343 /// `MATCH_RECOGNIZE` is reachable only where the keyword is in
1344 /// [`FeatureSet::reserved_bare_alias`]; where it is not, the suffix fires after an
1345 /// explicit alias — the `pivot`/`ASOF` reservation-dependency precedent. When off,
1346 /// `MATCH_RECOGNIZE` falls through to the named-table/alias path and the construct is
1347 /// a clean parse divergence.
1348 pub match_recognize: bool,
1349 /// Accept SQL Server's `OPENJSON(<json> [, <path>]) [WITH (<col> <type> [<path>] [AS JSON],
1350 /// …)]` rowset-function table factor ([`OpenJson`](crate::ast::OpenJson)) — a JSON document
1351 /// parsed into a relation, either with the default `key`/`value`/`type` schema (no `WITH`)
1352 /// or an explicit column schema. On for MSSQL (documented — SQL Server is the sole engine
1353 /// with this exact form; no differential oracle ships, so over-acceptance is unmeasurable
1354 /// and the conservative-preset family rule keeps it off elsewhere) and Lenient (the
1355 /// permissive superset). `OPENJSON` is unreserved (a rowset function name), so this fires
1356 /// only when the keyword is immediately followed by `(`; a bare `OPENJSON` stays an ordinary
1357 /// relation name. When off, `OPENJSON(` falls to the ordinary function/name path, which
1358 /// rejects at the `WITH (…)` clause tail — the [`json_table`](Self::json_table) precedent.
1359 ///
1360 /// Docs: <https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql>.
1361 pub open_json: bool,
1362}
1363
1364/// Dialect-owned mutation-statement (`INSERT`/`UPDATE`/`DELETE`) syntax extensions.
1365///
1366/// These cover the non-ANSI tails the standard expresses differently (the standard
1367/// upsert is `MERGE`, and it has no `RETURNING`): they are explicit dialect data
1368/// so a parser gate never type-checks the dialect. Each flag is purely a
1369/// grammar gate — when off, the keyword is left unconsumed and the trailing clause
1370/// surfaces as a parse error, which is how ANSI rejects PostgreSQL upserts.
1371#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1372pub struct MutationSyntax {
1373 /// Accept the post-verb `INSERT IGNORE` modifier.
1374 pub insert_ignore: bool,
1375 /// Accept the post-verb `INSERT OVERWRITE` modifier.
1376 pub insert_overwrite: bool,
1377 /// Accept a `RETURNING <output> [, ...]` clause on `INSERT`/`UPDATE`/`DELETE`
1378 /// (PostgreSQL; also Oracle/MariaDB/SQLite).
1379 pub returning: bool,
1380 /// Accept an `INSERT ... ON CONFLICT [<target>] DO {NOTHING | UPDATE ...}`
1381 /// upsert clause (PostgreSQL; also SQLite).
1382 pub on_conflict: bool,
1383 /// Accept an `INSERT ... ON DUPLICATE KEY UPDATE <col> = <expr> [, ...]` upsert
1384 /// clause (MySQL/MariaDB). MySQL infers the conflicting unique key, so there is
1385 /// no arbiter and no `DO NOTHING`; this also admits the `VALUES(<col>)` reference
1386 /// to a column's proposed insert value inside those update expressions.
1387 pub on_duplicate_key_update: bool,
1388 /// Accept `UPDATE ... SET ( col, ... ) = <source>` multiple-column assignment
1389 /// (PostgreSQL; SQL feature T641). Also reached through `ON CONFLICT DO UPDATE`.
1390 pub multi_column_assignment: bool,
1391 /// Reject explicit value-row RHS tuple assignments whose element count differs from
1392 /// the target column count (`UPDATE t SET (a, b) = (1)`). This is a parse-time
1393 /// DuckDB arity check; PostgreSQL parses the same surface and leaves arity to later
1394 /// analysis, so the behavior is intentionally split from [`multi_column_assignment`](Self::multi_column_assignment).
1395 pub update_tuple_value_row_arity: bool,
1396 /// Accept `WHERE CURRENT OF <cursor>` positioned `UPDATE`/`DELETE`
1397 /// (PostgreSQL; SQL feature F831).
1398 pub where_current_of: bool,
1399 /// Accept the `MERGE INTO <target> USING <source> ON <cond> WHEN [NOT] MATCHED ...`
1400 /// statement (SQL:2003 feature F312, the standard upsert; PostgreSQL 15+). Unlike
1401 /// the other flags here this gates a *leading* statement keyword, not a trailing
1402 /// clause: when off, `MERGE` is not dispatched and surfaces as an unknown
1403 /// statement, which is how MySQL (no `MERGE`) rejects it.
1404 pub merge: bool,
1405 /// Accept the MySQL `REPLACE [INTO] <table> ...` delete-then-insert statement.
1406 /// Like `merge` this gates a *leading* statement keyword: when off (ANSI /
1407 /// PostgreSQL), `REPLACE` is not dispatched and surfaces as an unknown statement.
1408 /// `REPLACE` reuses the `INSERT` tail grammar tagged
1409 /// [`InsertVerb::Replace`](crate::ast::InsertVerb), so it needs no node of its own.
1410 pub replace_into: bool,
1411 /// Accept the MySQL `INSERT`/`REPLACE ... SET <col> = <value> [, ...]`
1412 /// assignment-list source ([`InsertSource::Set`](crate::ast::InsertSource)). When
1413 /// off (ANSI / PostgreSQL), `SET` after the target is left unconsumed and surfaces
1414 /// as a parse error, the same reject mechanism the other trailing-clause gates use.
1415 pub insert_set: bool,
1416 /// Accept the MySQL single-table `UPDATE`/`DELETE ... [ORDER BY <keys>] [LIMIT
1417 /// <count>]` row-limiting tails. One flag gates both clauses on both statements
1418 /// because they are a single dialect unit — MySQL admits the `ORDER BY` only
1419 /// alongside the `LIMIT` it orders, and on `UPDATE` exactly as on `DELETE`
1420 /// (mirroring how `table_options` gates the trailing options and column attribute
1421 /// together). The multi-table `UPDATE`/`DELETE` forms take no such tail, so this is
1422 /// the single-table grammar only. Off in ANSI/PostgreSQL, which have neither tail:
1423 /// there the trailing `ORDER BY`/`LIMIT` is left unconsumed and surfaces as a clean
1424 /// parse error.
1425 pub update_delete_tails: bool,
1426 /// Accept joins in the target position of `UPDATE`/`DELETE` and comma-separated
1427 /// `DELETE FROM` targets.
1428 pub joined_update_delete: bool,
1429 /// Accept the SQLite `INSERT OR <action>` / `UPDATE OR <action>` conflict-resolution
1430 /// prefix on the mutation verb, where `<action>` is `REPLACE`/`IGNORE`/`ABORT`/`FAIL`/
1431 /// `ROLLBACK` ([`ConflictResolution`](crate::ast::ConflictResolution), the [`Insert::or_action`](crate::ast::Insert)
1432 /// / [`Update::or_action`](crate::ast::Update) slot). SQLite only. Distinct from the
1433 /// MySQL `INSERT IGNORE` surface (a bare post-verb `IGNORE`, no `OR`) — that is not
1434 /// absorbed here. Off in ANSI/PostgreSQL/MySQL, where the `OR` after the verb is left
1435 /// unconsumed and surfaces as a clean parse error.
1436 pub or_conflict_action: bool,
1437 /// Accept DuckDB `INSERT INTO t BY NAME|BY POSITION …` column-matching mode
1438 /// between the target and the source. When off, `BY` is left unconsumed.
1439 pub insert_column_matching: bool,
1440 /// Accept the `DELETE FROM <target> USING <from-list> …` multi-relation delete
1441 /// (PostgreSQL; also MySQL's multi-table delete). On for
1442 /// ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no `USING` on `DELETE`
1443 /// (engine-measured-rejected via rusqlite), so it is off; the `USING` keyword is then
1444 /// left unconsumed and the trailing relation list surfaces as a clean parse error.
1445 pub delete_using: bool,
1446 /// Accept a PostgreSQL/SQLite `UPDATE … SET … FROM <table refs>` additional-relations
1447 /// clause. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL has no `UPDATE … FROM` —
1448 /// its multi-table update lists every table in the target position
1449 /// (`UPDATE t1, t2 SET …`) — so `UPDATE t SET … FROM u` is an `ER_PARSE_ERROR` on
1450 /// mysql:8; with the flag off the `FROM` keyword is left unconsumed and surfaces as a
1451 /// clean parse error.
1452 pub update_from: bool,
1453 /// Accept an alias on the *target* of a `DELETE FROM <target> USING <table_refs>`
1454 /// multi-table delete (`DELETE FROM t AS e USING u …`). On for ANSI/PostgreSQL/
1455 /// DuckDB/Lenient. MySQL's `DELETE FROM tbl … USING …` names the delete target(s) as
1456 /// bare table names that must match the `USING` relations, so an alias there is an
1457 /// `ER_PARSE_ERROR` on mysql:8 (`DELETE FROM event AS e USING sales …` rejects, while a
1458 /// single-table `DELETE FROM event AS e WHERE …` — no `USING` — parses); it is off for
1459 /// MySQL. Only the `USING`-form target is governed — the plain single-table delete's
1460 /// alias is unaffected. Independent of [`delete_using`](Self::delete_using), which gates
1461 /// whether the `USING` clause is admitted at all.
1462 pub delete_using_target_alias: bool,
1463 /// Accept a leading `WITH` CTE clause before an `INSERT` statement
1464 /// (`WITH a AS (…) INSERT INTO t …`). On for ANSI/PostgreSQL/DuckDB/Lenient. MySQL
1465 /// admits a statement-leading `WITH` before `SELECT`/`UPDATE`/`DELETE` but *not* before
1466 /// `INSERT` (its CTE for an insert rides the `INSERT … SELECT` source instead:
1467 /// `INSERT INTO t WITH … SELECT …`), so `WITH … INSERT …` is an `ER_PARSE_ERROR` on
1468 /// mysql:8; it is off for MySQL and the `INSERT` after the CTE list then surfaces as a
1469 /// clean parse error. The bare `INSERT` with no leading `WITH` is unaffected.
1470 pub cte_before_insert: bool,
1471 /// Accept a leading `WITH` CTE clause before a `MERGE` statement
1472 /// (`WITH a AS (…) MERGE INTO t USING a …`; PostgreSQL 15+, DuckDB — probed on
1473 /// 1.5.4). Off for ANSI: SQL:2016's `<merge statement>` takes no `<with clause>`
1474 /// (unlike an `INSERT`, whose source query carries one), so a leading `WITH`
1475 /// before `MERGE` is a dialect extension, not standard surface. The `MERGE`
1476 /// counterpart of [`cte_before_insert`](Self::cte_before_insert): when off, the
1477 /// `MERGE` after the CTE list surfaces as a clean parse error, and the bare
1478 /// `MERGE` (no leading `WITH`) is unaffected. Only reachable where
1479 /// [`merge`](Self::merge) dispatches `MERGE` at all (the dependency is
1480 /// [`FeatureDependencyViolation::CteBeforeMergeWithoutMerge`]).
1481 pub cte_before_merge: bool,
1482 /// Accept a data-modifying statement — `INSERT`/`UPDATE`/`DELETE`/`MERGE`, the
1483 /// `MERGE` arm PG 17+ — as a CTE body
1484 /// (`WITH t AS (DELETE FROM x RETURNING *) SELECT * FROM t`;
1485 /// [`CteBody`](crate::ast::CteBody)). PostgreSQL admits the DML body at *every*
1486 /// `WITH` site during raw parsing — nested subquery/scalar-subquery `WITH`s,
1487 /// `DECLARE CURSOR`/`CREATE TABLE AS`/`CREATE VIEW`/`EXPLAIN`/`COPY (…) TO`
1488 /// bodies (all probed on pg_query 17; misuse is rejected at analysis, past the
1489 /// parse boundary this crate models) — so the gate governs the one shared CTE
1490 /// grammar uniformly rather than per placement. Off everywhere else: DuckDB
1491 /// parse-rejects a DML CTE body (`A CTE needs a SELECT`, probed on 1.5.4), MySQL
1492 /// is an `ER_PARSE_ERROR` (probed on mysql:8), SQLite rejects, and the SQL
1493 /// standard has no data-modifying `WITH`; there the DML keyword after `AS (` is
1494 /// not dispatched and surfaces as the ordinary query-body parse error.
1495 pub data_modifying_ctes: bool,
1496 /// Accept the `WHEN NOT MATCHED BY {SOURCE | TARGET}` `MERGE` arms (PostgreSQL 17+,
1497 /// DuckDB — both probed). `NOT MATCHED BY TARGET` is the bare `NOT MATCHED` (an
1498 /// unpaired source row → insert); `NOT MATCHED BY SOURCE` is the new arm firing on an
1499 /// unpaired *target* row (→ update/delete, like `MATCHED`). Off in ANSI: SQL:2016's
1500 /// `<merge when clause>` is only `MATCHED`/`NOT MATCHED`, so the `BY` after `NOT
1501 /// MATCHED` is not standard surface — with the flag off it is left unconsumed and the
1502 /// clause surfaces as a clean parse error. Only reachable where [`merge`](Self::merge)
1503 /// dispatches `MERGE` at all (the dependency is
1504 /// [`FeatureDependencyViolation::MergeWhenNotMatchedByWithoutMerge`]); the bare
1505 /// `WHEN NOT MATCHED` is unaffected.
1506 pub merge_when_not_matched_by: bool,
1507 /// Accept the `MERGE ... WHEN NOT MATCHED THEN INSERT DEFAULT VALUES` action — a
1508 /// column-default row taking neither a column list nor an `OVERRIDING` clause
1509 /// (PostgreSQL, DuckDB — both probed). Off in ANSI: SQL:2016's
1510 /// `<merge insert specification>` is `INSERT [cols] [override] VALUES (...)` with no
1511 /// `DEFAULT VALUES` alternative, so with the flag off the `DEFAULT` after `INSERT`
1512 /// surfaces as a clean parse error. Distinct from the top-level `INSERT ... DEFAULT
1513 /// VALUES` source, which every dialect admits. Only reachable where
1514 /// [`merge`](Self::merge) dispatches `MERGE` at all (the dependency is
1515 /// [`FeatureDependencyViolation::MergeInsertDefaultValuesWithoutMerge`]).
1516 pub merge_insert_default_values: bool,
1517 /// Accept the `MERGE ... WHEN NOT MATCHED THEN INSERT ... OVERRIDING {SYSTEM | USER}
1518 /// VALUE` identity override on the merge insert action (SQL:2016
1519 /// `<merge insert specification>`'s `<override clause>`; PostgreSQL). Unlike the
1520 /// other two merge extensions here this is *standard* surface, so it is on for ANSI —
1521 /// but DuckDB rejects `OVERRIDING` inside `MERGE` (`syntax error at or near
1522 /// "OVERRIDING"`, probed on 1.5.4) while accepting it on a top-level `INSERT`, so its
1523 /// preset splits from the shared top-level [`InsertOverriding`](crate::ast::InsertOverriding)
1524 /// grammar in exactly this knob. Off for DuckDB/MySQL; with the flag off the
1525 /// `OVERRIDING` between the column list and `VALUES` is left unconsumed and surfaces
1526 /// as a clean parse error. Only reachable where [`merge`](Self::merge) dispatches
1527 /// `MERGE` at all (the dependency is
1528 /// [`FeatureDependencyViolation::MergeInsertOverridingWithoutMerge`]).
1529 pub merge_insert_overriding: bool,
1530 /// Accept additional comma-separated value rows in a `MERGE ... THEN INSERT` action.
1531 pub merge_insert_multirow: bool,
1532 /// Accept DuckDB `UPDATE SET *` in a MERGE WHEN MATCHED arm (column-wise copy
1533 /// from the source). Off in ANSI/PostgreSQL.
1534 pub merge_update_set_star: bool,
1535 /// Accept DuckDB `INSERT *` / `INSERT BY NAME [*]` merge insert spellings.
1536 /// Off in ANSI/PostgreSQL (only the standard column-list / VALUES form).
1537 pub merge_insert_star_by_name: bool,
1538 /// Accept DuckDB `THEN ERROR` as a MERGE action (runtime raises when the arm
1539 /// fires). Off in ANSI/PostgreSQL.
1540 pub merge_error_action: bool,
1541 /// Accept a multi-part (qualified) column name as an `UPDATE … SET` assignment target
1542 /// (`UPDATE t SET t.i = 1` / `schema.t.col = …`). On for ANSI/PostgreSQL/MySQL/SQLite/Lenient.
1543 /// DuckDB parse-rejects qualified SET targets ("Qualified column names in UPDATE .. SET
1544 /// not supported", probed on 1.5.4), so it is off there; with the flag off a target whose
1545 /// [`ObjectName`](crate::ast::ObjectName) has more than one part surfaces as a clean parse
1546 /// error. A bare single-part column remains free either way.
1547 pub update_set_qualified_column: bool,
1548}
1549
1550/// Dialect-owned whole-statement DDL dispatch gates accepted by the parser.
1551/// Dialect-owned view/sequence *clause* syntax accepted after a DDL statement head.
1552///
1553/// Post-dispatch refinements on `CREATE VIEW` / `CREATE MATERIALIZED VIEW` / `CREATE SEQUENCE`
1554/// — temporary views, recursive views, view `WITH` options, matview `TO` target, sequence
1555/// `CACHE` — split out of [`StatementDdlGates`] so whole-statement object-kind gates stay
1556/// separate from clause-level grammar. Statement-head flags (`create_sequence`,
1557/// `materialized_views`, `or_replace`, …) remain on [`StatementDdlGates`].
1558#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1559pub struct ViewSequenceClauseSyntax {
1560 /// Accept `CACHE <n>` in the option list of `CREATE SEQUENCE`.
1561 pub create_sequence_cache: bool,
1562 /// Accept a materialized-view storage target: `CREATE MATERIALIZED VIEW name TO target AS ...`.
1563 pub materialized_view_to: bool,
1564 /// Accept the `TEMP`/`TEMPORARY` modifier on a plain `CREATE [OR REPLACE] VIEW`
1565 /// (PostgreSQL/SQLite/DuckDB spell session-local views). On for ANSI/PostgreSQL/SQLite/
1566 /// DuckDB/Lenient. MySQL has no temporary views (only temporary *tables*), so it is off;
1567 /// a consumed `TEMP`/`TEMPORARY` prefix leading into `VIEW` is then the syntax error
1568 /// MySQL reports (engine-measured-rejected on mysql:8). Gates only the view surface — the
1569 /// `CREATE TEMPORARY TABLE` form is a separate, unaffected family.
1570 pub temporary_views: bool,
1571 /// Accept the `RECURSIVE` keyword before `VIEW` in `CREATE [OR REPLACE]
1572 /// [TEMP|TEMPORARY] RECURSIVE VIEW <name> (<columns>) AS <query>` (DuckDB,
1573 /// engine-measured on duckdb 1.5.4). On for DuckDB/Lenient only — although
1574 /// PostgreSQL spells the same form, it is gated to the measured dialect per the
1575 /// no-shadowing doctrine rather than widened to the PostgreSQL reference without a
1576 /// differential. Off elsewhere, where the `RECURSIVE` keyword is left unconsumed
1577 /// before the expected `VIEW` and surfaces as a clean parse error. The keyword sits
1578 /// between the `TEMP`/`TEMPORARY` prefix and `VIEW`, never composes with
1579 /// `MATERIALIZED`, and requires the explicit column list (the engine desugars a
1580 /// recursive view to `WITH RECURSIVE`, which names its output columns).
1581 pub recursive_views: bool,
1582 /// Accept MySQL's view definition-option surface: the `[ALGORITHM = {UNDEFINED | MERGE |
1583 /// TEMPTABLE}] [DEFINER = <user>] [SQL SECURITY {DEFINER | INVOKER}]` prefix (before the
1584 /// `VIEW` keyword) on `CREATE VIEW`, and the whole `ALTER VIEW` redefinition statement,
1585 /// dispatched to the [`AlterView`](crate::ast::AlterView) node. One flag gates both because
1586 /// they are the one MySQL view-definition behaviour — the identical [`ViewOptions`](crate::ast::ViewOptions)
1587 /// prefix decorates `CREATE VIEW` and heads `ALTER VIEW`, and no dialect has one without the
1588 /// other. On for MySQL/Lenient. Off elsewhere: the option keywords before `VIEW` are left
1589 /// unconsumed (a clean parse error), and `ALTER VIEW` routes only to the DuckDB
1590 /// [`alter_object_set_schema`](StatementDdlGates::alter_object_set_schema) `SET SCHEMA` head
1591 /// where that flag is on. A *separate* behaviour from that schema-relocation gate: this is
1592 /// the redefinition/option surface, that is the cross-schema move.
1593 pub view_definition_options: bool,
1594}
1595
1596/// Dialect-owned whole-statement non-`TABLE` DDL dispatch gates accepted by the parser.
1597///
1598/// Leading-object-kind gates for `CREATE`/`DROP`/`ALTER` families other than table-body
1599/// clauses (those live on [`CreateTableClauseSyntax`] / [`ColumnDefinitionSyntax`] /
1600/// [`ConstraintSyntax`]). View/sequence *clause* refinements after dispatch live on
1601/// [`ViewSequenceClauseSyntax`]. Each flag is a statement-head gate unless noted:
1602/// when off the keyword is not dispatched and surfaces as an unknown statement.
1603#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1604pub struct StatementDdlGates {
1605 /// Accept `CREATE`/`DROP COLOCATION GROUP` and table membership clauses.
1606 pub colocation_groups: bool,
1607 /// Accept the SQLite `CREATE [TEMP] TRIGGER [IF NOT EXISTS] <name> <timing>
1608 /// <event> ON <table> [FOR EACH ROW] [WHEN <expr>] BEGIN <stmt>; … END` statement.
1609 /// Like the other flags on this axis it gates the *whole* statement (the leading
1610 /// `TRIGGER` after `CREATE`), not a decoration of an already-accepted family: only
1611 /// SQLite's SQL-statement body form is modelled, and PostgreSQL/MySQL spell an
1612 /// incompatible `EXECUTE FUNCTION`/external-routine body they reject for this form,
1613 /// so gating to SQLite (and Lenient) is behaviour-accurate. When off, `TRIGGER`
1614 /// falls through to the `CREATE TABLE` expectation and surfaces as an unknown
1615 /// statement.
1616 pub create_trigger: bool,
1617 /// Accept the DuckDB `CREATE [OR REPLACE] [TEMP] {MACRO | FUNCTION} <name>(<params>)
1618 /// AS <expr> | AS TABLE <query>` macro DDL. Like [`create_trigger`](StatementDdlGates::create_trigger)
1619 /// this gates the *whole* statement, not a decoration of an always-accepted family:
1620 /// DuckDB's `MACRO` keyword and its live-body `FUNCTION` are dispatched to the
1621 /// [`CreateMacro`](crate::ast::CreateMacro) node only under this flag. On for
1622 /// DuckDB/Lenient. Off elsewhere: `MACRO` then falls through to the `CREATE TABLE`
1623 /// expectation (an unknown statement), and `CREATE FUNCTION` keeps routing to the
1624 /// string-body routine parser gated by [`routines`](StatementDdlGates::routines) — the two
1625 /// grammars are disjoint (a live expr/query body vs an opaque source string), so a
1626 /// macro is never silently reinterpreted as a routine where the flag is off.
1627 ///
1628 /// This same flag also gates the matching `DROP MACRO [TABLE] <name>` object kinds
1629 /// ([`DropObjectKind::Macro`](crate::ast::DropObjectKind::Macro) /
1630 /// [`MacroTable`](crate::ast::DropObjectKind::MacroTable)) — a dialect with `CREATE MACRO`
1631 /// has `DROP MACRO`. Unlike the sibling one-flag-gates-both forms (`TYPE`/`SEQUENCE`),
1632 /// only the `MACRO` drop spelling is gated here: the `FUNCTION` synonym DuckDB accepts on
1633 /// `DROP` routes to the signature routine drop gated by [`routines`](StatementDdlGates::routines),
1634 /// not to a macro object kind.
1635 pub create_macro: bool,
1636 /// Accept DuckDB's secrets-management statements: `CREATE [PERSISTENT] SECRET <name>
1637 /// (<option> <value>, …)`, dispatched to the [`CreateSecret`](crate::ast::CreateSecret)
1638 /// node, and its drop counterpart `DROP [PERSISTENT | TEMPORARY] SECRET [IF EXISTS] <name>
1639 /// [FROM <storage>]`, dispatched to [`DropSecretStmt`](crate::ast::DropSecretStmt). One
1640 /// flag covers both because they are the same secrets behaviour surface (the drop is
1641 /// meaningless without the create). Like [`create_macro`](StatementDdlGates::create_macro)
1642 /// this gates the *whole* statement: on for DuckDB/Lenient, off elsewhere, where the
1643 /// `PERSISTENT`/`SECRET` keyword falls through to the `CREATE TABLE`/`DROP` object-kind
1644 /// expectation and surfaces as an unknown statement.
1645 pub create_secret: bool,
1646 /// Accept DuckDB's `CREATE [OR REPLACE] [TEMP] TYPE <name> AS ENUM(…)/STRUCT(…)/<alias>`
1647 /// user-defined-type DDL, dispatched to the [`CreateType`](crate::ast::CreateType) node,
1648 /// and the matching `DROP TYPE` object kind ([`DropObjectKind::Type`](crate::ast::DropObjectKind)).
1649 /// Like [`create_macro`](StatementDdlGates::create_macro) this gates the *whole* statement: on for
1650 /// DuckDB/Lenient, off elsewhere, where the `TYPE` keyword falls through to the
1651 /// `CREATE TABLE` expectation (an unknown statement) and `DROP TYPE` is an unexpected
1652 /// object kind. One flag gates both leading forms — a dialect with `CREATE TYPE` has
1653 /// `DROP TYPE`.
1654 pub create_type: bool,
1655 /// Accept the SQLite `CREATE VIRTUAL TABLE [IF NOT EXISTS] <name> USING <module>
1656 /// [(<args>)]` statement, dispatched to the
1657 /// [`CreateVirtualTable`](crate::ast::CreateVirtualTable) node. Like
1658 /// [`create_trigger`](StatementDdlGates::create_trigger) this gates the *whole* statement (the
1659 /// leading `VIRTUAL` after `CREATE`): only SQLite has virtual tables, and the
1660 /// module-owned argument list is meaningless elsewhere, so on for SQLite/Lenient and
1661 /// off everywhere else — where `VIRTUAL` falls through to the `CREATE TABLE`
1662 /// expectation and surfaces as an unknown statement.
1663 pub create_virtual_table: bool,
1664 /// Accept the `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] <name> [<option> ...]`
1665 /// sequence-generator statement (SQL:2003 T176; PostgreSQL/DuckDB), dispatched to the
1666 /// [`CreateSequence`](crate::ast::CreateSequence) node, and the matching `DROP SEQUENCE`
1667 /// object kind ([`DropObjectKind::Sequence`](crate::ast::DropObjectKind)). Like
1668 /// [`create_type`](StatementDdlGates::create_type) this gates the *whole* statement: on for
1669 /// PostgreSQL/DuckDB/Lenient, off elsewhere, where the `SEQUENCE` keyword falls through
1670 /// to the `CREATE TABLE` expectation (an unknown statement) and `DROP SEQUENCE` is an
1671 /// unexpected object kind. One flag gates both leading forms — a dialect with
1672 /// `CREATE SEQUENCE` has `DROP SEQUENCE`. The modelled tail is the shared standard option
1673 /// core both engines' parsers accept (`START [WITH]`, `INCREMENT [BY]`, `MIN`/`MAXVALUE`,
1674 /// `NO MIN`/`MAXVALUE`, `CYCLE`/`NO CYCLE`). The independently gated PostgreSQL
1675 /// `CACHE` extension is modelled by
1676 /// [`ViewSequenceClauseSyntax::create_sequence_cache`];
1677 /// `AS` and `OWNED BY` remain unmodelled.
1678 pub create_sequence: bool,
1679 /// Accept the PostgreSQL extension-DDL statements `CREATE EXTENSION [IF NOT EXISTS]
1680 /// <name> [WITH] [SCHEMA s] [VERSION v] [CASCADE]` and `ALTER EXTENSION <name>
1681 /// {UPDATE [TO v] | ADD <member> | DROP <member>}`, dispatched to the
1682 /// [`CreateExtension`](crate::ast::CreateExtension) and
1683 /// [`AlterExtension`](crate::ast::AlterExtension) nodes. Like
1684 /// [`create_sequence`](StatementDdlGates::create_sequence) this gates the *whole*
1685 /// statement (the leading `EXTENSION` keyword after `CREATE`, and the `EXTENSION`
1686 /// dispatch after `ALTER`): PostgreSQL is the only shipped dialect with an extension
1687 /// catalogue, so on for PostgreSQL/Lenient and off everywhere else — where `EXTENSION`
1688 /// falls through to the `CREATE TABLE` expectation (an unknown statement) or the
1689 /// `ALTER TABLE` expectation. One flag gates both the `CREATE` and `ALTER` forms — a
1690 /// dialect with `CREATE EXTENSION` has `ALTER EXTENSION`.
1691 pub extension_ddl: bool,
1692 /// Accept the PostgreSQL `DROP TRANSFORM [IF EXISTS] FOR <type> LANGUAGE <lang>
1693 /// [CASCADE | RESTRICT]` statement, dispatched to the
1694 /// [`DropTransform`](crate::ast::DropTransform) node. Like
1695 /// [`extension_ddl`](StatementDdlGates::extension_ddl) this gates the *whole* statement
1696 /// (the `TRANSFORM` keyword after `DROP`): only PostgreSQL has the transform catalogue
1697 /// (`pg_transform` — a `(type, language)` conversion registered by `CREATE TRANSFORM`),
1698 /// so on for PostgreSQL/Lenient and off everywhere else — where `TRANSFORM` falls through
1699 /// to the `DROP` object-kind expectation and surfaces as a clean parse error.
1700 ///
1701 /// A *separate* behaviour from [`extension_ddl`](StatementDdlGates::extension_ddl),
1702 /// carrying its own gate rather than riding that one — the same split
1703 /// [`alter_system`](StatementDdlGates::alter_system) makes. A transform is procedural-
1704 /// language infrastructure (a type↔language conversion), not an extension-catalogue
1705 /// operation: unlike `ALTER … DEPENDS ON EXTENSION` (which names `EXTENSION` in its
1706 /// syntax and so rides `extension_ddl`), `DROP TRANSFORM` mutates a standalone
1707 /// `pg_transform` object with no extension in the grammar. It reuses the shared
1708 /// [`ObjectReference::Transform`](crate::ast::ObjectReference) axis for its `FOR type
1709 /// LANGUAGE lang` shape — the same axis `ALTER EXTENSION … ADD|DROP TRANSFORM` names a
1710 /// member with — but that shared *node* is not a shared *behaviour gate*.
1711 pub transform_ddl: bool,
1712 /// Accept the PostgreSQL `ALTER SYSTEM { SET <name> {= | TO} <value> | RESET <name> |
1713 /// RESET ALL }` server-configuration statement, dispatched to the
1714 /// [`AlterSystem`](crate::ast::AlterSystem) node. Like
1715 /// [`extension_ddl`](StatementDdlGates::extension_ddl) this gates the *whole* statement
1716 /// (the `SYSTEM` dispatch after `ALTER`): only PostgreSQL persists a server-wide
1717 /// configuration through SQL (`postgresql.auto.conf`), so on for PostgreSQL/Lenient and
1718 /// off everywhere else — where `SYSTEM` falls through to the `ALTER TABLE` expectation
1719 /// and surfaces as a clean parse error. A *separate* behaviour from
1720 /// [`extension_ddl`](StatementDdlGates::extension_ddl): `ALTER SYSTEM` is server
1721 /// configuration, unrelated to the extension catalogue, so it carries its own gate rather
1722 /// than riding that one. It reuses the session-`SET` value axis
1723 /// ([`SetValue`](crate::ast::SetValue) / [`ConfigParameter`](crate::ast::ConfigParameter))
1724 /// for the setting name/value grammar, but admits no `SESSION`/`LOCAL` scope and no
1725 /// `FROM CURRENT` — the wrapper is exactly PostgreSQL's `generic_set`/`generic_reset`.
1726 pub alter_system: bool,
1727 /// Accept MySQL's tablespace storage-DDL statements — `CREATE [UNDO] TABLESPACE <name> …`,
1728 /// `ALTER [UNDO] TABLESPACE <name> <action>`, and `DROP [UNDO] TABLESPACE <name> [<option>...]`
1729 /// — dispatched to the [`CreateTablespace`](crate::ast::CreateTablespace),
1730 /// [`AlterTablespace`](crate::ast::AlterTablespace), and
1731 /// [`DropTablespace`](crate::ast::DropTablespace) nodes. Like
1732 /// [`extension_ddl`](StatementDdlGates::extension_ddl) this gates the *whole* statement (the
1733 /// `TABLESPACE`/`UNDO` dispatch after `CREATE`/`ALTER`/`DROP`): only MySQL models an
1734 /// InnoDB/NDB tablespace catalogue, so on for MySQL/Lenient and off everywhere else — where
1735 /// the keyword falls through to the `CREATE`/`ALTER TABLE` expectation (an unknown statement)
1736 /// or the `DROP` object-kind expectation. One flag gates all three verbs and both the plain
1737 /// and `UNDO` variants — a dialect with `CREATE TABLESPACE` has the whole family. A separate
1738 /// behaviour from [`logfile_group_ddl`](StatementDdlGates::logfile_group_ddl): a tablespace and
1739 /// a logfile group are distinct storage objects with distinct leading keywords, so each
1740 /// carries its own gate rather than one bundling both.
1741 pub tablespace_ddl: bool,
1742 /// Accept MySQL's NDB logfile-group storage-DDL statements — `CREATE LOGFILE GROUP <name> ADD
1743 /// UNDOFILE '<f>' [<option>...]`, `ALTER LOGFILE GROUP <name> ADD UNDOFILE '<f>' [<option>...]`,
1744 /// and `DROP LOGFILE GROUP <name> [<option>...]` — dispatched to the
1745 /// [`CreateLogfileGroup`](crate::ast::CreateLogfileGroup),
1746 /// [`AlterLogfileGroup`](crate::ast::AlterLogfileGroup), and
1747 /// [`DropLogfileGroup`](crate::ast::DropLogfileGroup) nodes. Like
1748 /// [`tablespace_ddl`](StatementDdlGates::tablespace_ddl) this gates the *whole* statement (the
1749 /// `LOGFILE GROUP` dispatch after `CREATE`/`ALTER`/`DROP`): only MySQL (NDB) has a logfile-group
1750 /// catalogue, so on for MySQL/Lenient and off everywhere else — where `LOGFILE` falls through
1751 /// to the `CREATE`/`ALTER TABLE` expectation (an unknown statement) or the `DROP` object-kind
1752 /// expectation. One flag gates all three verbs — a dialect with `CREATE LOGFILE GROUP` has the
1753 /// whole family. A separate behaviour from
1754 /// [`tablespace_ddl`](StatementDdlGates::tablespace_ddl), for the reason documented there.
1755 pub logfile_group_ddl: bool,
1756 /// Accept the `CREATE SCHEMA …` and `DROP SCHEMA …` schema-object statements
1757 /// (SQL:2016 F771; PostgreSQL/MySQL). One flag gates both leading forms — a dialect
1758 /// with `CREATE SCHEMA` has `DROP SCHEMA`. On for ANSI/PostgreSQL/MySQL/DuckDB/
1759 /// Lenient. SQLite has no schema objects (a database *is* the schema namespace), so
1760 /// it is off; the `SCHEMA` keyword is then not dispatched and surfaces as an unknown
1761 /// statement.
1762 pub schemas: bool,
1763 /// Accept the SQL-standard embedded schema-element list on `CREATE SCHEMA`
1764 /// (`CREATE SCHEMA s CREATE TABLE t (...) CREATE VIEW ...`): the component objects
1765 /// created *inside* the new schema, parsed as children of the [`CreateSchema`](crate::ast::CreateSchema)
1766 /// node so the whole construct stays ONE statement. The admissible element set is
1767 /// closed and measured against PostgreSQL — `CREATE TABLE`/`VIEW`/`INDEX`/`SEQUENCE`/
1768 /// `TRIGGER` and `GRANT` — with `CREATE MATERIALIZED VIEW`/`FUNCTION`, a nested
1769 /// `CREATE SCHEMA`, and `DROP`/`ALTER`/`INSERT`/… all rejected as elements.
1770 ///
1771 /// On for PostgreSQL and Lenient (its superset). Off elsewhere: ANSI/MySQL/DuckDB
1772 /// accept the schema head but not the embedded form (MySQL/DuckDB reject the
1773 /// standard embedding; ANSI keeps a bare head), so a following `CREATE`/`GRANT`
1774 /// there is left to the top-level statement loop rather than consumed as an element.
1775 /// Narrower than [`schemas`](StatementDdlGates::schemas) on purpose — the head is
1776 /// widely accepted, the embedding is not.
1777 pub schema_elements: bool,
1778 /// Accept the `CREATE DATABASE …` statement (PostgreSQL/MySQL). On for
1779 /// ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no `CREATE DATABASE` (databases
1780 /// are files, reached via `ATTACH`), so it is off; the `DATABASE` keyword is then not
1781 /// dispatched and surfaces as an unknown statement.
1782 pub databases: bool,
1783 /// Accept MySQL's `DROP {DATABASE | SCHEMA} [IF EXISTS] <name>` — a single-database drop
1784 /// where `DATABASE` and `SCHEMA` are exact synonyms (the lexer folds them onto one
1785 /// grammar), naming exactly one unqualified database with no `CASCADE`/`RESTRICT` and no
1786 /// comma list (server-measured on mysql:8: `DROP DATABASE a, b`, `DROP DATABASE db.x`, and
1787 /// `DROP DATABASE a CASCADE` are each `ER_PARSE_ERROR`). Distinct from the shared
1788 /// name-list `DROP SCHEMA <name> [, …] [CASCADE | RESTRICT]` gated by
1789 /// [`schemas`](StatementDdlGates::schemas): where this flag is on, both the `DATABASE` and
1790 /// `SCHEMA` keywords are intercepted for the single-name form *before* the shared
1791 /// name-list path, so the two cannot both fire. On for MySQL only. Off elsewhere,
1792 /// including Lenient: because enabling it would recast `DROP SCHEMA` as the single-name
1793 /// form and forfeit the more permissive PostgreSQL/DuckDB name-list-plus-`CASCADE`
1794 /// `DROP SCHEMA` — a documented conflict resolution — Lenient keeps the name-list path and
1795 /// forgoes the MySQL `DROP DATABASE` spelling. With the flag off the `DATABASE` keyword is
1796 /// not dispatched as a drop and surfaces as an unknown drop object kind.
1797 pub drop_database: bool,
1798 /// Accept the `CREATE MATERIALIZED VIEW …` and `DROP MATERIALIZED VIEW …` statements
1799 /// (PostgreSQL; DuckDB). One flag gates both — a dialect with the create form has the
1800 /// drop form. On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no materialized
1801 /// views, so it is off; `MATERIALIZED` is then not dispatched and surfaces as an
1802 /// unknown statement (a plain `CREATE VIEW` is unaffected — a separate always-accepted
1803 /// family).
1804 pub materialized_views: bool,
1805 /// Accept the stored-routine DDL — `CREATE FUNCTION …`, `DROP FUNCTION …`, and
1806 /// `DROP PROCEDURE …` (PostgreSQL/MySQL SQL/PSM). One flag gates the routine family as
1807 /// a unit. On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no stored routines
1808 /// (its functions are C-registered, not SQL-declared), so it is off; the `FUNCTION`/
1809 /// `PROCEDURE` keyword is then not dispatched and surfaces as an unknown statement.
1810 pub routines: bool,
1811 /// Accept the `CREATE OR REPLACE …` object-replacement modifier on `VIEW`/`FUNCTION`
1812 /// (PostgreSQL; MySQL `OR REPLACE VIEW`). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient.
1813 /// SQLite has no `OR REPLACE` (it spells the intent `DROP` then `CREATE`), so it is
1814 /// off; the `OR` after `CREATE` is then left unconsumed and surfaces as a clean parse
1815 /// error.
1816 pub or_replace: bool,
1817 /// Accept DuckDB's `CREATE OR REPLACE TABLE` — the `OR REPLACE` object-replacement
1818 /// modifier on `TABLE` (threaded onto the [`CreateTable`](crate::ast::CreateTable) node).
1819 /// **Statement-head** gate: consulted during `CREATE` dispatch when the object kind is
1820 /// `TABLE`, not a table-body clause (moved off [`CreateTableClauseSyntax`] for MECE).
1821 /// On for DuckDB/Lenient. Off elsewhere: other dialects take `OR REPLACE` only on
1822 /// `VIEW`/`FUNCTION` (gated by [`or_replace`](Self::or_replace)), so with this flag off a
1823 /// `TABLE` after `OR REPLACE` is left for the `VIEW` expectation and surfaces as a clean
1824 /// parse error.
1825 pub create_or_replace_table: bool,
1826 /// Parse a routine/trigger/event body as a MySQL SQL/PSM *compound statement* — the
1827 /// `[<label>:] BEGIN [<declarations>] … END` block with its `DECLARE` prefix, the
1828 /// flow-control statements (`IF`/`CASE`/`LOOP`/`WHILE`/`REPEAT`/`LEAVE`/`ITERATE`/
1829 /// `RETURN`) and the cursor operations (`OPEN`/`FETCH`/`CLOSE`). This is a
1830 /// *body-context* behaviour, not a top-level statement gate: it governs the separate
1831 /// `parse_body_statement` dispatcher the routine/trigger wrappers invoke, never the
1832 /// top-level one (a bare top-level `BEGIN … END` stays transaction-start regardless).
1833 /// On for MySQL (and Lenient). Off elsewhere: PostgreSQL/ANSI spell a routine body as
1834 /// an opaque `$$…$$`/string routine definition (a different, unaffected grammar), and
1835 /// SQLite has no stored programs — so where it is off the body dispatcher rejects the
1836 /// compound grammar and the existing string/opaque body paths are untouched.
1837 pub compound_statements: bool,
1838 /// Accept DuckDB's `ALTER DATABASE [IF EXISTS] <name> SET ALIAS TO <alias>` statement
1839 /// (`AlterDatabaseStmt`), dispatched to the [`AlterDatabase`](crate::ast::AlterDatabase)
1840 /// node. Like [`alter_system`](StatementDdlGates::alter_system) this gates the *whole*
1841 /// statement (the `DATABASE` dispatch after `ALTER`): DuckDB's sole `ALTER DATABASE` form
1842 /// re-aliases an attached database. On for DuckDB/Lenient, off elsewhere — where
1843 /// `DATABASE` falls through to the `ALTER TABLE` expectation and surfaces as a clean parse
1844 /// error. Named for the object it alters, not the DuckDB-specific `SET ALIAS TO` spelling.
1845 /// MySQL's `ALTER DATABASE` (the charset/collation/encryption/read-only option list) is a
1846 /// *disjoint* behaviour on its own
1847 /// [`alter_database_options`](StatementDdlGates::alter_database_options) gate: the two
1848 /// grammars share only the `ALTER DATABASE` head and cannot be unioned under one gate (DuckDB
1849 /// rejects MySQL's options and vice versa), so each is its own flag and node per the MECE
1850 /// doctrine. Although PostgreSQL also has an `ALTER DATABASE` grammar, this stays gated to the
1851 /// measured dialect (DuckDB) per the no-shadowing doctrine rather than widened to the
1852 /// PostgreSQL reference without a differential.
1853 pub alter_database: bool,
1854 /// Accept MySQL's `ALTER {DATABASE | SCHEMA} [<name>] <option> …` schema-option change
1855 /// (`alter_database_stmt`), dispatched to the
1856 /// [`AlterDatabaseOptions`](crate::ast::AlterDatabaseOptions) node. A *disjoint* behaviour
1857 /// from DuckDB's [`alter_database`](StatementDdlGates::alter_database) `SET ALIAS`
1858 /// relocation: the two share only the `ALTER DATABASE` head, but MySQL adds the `SCHEMA`
1859 /// synonym and an optional name and takes a non-empty, repeatable list of charset/collation/
1860 /// encryption/read-only options — so it is its own gate and its own node rather than a union
1861 /// (which would make each dialect over-accept the other's grammar). On for MySQL/Lenient, off
1862 /// elsewhere — where `DATABASE`/`SCHEMA` falls through to the `ALTER TABLE` expectation and
1863 /// surfaces as a clean parse error (the DuckDB `SET ALIAS` head is intercepted first where
1864 /// its gate is on, and the two are disambiguated by lookahead under Lenient, which enables
1865 /// both).
1866 pub alter_database_options: bool,
1867 /// Accept MySQL's federated-server DDL — `CREATE SERVER <name> FOREIGN DATA WRAPPER
1868 /// <wrapper> OPTIONS ( … )`, `ALTER SERVER <name> OPTIONS ( … )`, and `DROP SERVER
1869 /// [IF EXISTS] <name>` — dispatched to the [`CreateServer`](crate::ast::CreateServer),
1870 /// [`AlterServer`](crate::ast::AlterServer), and [`DropServer`](crate::ast::DropServer)
1871 /// nodes. One flag gates all three leading dispatches because they are one cohesive
1872 /// server-object behaviour: `CREATE`/`ALTER` share the
1873 /// [`ServerOption`](crate::ast::ServerOption) axis (the `server_options_list` grammar) and
1874 /// `DROP` disposes of the same object — the `extension_ddl` (CREATE + ALTER extension) and
1875 /// `view_definition_options` (CREATE VIEW prefix + ALTER VIEW) one-object-one-gate precedent.
1876 /// On for MySQL/Lenient, off elsewhere, where the `SERVER` head falls through and surfaces as
1877 /// a clean parse error.
1878 pub server_definition: bool,
1879 /// Accept MySQL's `ALTER INSTANCE <action>` server-instance administration statement
1880 /// (`alter_instance_stmt`), dispatched to the [`AlterInstance`](crate::ast::AlterInstance)
1881 /// node. A whole-statement gate like [`alter_system`](StatementDdlGates::alter_system) — the
1882 /// `INSTANCE` dispatch after `ALTER`: a single instance-wide maintenance action (rotate a
1883 /// master key, reload TLS/the keyring, toggle the InnoDB redo log). A *separate* behaviour
1884 /// from the server and database DDL: it names no object and touches the running instance, not
1885 /// a catalogue object. On for MySQL/Lenient, off elsewhere, where `INSTANCE` surfaces as the
1886 /// "expected TABLE" parse error.
1887 pub alter_instance: bool,
1888 /// Accept MySQL's spatial-reference-system DDL — `CREATE [OR REPLACE] SPATIAL REFERENCE
1889 /// SYSTEM [IF NOT EXISTS] <srid> <attributes>` and `DROP SPATIAL REFERENCE SYSTEM
1890 /// [IF EXISTS] <srid>` — dispatched to the
1891 /// [`CreateSpatialReferenceSystem`](crate::ast::CreateSpatialReferenceSystem) and
1892 /// [`DropSpatialReferenceSystem`](crate::ast::DropSpatialReferenceSystem) nodes. One flag
1893 /// gates both dispatches because they are one catalogue-object behaviour (the
1894 /// [`server_definition`](StatementDdlGates::server_definition) one-object-one-gate
1895 /// precedent). On for MySQL/Lenient, off elsewhere — where the `SPATIAL` head falls through
1896 /// to the `TABLE` expectation and surfaces as a clean parse error.
1897 pub spatial_reference_system: bool,
1898 /// Accept MySQL's resource-group DDL — `CREATE RESOURCE GROUP <name> TYPE [=] {SYSTEM |
1899 /// USER} [VCPU …] [THREAD_PRIORITY …] [ENABLE | DISABLE]`, `ALTER RESOURCE GROUP <name>
1900 /// [VCPU …] [THREAD_PRIORITY …] [ENABLE | DISABLE] [FORCE]`, `DROP RESOURCE GROUP <name>
1901 /// [FORCE]`, and the session-statement `SET RESOURCE GROUP <name> [FOR <thread_ids>]` —
1902 /// dispatched to the [`CreateResourceGroup`](crate::ast::CreateResourceGroup) /
1903 /// [`AlterResourceGroup`](crate::ast::AlterResourceGroup) /
1904 /// [`DropResourceGroup`](crate::ast::DropResourceGroup) statement nodes and the
1905 /// [`SetResourceGroup`](crate::ast::SessionStatement::SetResourceGroup) session variant. One
1906 /// flag gates all four because they are one resource-group behaviour sharing the
1907 /// `VCPU`/`THREAD_PRIORITY`/`ENABLE|DISABLE` axes (the
1908 /// [`server_definition`](StatementDdlGates::server_definition) precedent); the `SET` member
1909 /// rides the same flag from inside the `SET`-statement dispatch, claimed off the
1910 /// `RESOURCE GROUP` two-word lookahead before the variable-assignment fallback. On for
1911 /// MySQL/Lenient, off elsewhere — where `RESOURCE` falls through and surfaces as a clean
1912 /// parse error (and `SET RESOURCE GROUP g` stays a variable-assignment parse error).
1913 pub resource_group: bool,
1914 /// Accept DuckDB's `ALTER SEQUENCE [IF EXISTS] <name> <option>...` statement
1915 /// (`AlterSeqStmt`), dispatched to the [`AlterSequence`](crate::ast::AlterSequence) node.
1916 /// Like [`alter_database`](StatementDdlGates::alter_database) this gates the *whole*
1917 /// statement (the `SEQUENCE` dispatch after `ALTER`): the option-list form changes a
1918 /// sequence generator's options, reusing the shared
1919 /// [`IdentityOption`](crate::ast::IdentityOption) axis for the core `CREATE SEQUENCE` also
1920 /// accepts. On for DuckDB/Lenient, off elsewhere — where `SEQUENCE` falls through to the
1921 /// `ALTER TABLE` expectation and surfaces as a clean parse error. A *separate* behaviour
1922 /// from [`alter_object_set_schema`](StatementDdlGates::alter_object_set_schema): `ALTER
1923 /// SEQUENCE … SET SCHEMA` is a schema relocation (that gate), not a sequence-option change.
1924 /// Although PostgreSQL also has `ALTER SEQUENCE`, this stays gated to DuckDB per the
1925 /// no-shadowing doctrine.
1926 pub alter_sequence: bool,
1927 /// Accept DuckDB's `ALTER {TABLE | VIEW | SEQUENCE} [IF EXISTS] <name> SET SCHEMA
1928 /// <schema>` statement (`AlterObjectSchemaStmt`), dispatched to the
1929 /// [`AlterObjectSchema`](crate::ast::AlterObjectSchema) node. Like
1930 /// [`alter_database`](StatementDdlGates::alter_database) this gates the *whole* statement
1931 /// (the `SET SCHEMA` tail after the object head): it relocates a relocatable object to
1932 /// another schema. DuckDB 1.5.4's binder rejects this as `Not implemented`, but the
1933 /// production is parse-reachable and PARSE-level parity is the modelled bar (analogous to
1934 /// PostgreSQL's grammar-present, engine-unimplemented `CREATE ASSERTION`). On for
1935 /// DuckDB/Lenient, off elsewhere — where a `SET SCHEMA` tail is left to the `ALTER TABLE`
1936 /// command parser (a clean parse error for the `VIEW`/`SEQUENCE`/`DATABASE` heads). Gated
1937 /// to DuckDB per the no-shadowing doctrine, though PostgreSQL shares the grammar.
1938 pub alter_object_set_schema: bool,
1939}
1940
1941/// Dialect-owned `CREATE TABLE` table-level clause syntax accepted by the parser.
1942///
1943/// The table-level decorations on a `CREATE TABLE` — the clauses that attach to the table
1944/// as a whole rather than to a single column or constraint. Split out of the retired
1945/// `SchemaChangeSyntax` at its 16-field line as the table-clause axis, distinct from the
1946/// column-definition and constraint axes. Each flag is a grammar gate: when off the clause
1947/// keyword is left unconsumed and surfaces as a clean parse error.
1948#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1949pub struct CreateTableClauseSyntax {
1950 /// Accept the MySQL `CREATE TABLE` storage decorations: the trailing table-option
1951 /// list (`ENGINE = InnoDB`, `AUTO_INCREMENT = 100`, `DEFAULT CHARSET = utf8mb4`,
1952 /// `COMMENT = '...'`, `ROW_FORMAT = ...`, `COLLATE = ...`) **and** the column-level
1953 /// underscored `AUTO_INCREMENT` attribute.
1954 ///
1955 /// **Deliberate dual-position unit (not a MECE bug):** both grammar points are one
1956 /// MySQL dialect unit — MySQL always admits the column attribute wherever it admits
1957 /// the table options (mirroring how `existence_guards.if_exists` co-gates related
1958 /// sites). Splitting them would invent independent axes no shipped engine separates.
1959 /// The SQLite joined `AUTOINCREMENT` spelling is a separate flag on
1960 /// [`ColumnDefinitionSyntax`]. Not ANSI/PostgreSQL, which reject both surfaces as
1961 /// leftover input.
1962 pub table_options: bool,
1963 /// Accept the SQLite trailing `WITHOUT ROWID` table option on `CREATE TABLE`
1964 /// (`CREATE TABLE t (a INTEGER PRIMARY KEY) WITHOUT ROWID`), recorded as
1965 /// [`CreateTableOptionKind::WithoutRowid`](crate::ast::CreateTableOptionKind::WithoutRowid).
1966 /// SQLite-only; off in every other preset, where the trailing `WITHOUT ROWID` is left
1967 /// unconsumed and surfaces as a clean parse error. Split out of
1968 /// the retired `sqlite_table_decorations` bundle because the rowid-storage
1969 /// table option is an independent grammar point from the trailing `STRICT` option, the
1970 /// typeless column, `AUTOINCREMENT`, the column `COLLATE`, and the inline-`PRIMARY KEY`
1971 /// ordering.
1972 pub without_rowid_table_option: bool,
1973 /// Accept the SQLite trailing `STRICT` table option on `CREATE TABLE`
1974 /// (`CREATE TABLE t (a INTEGER) STRICT`), recorded as
1975 /// [`CreateTableOptionKind::Strict`](crate::ast::CreateTableOptionKind::Strict); the table
1976 /// then enforces its declared column types instead of SQLite's default flexible typing.
1977 /// SQLite-only; off in every other preset, where the trailing `STRICT` is left unconsumed
1978 /// and surfaces as a clean parse error. Split out of
1979 /// the retired `sqlite_table_decorations` bundle because the strict-typing
1980 /// table option is an independent grammar point from the trailing `WITHOUT ROWID` option,
1981 /// the typeless column, `AUTOINCREMENT`, the column `COLLATE`, and the inline-`PRIMARY KEY`
1982 /// ordering.
1983 pub strict_table_option: bool,
1984 /// Accept the `CREATE TABLE … WITH ( <name> = <value>, … )` storage-parameter list
1985 /// (PostgreSQL `WITH (fillfactor=…)`; Trino/Spark `WITH (format=…)`). On for
1986 /// ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no `WITH (…)` table clause, so it
1987 /// is off; the `WITH` keyword is then not read as a table option and surfaces as a
1988 /// clean parse error.
1989 pub storage_parameters: bool,
1990 /// Accept the temporary-table `ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP }`
1991 /// action (SQL:1999; PostgreSQL). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite
1992 /// has no `ON COMMIT` table clause, so it is off; the `ON` keyword is then left
1993 /// unconsumed and surfaces as a clean parse error.
1994 pub on_commit: bool,
1995 /// Accept the trailing `WITH [NO] DATA` populate clause on `CREATE TABLE … AS <query>`
1996 /// (and `CREATE MATERIALIZED VIEW`). On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL's
1997 /// `CREATE TABLE … AS SELECT` has no `WITH [NO] DATA` clause (`ER_PARSE_ERROR` on
1998 /// mysql:8), so it is off there; the `WITH` keyword after the query is then left as
1999 /// leftover input and surfaces as a clean parse error.
2000 pub create_table_as_with_data: bool,
2001 /// Accept the PostgreSQL `CREATE TABLE t [(cols)] AS EXECUTE <prepared> [(args)] [WITH [NO]
2002 /// DATA]` form — a CTAS whose rows come from running a prepared statement. On for
2003 /// PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB (DuckDB rejects `AS EXECUTE`), where
2004 /// the `EXECUTE` keyword after `AS` is left unconsumed and the inline-query CTAS path rejects
2005 /// it as a clean parse error.
2006 pub create_table_as_execute: bool,
2007 /// Accept PostgreSQL declarative partitioning: the parent `CREATE TABLE … PARTITION BY
2008 /// {LIST | RANGE | HASH} (<key>, …)` clause, the child `CREATE TABLE … PARTITION OF <parent>
2009 /// [(<augmentation>, …)] {FOR VALUES … | DEFAULT}` body, and the `ALTER TABLE … {ATTACH |
2010 /// DETACH} PARTITION` actions. On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB:
2011 /// none spell this grammar (MySQL's `PARTITION BY HASH(c) PARTITIONS n` and DuckDB's
2012 /// COPY-level `PARTITION_BY` are unrelated surfaces), so the `PARTITION` / `ATTACH` /
2013 /// `DETACH` keyword is left unconsumed and surfaces as a clean parse error. One flag gates
2014 /// the whole family — the parent spec, the child body, and the two alter actions travel
2015 /// together as a single dialect unit.
2016 pub declarative_partitioning: bool,
2017 /// Accept the PostgreSQL `INHERITS (<parent>, ...)` legacy table-inheritance clause
2018 /// (`CREATE TABLE t (…) INHERITS (parent)`). On for PostgreSQL/Lenient. Off for
2019 /// ANSI/MySQL/SQLite/DuckDB — none have table inheritance (DuckDB, which otherwise inherits
2020 /// the PostgreSQL schema surface, rejects it), so the `INHERITS` keyword is left unconsumed
2021 /// and surfaces as a clean parse error.
2022 pub table_inheritance: bool,
2023 /// Accept the PostgreSQL `LIKE <source> [{INCLUDING | EXCLUDING} <feature> …]` source-table
2024 /// copy *element* inside the parenthesized `CREATE TABLE` definition list (`CREATE TABLE t
2025 /// (LIKE src INCLUDING ALL)`). On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB:
2026 /// DuckDB rejects the element form, and MySQL's `CREATE TABLE t LIKE src` is a distinct
2027 /// *statement-level* production (no parentheses), not this element — so when off, a `LIKE` at
2028 /// an element position surfaces as a clean parse error.
2029 pub like_source_table: bool,
2030 /// Accept MySQL's statement-level `CREATE TABLE t LIKE <source>` table-clone body and its
2031 /// parenthesized twin `CREATE TABLE t (LIKE <source>)` — a whole-statement production that
2032 /// copies an existing table's definition, distinct from the PostgreSQL copy *element* gated
2033 /// by [`like_source_table`](CreateTableClauseSyntax::like_source_table). The source is a single bare (qualified)
2034 /// name: no `{INCLUDING | EXCLUDING} <feature>` options, no co-element, no trailing table
2035 /// options (`LIKE src ENGINE=…`, `(LIKE src, x INT)`, `(LIKE src INCLUDING ALL)` are all
2036 /// `ER_PARSE_ERROR` on mysql:8.4). On for MySQL/Lenient. Off for ANSI/PostgreSQL/SQLite/
2037 /// DuckDB, where a `LIKE` after the table name (or as the first token inside `(`) is left
2038 /// unconsumed and surfaces as a clean parse error — PostgreSQL rejects the bare form at raw
2039 /// parse, and only reads `(LIKE src …)` as the element form above. When both this and
2040 /// [`like_source_table`](CreateTableClauseSyntax::like_source_table) are on (Lenient), the parenthesized `(LIKE
2041 /// …)` reads as the more general PostgreSQL element (a superset that also admits the feature
2042 /// options), so this flag governs only the bare form there; MySQL, with the element flag off,
2043 /// takes both spellings onto this body.
2044 pub statement_level_table_like: bool,
2045 /// Accept `CREATE UNLOGGED TABLE` — the non-WAL-logged persistence keyword. On for
2046 /// PostgreSQL/DuckDB/Lenient (DuckDB parses it as a no-op). Off for ANSI/MySQL/SQLite, where
2047 /// `UNLOGGED` after `CREATE` is left unconsumed and surfaces as a clean parse error. In
2048 /// PostgreSQL's grammar `UNLOGGED` is a peer of `TEMP`/`TEMPORARY`, so the two are mutually
2049 /// exclusive; the parser rejects `CREATE TEMP UNLOGGED TABLE` accordingly.
2050 pub unlogged_tables: bool,
2051 /// Accept the trailing `USING <access_method>` table access-method clause (PostgreSQL
2052 /// `CREATE TABLE … USING heap`). On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB
2053 /// (DuckDB has no pluggable table access methods), where the `USING` keyword after the table
2054 /// body is left unconsumed and surfaces as a clean parse error. Distinct from the CREATE
2055 /// INDEX `USING <method>` clause gated by [`index_using_method`](IndexAlterSyntax::index_using_method).
2056 pub table_access_method: bool,
2057 /// Accept the legacy `WITHOUT OIDS` trailing option (PostgreSQL) — kept as an accepted
2058 /// no-op. On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB, where the `WITHOUT`
2059 /// keyword (absent SQLite's `WITHOUT ROWID`, a separate
2060 /// [`without_rowid_table_option`](CreateTableClauseSyntax::without_rowid_table_option) option) is left unconsumed
2061 /// and surfaces as a clean parse error.
2062 pub without_oids: bool,
2063 /// Accept the `CREATE TABLE t OF <type> [(…)]` typed-table form (PostgreSQL): the table's
2064 /// column shape is drawn from a composite type. On for PostgreSQL/Lenient. Off for
2065 /// ANSI/MySQL/SQLite/DuckDB, where `OF` after the table name is left unconsumed and surfaces
2066 /// as a clean parse error.
2067 pub typed_tables: bool,
2068}
2069
2070/// Dialect-owned `CREATE TABLE` column-definition syntax accepted by the parser.
2071///
2072/// The column-level decorations inside a `CREATE TABLE` definition — the attributes and
2073/// restrictions that attach to a single column. Split out of the retired
2074/// `SchemaChangeSyntax` at its 16-field line as the column-definition axis, distinct from
2075/// the table-clause and constraint axes. Each flag is a grammar gate: when off the
2076/// decoration is left unconsumed and rejects.
2077#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2078pub struct ColumnDefinitionSyntax {
2079 /// Accept the keywordless generated-column shorthand `<col> <type> AS (<expr>)
2080 /// [STORED|VIRTUAL]`, written without the leading `GENERATED ALWAYS` (MySQL,
2081 /// SQLite). It folds onto the one [`GeneratedColumn`](crate::ast::GeneratedColumn)
2082 /// shape tagged
2083 /// [`GeneratedColumnSpelling::Shorthand`](crate::ast::GeneratedColumnSpelling),
2084 /// never a new node. PostgreSQL requires the `GENERATED ALWAYS`
2085 /// keywords, so this is off there and the bare `AS (…)` after a column type is left
2086 /// unconsumed and surfaces as a clean parse error.
2087 pub generated_column_shorthand: bool,
2088 /// Accept the SQLite column-level `ON CONFLICT <resolution>` clause on an inline
2089 /// `NOT NULL` / `UNIQUE` / `PRIMARY KEY` / `CHECK` constraint
2090 /// (`a INTEGER UNIQUE ON CONFLICT REPLACE`), recording the resolution algorithm in
2091 /// [`ColumnConstraint::conflict`](crate::ast::ColumnConstraint). SQLite-only; off in
2092 /// every other preset, where the trailing `ON` after the constraint is left unconsumed
2093 /// and surfaces as a clean parse error. Split out of
2094 /// the retired `sqlite_table_decorations` bundle because column-level
2095 /// conflict resolution is an independent grammar point from the trailing table options,
2096 /// the typeless column, `AUTOINCREMENT`, and the inline-`PRIMARY KEY` ordering.
2097 pub column_conflict_resolution_clause: bool,
2098 /// Accept a SQLite *typeless* column definition — a column named with no data type
2099 /// (`CREATE TABLE t (a, b)`), leaving [`ColumnDef::data_type`](crate::ast::ColumnDef)
2100 /// unset. SQLite-only; off in every other preset, where a column with no type falls
2101 /// through to `parse_data_type` and surfaces as a clean parse error. Split out of
2102 /// the retired `sqlite_table_decorations` bundle because the typeless
2103 /// column is an independent grammar point from the trailing table options,
2104 /// `AUTOINCREMENT`, the column `COLLATE`, and the inline-`PRIMARY KEY` ordering.
2105 pub typeless_column_definitions: bool,
2106 /// Accept a column that omits its data type *only* when the column is a generated column —
2107 /// DuckDB's narrowing of the SQLite typeless rule. DuckDB requires a data type on every
2108 /// column except a generated one: both the `GENERATED { ALWAYS | BY DEFAULT } AS …` form
2109 /// and the keywordless `AS (<expr>)` shorthand may drop the type
2110 /// (`CREATE TABLE t (x INT, gen_x AS (x + 5))`, engine-measured on libduckdb 1.5.4), while
2111 /// a plain typeless column (`x`) or a typeless non-generated constraint (`y DEFAULT 5`) is
2112 /// a parse error. On for DuckDB/Lenient. Off for ANSI/PostgreSQL/MySQL — a generated column
2113 /// with no type falls through to `parse_data_type` and surfaces as a clean parse error — and
2114 /// off for SQLite, which instead accepts *any* typeless column via the strictly wider
2115 /// [`typeless_column_definitions`](ColumnDefinitionSyntax::typeless_column_definitions), so
2116 /// this narrow gate stays off there.
2117 pub typeless_generated_columns: bool,
2118 /// Accept the SQLite joined `AUTOINCREMENT` column attribute — the bare one-word keyword
2119 /// on an inline `PRIMARY KEY` column (`a INTEGER PRIMARY KEY AUTOINCREMENT`), recorded as
2120 /// [`AutoIncrementSpelling::Joined`](crate::ast::AutoIncrementSpelling::Joined) so it
2121 /// round-trips as one word. SQLite-only; off in every other preset, where the trailing
2122 /// `AUTOINCREMENT` is left unconsumed and surfaces as a clean parse error. This gates
2123 /// *only* the joined spelling: the underscored MySQL `AUTO_INCREMENT` attribute is a
2124 /// separate surface gated by [`table_options`](CreateTableClauseSyntax::table_options), so the two spellings
2125 /// toggle independently and neither preset admits the other's word. Split out of
2126 /// the retired `sqlite_table_decorations` bundle because the auto-increment
2127 /// attribute is an independent grammar point from the typeless column, the column
2128 /// `COLLATE`, and the inline-`PRIMARY KEY` ordering.
2129 pub joined_autoincrement_attribute: bool,
2130 /// Accept an `ASC`/`DESC` sort-order qualifier on an inline `PRIMARY KEY` column
2131 /// constraint (`CREATE TABLE t (a INTEGER PRIMARY KEY DESC)`), recorded in the
2132 /// [`ColumnOption::PrimaryKey`](crate::ast::ColumnOption) `ascending` field (`ASC` →
2133 /// `Some(true)`, `DESC` → `Some(false)`, absent → `None`). SQLite-only; off in every other
2134 /// preset, where the trailing `ASC`/`DESC` is left unconsumed and surfaces as a clean parse
2135 /// error. Split out of the retired `sqlite_table_decorations` bundle because
2136 /// the inline primary-key ordering is an independent grammar point from the trailing table
2137 /// options, the typeless column, `AUTOINCREMENT`, and the column `COLLATE`.
2138 pub inline_primary_key_ordering: bool,
2139 /// Accept a `CONSTRAINT <name>` prefix on a column `COLLATE` clause
2140 /// (`CREATE TABLE t (a TEXT CONSTRAINT c COLLATE nocase)`): SQLite's grammar makes `COLLATE`
2141 /// an ordinary nameable column constraint, so a `CONSTRAINT <name>` symbol binds to it.
2142 /// SQLite-only; off in every other preset (PostgreSQL and DuckDB engine-measured reject the
2143 /// named form, where `COLLATE any_name` is a constraint alternative parallel to — not under —
2144 /// the nameable constraint element), leaving the `CONSTRAINT <name>` prefix on a `COLLATE`
2145 /// clause as a clean parse error. This gates *only* the named wrapper: the bare column
2146 /// `COLLATE` surface ([`ColumnOption::Collate`](crate::ast::ColumnOption)) is a separate
2147 /// cross-dialect shape gated by [`column_collation`](ColumnDefinitionSyntax::column_collation), so the accepting
2148 /// case needs both flags on. Split out of
2149 /// the retired `sqlite_table_decorations` bundle because the named-`COLLATE`
2150 /// wrapper is an independent grammar point from the inline-`PRIMARY KEY` ordering.
2151 pub named_column_collate_constraint: bool,
2152 /// Accept the `<col> <type> GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [(…)]`
2153 /// identity column (SQL:2003; PostgreSQL). On for ANSI/PostgreSQL/MySQL/DuckDB/
2154 /// Lenient. SQLite has no `IDENTITY` (its auto-key is `INTEGER PRIMARY KEY
2155 /// [AUTOINCREMENT]`), so it is off; the `IDENTITY` keyword is then left unconsumed and
2156 /// surfaces as a clean parse error. Gates only the `AS IDENTITY` reading — the
2157 /// `GENERATED ALWAYS AS (<expr>)` computed column (which SQLite has) is unaffected.
2158 pub identity_columns: bool,
2159 /// Accept the compact identity-column forms `<col> <type> IDENTITY` and
2160 /// `<col> <type> IDENTITY(<seed>, <increment>)`. The two numeric arguments map to the
2161 /// same start/increment identity options as the standard generated form.
2162 pub compact_identity_columns: bool,
2163 /// Require a *parenthesized* `DEFAULT (expr)` for a functional / general-expression
2164 /// column default. On for MySQL: a column `DEFAULT` there admits only a literal, a
2165 /// signed literal, the `CURRENT_TIMESTAMP`/`NOW()`/`LOCALTIME`/`LOCALTIMESTAMP` temporal
2166 /// family, or a parenthesized expression — a bare function call or operator expression
2167 /// (`DEFAULT UUID()`, `DEFAULT 1 + 2`) is an `ER_PARSE_ERROR` on mysql:8, while
2168 /// `DEFAULT (UUID())` parses. Off for ANSI/PostgreSQL/SQLite/DuckDB/Lenient, which accept
2169 /// a bare expression default. When off, the default expression is read whole with no
2170 /// wrapping requirement.
2171 pub default_expression_requires_parens: bool,
2172 /// Restrict a *column-constraint* `DEFAULT` to PostgreSQL's `b_expr` grammar class
2173 /// (`ColConstraintElem: DEFAULT b_expr`), rather than the full `a_expr` used everywhere
2174 /// else. `b_expr` is the "boolean-and-predicate-free" expression production: it keeps
2175 /// arithmetic, comparison, `||`, `OPERATOR(...)`, `::`, subscripts, `COLLATE`, and the
2176 /// `IS [NOT] DISTINCT FROM` / `IS [NOT] DOCUMENT` tests, but excludes the `a_expr`-only
2177 /// forms — `AND`/`OR`/`NOT`, `IN`, `BETWEEN`, `LIKE`/`ILIKE`/`SIMILAR TO`, `IS [NOT]
2178 /// NULL`/`TRUE`/`FALSE`/`UNKNOWN`, a quantified `= ANY(...)`, and `AT TIME ZONE`. So
2179 /// `... DEFAULT 1 IN (1, 2)` is a syntax error on PostgreSQL (the `IN` predicate is
2180 /// `a_expr`-level), while the parenthesized `DEFAULT (1 IN (1, 2))` — a `c_expr` reset to
2181 /// `a_expr` — parses. On for PostgreSQL only; every other dialect (including DuckDB, which
2182 /// otherwise inherits the PostgreSQL schema surface) reads the default as a full `a_expr`.
2183 /// Note the asymmetry: `ALTER COLUMN ... SET DEFAULT` is `a_expr` even on PostgreSQL, so
2184 /// this gates only the inline column-constraint site.
2185 pub column_default_requires_b_expr: bool,
2186 /// Accept the column-definition `COLLATE <collation>` clause (`a text COLLATE "C"`). On for
2187 /// PostgreSQL/SQLite/DuckDB/Lenient — all three spell a per-column collation inside the
2188 /// column definition (PostgreSQL takes a qualified `any_name`, SQLite/DuckDB a single bare
2189 /// identifier; the parser narrows the name grammar per dialect). Off for ANSI/MySQL, where a
2190 /// column-level `COLLATE` is left unconsumed and surfaces as a clean parse error. Distinct
2191 /// from the expression-level `COLLATE` gated by
2192 /// [`ExpressionSyntax::collate`] on a different sub-struct — this one
2193 /// qualifies a column, not an expression. (MySQL *does* spell a column `COLLATE` in its own
2194 /// `CHARACTER SET … COLLATE …` attribute grammar, a distinct surface left to a follow-up.)
2195 pub column_collation: bool,
2196 /// Accept the per-column `STORAGE {PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT}` and
2197 /// `COMPRESSION <method>` physical-storage clauses (PostgreSQL). The two travel together as a
2198 /// single dialect unit — both are fixed-position clauses between a column's type and its
2199 /// constraint list. On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB (DuckDB,
2200 /// which otherwise inherits the PostgreSQL schema surface, rejects both), where the
2201 /// `STORAGE`/`COMPRESSION` keyword is left unconsumed and surfaces as a clean parse error.
2202 pub column_storage: bool,
2203}
2204
2205/// Dialect-owned table/column-constraint syntax accepted by the parser.
2206///
2207/// The constraint forms and their decorations. Split out of the retired
2208/// `SchemaChangeSyntax` at its 16-field line as the constraint axis, distinct from the
2209/// table-clause and column-definition axes. Each flag is a grammar gate: when off the
2210/// constraint keyword is left unconsumed and rejects.
2211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2212pub struct ConstraintSyntax {
2213 /// Accept a trailing `<constraint characteristics>` clause (`[NOT] DEFERRABLE`,
2214 /// `INITIALLY {DEFERRED | IMMEDIATE}`) on a table/column constraint, in both `CREATE
2215 /// TABLE` and `ALTER TABLE ADD CONSTRAINT`. On for PostgreSQL/SQLite/DuckDB/Lenient.
2216 /// MySQL has no deferrable constraints — `… REFERENCES t (c) DEFERRABLE` / `INITIALLY
2217 /// DEFERRED` are `ER_PARSE_ERROR` on mysql:8 — so it is off there and the
2218 /// `DEFERRABLE`/`INITIALLY` keyword surfaces as a clean parse error.
2219 pub deferrable_constraints: bool,
2220 /// Accept a `CONSTRAINT <symbol>` name prefix on a *non-CHECK* inline column constraint
2221 /// (`a INT CONSTRAINT c REFERENCES b`, `… CONSTRAINT c UNIQUE`). On for ANSI/PostgreSQL/
2222 /// SQLite/DuckDB/Lenient. MySQL admits a named inline constraint only for `CHECK`
2223 /// (`a INT CONSTRAINT c CHECK (…)` parses on mysql:8), so a `CONSTRAINT <symbol>` before
2224 /// any other inline column constraint — `REFERENCES`, `UNIQUE`, `PRIMARY KEY`,
2225 /// `NOT NULL` — is an `ER_PARSE_ERROR`; it is off there. The bare, unnamed inline
2226 /// constraints and the named `CHECK` are unaffected either way.
2227 pub named_inline_non_check_constraints: bool,
2228 /// Accept a trailing bodyless `CONSTRAINT <name>` — a nameable constraint marker with no
2229 /// constraint element after it — in a column definition (`a INT CONSTRAINT cn`) or as a
2230 /// standalone table constraint (`CREATE TABLE t (a INT, CONSTRAINT cn)`), recorded as
2231 /// [`ColumnOption::Bare`](crate::ast::ColumnOption)/[`TableConstraint::Bare`](crate::ast::TableConstraint).
2232 /// SQLite's grammar makes the constraint element optional after `CONSTRAINT <name>`, and (in
2233 /// the table-constraint list only) lets the separating comma before such a bare marker be
2234 /// omitted too — `UNIQUE(a) CONSTRAINT c` and `CONSTRAINT a UNIQUE(x) CONSTRAINT b` both
2235 /// engine-measured accept, any number of bare/named-with-body constraints chaining freely.
2236 /// Parsed only when nothing but the element terminator (`,`/`)`) follows the name — a
2237 /// `CONSTRAINT <name>` immediately followed by a constraint element (`CHECK`, `UNIQUE`, …)
2238 /// still takes that element as its body, unaffected by this flag. On for SQLite/Lenient. Off
2239 /// elsewhere (PostgreSQL engine-measured rejects a bodyless `CONSTRAINT <name>`, requiring a
2240 /// constraint element), where a `CONSTRAINT <name>` with nothing following is a clean parse
2241 /// error.
2242 pub bare_constraint_name: bool,
2243 /// Accept the PostgreSQL `EXCLUDE [USING <method>] (<element> WITH <operator> [, ...]) [tail]`
2244 /// exclusion constraint as a table element (`CREATE TABLE t (c circle, EXCLUDE USING gist (c
2245 /// WITH &&))`). On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB (DuckDB, which
2246 /// otherwise inherits the PostgreSQL schema surface, rejects it), where `EXCLUDE` at a
2247 /// constraint position is left unconsumed and surfaces as a clean parse error.
2248 pub exclusion_constraints: bool,
2249 /// Accept the `NO INHERIT` / `NOT VALID` constraint markers on `CHECK` (and `NOT VALID` on
2250 /// `FOREIGN KEY`) constraints — PostgreSQL's shared `ConstraintAttributeSpec` slot, also
2251 /// accepted by DuckDB. On for PostgreSQL/DuckDB/Lenient. Off for ANSI/MySQL/SQLite, where the
2252 /// `NO INHERIT` / `NOT VALID` keywords after a constraint are left unconsumed and surface as a
2253 /// clean parse error. The parser enforces which constraint kinds admit which marker
2254 /// (PostgreSQL rejects `NOT VALID` on `PRIMARY KEY`/`UNIQUE`/`EXCLUDE` and `NO INHERIT` on
2255 /// everything but `CHECK`, in the grammar action — reproduced at parse), so the flag only
2256 /// opens the keywords, not every combination.
2257 pub constraint_no_inherit_not_valid: bool,
2258 /// Accept the PostgreSQL index-backed-constraint parameters on `UNIQUE`/`PRIMARY KEY`: the
2259 /// covering `INCLUDE (<col>, ...)` list, the `NULLS [NOT] DISTINCT` null-treatment (PG 15+),
2260 /// and the `USING INDEX TABLESPACE <name>` index tablespace. The three travel together as one
2261 /// PostgreSQL `ConstraintElem` index-parameter unit (the same "single dialect unit" rationale
2262 /// as [`column_storage`](ColumnDefinitionSyntax::column_storage)). On for PostgreSQL/Lenient. Off for
2263 /// ANSI/MySQL/SQLite/DuckDB (DuckDB rejects all three), where the `INCLUDE`/`NULLS`/`USING`
2264 /// keyword is left unconsumed and surfaces as a clean parse error.
2265 pub index_constraint_parameters: bool,
2266 /// Accept a per-column `COLLATE <collation>` and `ASC`/`DESC` sort order inside a
2267 /// `PRIMARY KEY (...)` / `UNIQUE (...)` table-constraint column list — SQLite's
2268 /// "indexed-column" spelling in constraint position (`PRIMARY KEY (a COLLATE nocase)`,
2269 /// `UNIQUE ('b' COLLATE nocase DESC)`). On for SQLite/Lenient.
2270 ///
2271 /// Engine-measured scope (constraint position, *not* `CREATE INDEX`): SQLite admits a bare
2272 /// column name optionally decorated with `COLLATE`/`ASC`/`DESC`, but prohibits general
2273 /// expressions (`UNIQUE (a+b)` / `(lower(a))` → "expressions prohibited in PRIMARY KEY and
2274 /// UNIQUE constraints") and `NULLS FIRST`/`LAST` ("unsupported use of NULLS FIRST"), so the
2275 /// widened parser stays column-name + `COLLATE` + `ASC`/`DESC` and never fills
2276 /// [`IndexColumn::nulls_first`](crate::ast::IndexColumn). Off for ANSI/PostgreSQL/DuckDB
2277 /// (all engine-measured reject `COLLATE`/`ASC`/`DESC` here — the decoration belongs to their
2278 /// `CREATE INDEX` grammar, not the table constraint), where the keyword is left unconsumed
2279 /// and surfaces as a clean parse error. Also off for MySQL: its `key_part` admits `ASC`/`DESC`
2280 /// and length prefixes / functional `(expr)` parts but not `COLLATE`, a differently-shaped
2281 /// surface with no corpus demand — left off and scoped out rather than modelled as this
2282 /// SQLite-shaped gate.
2283 pub constraint_column_collate_order: bool,
2284 /// Accept the cascading referential actions `ON {DELETE|UPDATE} {CASCADE | SET NULL |
2285 /// SET DEFAULT}` on a foreign-key constraint. On for ANSI/PostgreSQL/MySQL/SQLite/Lenient.
2286 /// DuckDB parse-rejects those three actions ("FOREIGN KEY constraints cannot use CASCADE,
2287 /// SET NULL or SET DEFAULT", probed on 1.5.4) while still admitting `RESTRICT` and
2288 /// `NO ACTION`, so it is off there; with the flag off those three keywords after
2289 /// `ON DELETE`/`ON UPDATE` surface as a clean parse error. Distinct from admitting the
2290 /// `ON DELETE`/`ON UPDATE` clause itself — `RESTRICT`/`NO ACTION` remain free either way.
2291 pub referential_action_cascade_set: bool,
2292 /// Accept a subquery (or `EXISTS` / `IN (SELECT …)`) inside a `CHECK` constraint
2293 /// expression. On for ANSI/PostgreSQL/MySQL/Lenient. Off for SQLite and DuckDB, both of
2294 /// which parse-reject subqueries in `CHECK` ("subqueries prohibited in CHECK constraints",
2295 /// engine-measured); with the flag off a subquery in the CHECK body is a clean parse error.
2296 pub check_constraint_subqueries: bool,
2297}
2298
2299/// Dialect-owned `CREATE INDEX` / `ALTER TABLE` / `DROP` syntax accepted by the parser.
2300///
2301/// The clause-level gates on the index, alter-table, and drop statements — the object-DDL
2302/// surface that decorates an already-dispatched statement rather than deciding its leading
2303/// keyword. Split out of the retired `SchemaChangeSyntax` at its 16-field line as the
2304/// index/alter/drop axis, distinct from the whole-statement-dispatch axis. Each flag is a
2305/// grammar gate: when off the keyword is left unconsumed and rejects.
2306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2307pub struct IndexAlterSyntax {
2308 /// Accept PostgreSQL `ALTER TABLE … RENAME CONSTRAINT <old> TO <new>`.
2309 pub rename_constraint: bool,
2310 /// Accept `ALTER TABLE … SET (<name> = <value>, …)` table options.
2311 pub alter_table_set_options: bool,
2312 /// Accept the unnamed `ALTER TABLE … DROP PRIMARY KEY` action.
2313 pub drop_primary_key: bool,
2314 /// Accept `ALTER TABLE … ALTER COLUMN <name> ADD GENERATED {ALWAYS | BY DEFAULT}
2315 /// AS IDENTITY [(…)]`.
2316 pub alter_column_add_identity: bool,
2317 /// Accept `CREATE INDEX … (<keys>) WITH (<name> = <value>, …)` storage parameters.
2318 pub index_storage_parameters: bool,
2319 /// Accept a trailing `CASCADE` / `RESTRICT` drop behaviour on `DROP` statements
2320 /// and `ALTER TABLE ... DROP` actions (the SQL standard `<drop behavior>`;
2321 /// PostgreSQL). A dialect that does not model dependency behaviour leaves it off.
2322 pub drop_behavior: bool,
2323 /// Accept MySQL's `DROP INDEX <name> ON <table> [ALGORITHM [=] {DEFAULT | INPLACE |
2324 /// INSTANT | COPY}] [LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}]` — the index drop
2325 /// that names its owning table with a mandatory `ON` and carries the online-DDL
2326 /// `ALGORITHM`/`LOCK` execution hints (`drop_index_stmt`/`opt_index_lock_and_algorithm`,
2327 /// `sql_yacc.yy`). Server-measured on mysql:8: the `ON <table>` is mandatory (`DROP INDEX
2328 /// i` with no `ON` is `ER_PARSE_ERROR`), the tail admits at most one `ALGORITHM` and one
2329 /// `LOCK` in either order, and no trailing `CASCADE`/`RESTRICT`. On for MySQL only. Off
2330 /// elsewhere, where `DROP INDEX <name> [, …]` stays the shared name-list drop; enabling it
2331 /// would make the mandatory-`ON` form displace that name-list drop, so Lenient forgoes it
2332 /// to keep the more permissive bare-name form (a documented conflict resolution). With the
2333 /// flag off a `DROP INDEX i ON t` leaves `ON` unconsumed and surfaces as a clean parse
2334 /// error.
2335 pub index_drop_on_table: bool,
2336 /// Accept `CREATE INDEX CONCURRENTLY` (PostgreSQL builds the index without an
2337 /// exclusive table lock). Not ANSI.
2338 pub index_concurrently: bool,
2339 /// Accept the `CREATE INDEX … USING <method>` access-method clause (PostgreSQL
2340 /// `btree`/`hash`/`gin`/`gist`/…; MySQL `USING BTREE`). Not ANSI.
2341 pub index_using_method: bool,
2342 /// Accept a trailing `WHERE <predicate>` partial-index clause on `CREATE INDEX`
2343 /// (PostgreSQL, SQLite). Not ANSI.
2344 pub partial_index: bool,
2345 /// Accept the `IF NOT EXISTS` guard on `CREATE INDEX` (PostgreSQL/SQLite/DuckDB). On for
2346 /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL has no `CREATE INDEX IF NOT EXISTS`
2347 /// (engine-measured `ER_PARSE_ERROR` on mysql:8), so it is off; the `IF NOT EXISTS` is
2348 /// then left unconsumed and the following index name surfaces as a clean parse error.
2349 /// Distinct from the `CREATE TABLE`/`CREATE DATABASE` guards, which MySQL *does* admit.
2350 pub index_if_not_exists: bool,
2351 /// Accept the per-key `NULLS FIRST` / `NULLS LAST` null-ordering modifier on a
2352 /// `CREATE INDEX` column (PostgreSQL/SQLite 3.30+). On for ANSI/PostgreSQL/SQLite/DuckDB/
2353 /// Lenient. MySQL has no index-key `NULLS` ordering (engine-measured `ER_PARSE_ERROR` on
2354 /// mysql:8; it orders NULLs implicitly), so it is off; the `NULLS` keyword is then left
2355 /// unconsumed and surfaces as a clean parse error. Independent of the `ORDER BY` null
2356 /// ordering, which is a separate grammar position.
2357 pub index_nulls_order: bool,
2358 /// Accept the extended `ALTER TABLE` surface beyond SQLite's lenient action set: the
2359 /// table-level `IF EXISTS`, comma-separated multiple actions, `ALTER COLUMN …`, the
2360 /// `ADD PRIMARY KEY`/`UNIQUE`/`FOREIGN KEY` table constraints, and the `IF [NOT]
2361 /// EXISTS` guard on `ADD`/`DROP COLUMN` (PostgreSQL/MySQL). On for ANSI/PostgreSQL/
2362 /// MySQL/DuckDB/Lenient. Off for SQLite, whose `ALTER TABLE` (engine-measured via
2363 /// rusqlite) admits `RENAME TO`/`RENAME COLUMN`, a single `ADD [COLUMN] <def>` / `ADD
2364 /// [CONSTRAINT …] CHECK (…)`, and a single `DROP [COLUMN]` / `DROP CONSTRAINT` — those
2365 /// stay accepted with the flag off; every other form is left unconsumed and surfaces
2366 /// as a clean parse error. Distinct from [`ExistenceGuards::if_exists`], which stays
2367 /// on for SQLite (its `DROP TABLE IF EXISTS` is valid) — only the *`ALTER`* existence
2368 /// guard is gated here.
2369 pub alter_table_extended: bool,
2370 /// Accept a comma-separated multi-action `ALTER TABLE` list
2371 /// (`ALTER TABLE t ADD COLUMN a INT, DROP COLUMN b`). On for ANSI/PostgreSQL/MySQL/Lenient.
2372 /// Off for DuckDB (parse-rejects with "Only one ALTER command per statement is supported",
2373 /// probed on 1.5.4) and moot for SQLite (its
2374 /// [`alter_table_extended`](IndexAlterSyntax::alter_table_extended) is already off, so the
2375 /// multi-action loop is never entered). Only reachable where `alter_table_extended` is on;
2376 /// with the flag off the first action parses and a trailing comma surfaces as a clean parse
2377 /// error.
2378 pub alter_table_multiple_actions: bool,
2379 /// Accept an `IF EXISTS` / `IF NOT EXISTS` existence guard *inside* `ALTER TABLE` — the
2380 /// table-level `ALTER TABLE IF EXISTS t …` and the per-action `ADD COLUMN IF NOT EXISTS`
2381 /// / `DROP [COLUMN|CONSTRAINT] IF EXISTS` guards. On for PostgreSQL/DuckDB/Lenient (and
2382 /// unused by SQLite, whose non-[`alter_table_extended`](IndexAlterSyntax::alter_table_extended) path
2383 /// parses no guard, so it rides that gate —
2384 /// [`FeatureDependencyViolation::AlterExistenceGuardsWithoutAlterTableExtended`]). MySQL
2385 /// supports the extended `ALTER TABLE` surface (multi-action
2386 /// lists, `ADD`/`DROP CONSTRAINT`, `ALTER COLUMN`) but *not* these guards
2387 /// (`ALTER TABLE IF EXISTS`, `ADD COLUMN IF NOT EXISTS`, `DROP COLUMN IF EXISTS` are each
2388 /// `ER_PARSE_ERROR` on mysql:8), so it is off there; the `IF` keyword is then read as a
2389 /// name and surfaces as a clean parse error. Distinct from
2390 /// [`ExistenceGuards::if_exists`](ExistenceGuards::if_exists), which MySQL keeps on for
2391 /// `DROP TABLE IF EXISTS`.
2392 pub alter_existence_guards: bool,
2393 /// Accept dotted nested-column paths in DuckDB `ALTER TABLE` column targets for the
2394 /// actions the engine parses (`ADD COLUMN s.k`, `DROP COLUMN s.k`, and old-side
2395 /// `RENAME COLUMN s.k TO k2`). The gate does not affect `ALTER COLUMN` or the rename
2396 /// destination: DuckDB 1.5.4 parse-rejects dotted paths in those positions.
2397 pub alter_nested_column_paths: bool,
2398 /// Accept the PostgreSQL `ALTER TABLE … ALTER COLUMN` actions beyond `SET`/`DROP
2399 /// DEFAULT` — `SET DATA TYPE <type>` (and its bare `TYPE <type>` synonym, with an
2400 /// optional `USING <expr>`) plus `SET`/`DROP NOT NULL`. On for PostgreSQL/DuckDB/Lenient.
2401 /// MySQL's `ALTER COLUMN` admits only `SET`/`DROP DEFAULT` — it changes a column's type
2402 /// with `MODIFY`/`CHANGE` — so `ALTER COLUMN i SET DATA TYPE …`/`SET NOT NULL`/`DROP NOT
2403 /// NULL` are `ER_PARSE_ERROR` on mysql:8; with the flag off those actions surface as a
2404 /// clean parse error. SQLite never reaches the action (its
2405 /// [`alter_table_extended`](IndexAlterSyntax::alter_table_extended) is off), so this is moot there —
2406 /// it rides that gate
2407 /// ([`FeatureDependencyViolation::AlterColumnSetDataTypeWithoutAlterTableExtended`]).
2408 pub alter_column_set_data_type: bool,
2409 /// Accept a routine reference's parenthesized argument-type list — `DROP FUNCTION f(INT)`,
2410 /// `GRANT EXECUTE ON FUNCTION f(INT) …` (PostgreSQL overload-disambiguation). On for
2411 /// ANSI/PostgreSQL/DuckDB/Lenient. MySQL identifies a routine by name alone, so the arg
2412 /// list is a syntax error there (`DROP FUNCTION f(INT)` → engine-measured `ER_PARSE_ERROR`
2413 /// on mysql:8); with the flag off the `(` is left unconsumed and surfaces as a clean parse
2414 /// error. Off for SQLite too (it has no stored routines — [`routines`](StatementDdlGates::routines) is
2415 /// already off, so this is moot there).
2416 pub routine_arg_types: bool,
2417 /// Accept a `CREATE FUNCTION` parameter default — `func_arg DEFAULT <expr>` / `func_arg =
2418 /// <expr>` (PostgreSQL `func_arg_with_default`). On for ANSI/PostgreSQL/DuckDB/Lenient.
2419 /// MySQL's routine parameters are `[IN|OUT|INOUT] name type` with no default, so a
2420 /// `DEFAULT`/`=` after the type is a syntax error there (`ER_PARSE_ERROR` on mysql:8); with
2421 /// the flag off the `DEFAULT`/`=` is left unconsumed and the parameter-list close surfaces
2422 /// as a clean parse error. Distinct from [`routine_arg_types`](Self::routine_arg_types)
2423 /// (which gates the *reference*-site arg-type list on `DROP`/`GRANT`): this gates the
2424 /// *definition*-site default on `CREATE FUNCTION`. Off for SQLite too (no stored routines —
2425 /// [`routines`](StatementDdlGates::routines) is already off, so this is moot there).
2426 pub routine_arg_defaults: bool,
2427 /// Accept a `CREATE FUNCTION` parameter argument mode — the `arg_class` prefix
2428 /// `IN`/`OUT`/`INOUT`/`VARIADIC` before the parameter (PostgreSQL `func_arg`). On for
2429 /// ANSI/PostgreSQL/DuckDB/Lenient. MySQL's `CREATE FUNCTION` parameters are `name type`
2430 /// with no mode (the `IN`/`OUT`/`INOUT` modes are a stored-*procedure* form, a distinct
2431 /// statement), so a mode keyword before a `CREATE FUNCTION` parameter is a syntax error
2432 /// there; with the flag off the keyword is left for the name/type parse (a reserved mode
2433 /// keyword like `IN`/`VARIADIC` then surfaces as a clean parse error). A sibling of
2434 /// [`routine_arg_defaults`](Self::routine_arg_defaults): both gate independent
2435 /// *definition*-site `CREATE FUNCTION` parameter facets that MySQL's grammar omits. Off
2436 /// for SQLite too (no stored routines — [`routines`](StatementDdlGates::routines) is
2437 /// already off, so this is moot there).
2438 pub routine_arg_modes: bool,
2439 /// Accept a string-constant (`Sconst`) spelling of the routine `LANGUAGE` name —
2440 /// `LANGUAGE 'sql'`/`E'sql'`/`$$sql$$` — alongside the bare word, PostgreSQL's
2441 /// `NonReservedWord_or_Sconst` operand (the same shape the `DO … LANGUAGE` argument
2442 /// spells). On for PostgreSQL/Lenient. MySQL's routine `LANGUAGE` admits only the bare
2443 /// word `SQL` — `LANGUAGE 'SQL'` is engine-measured `ER_PARSE_ERROR` (1064) on mysql:8 for
2444 /// both `CREATE FUNCTION` and `CREATE PROCEDURE` — so off there; with the flag off a string
2445 /// in the `LANGUAGE` position falls through to the bare-word parse, which rejects the string
2446 /// as MySQL does. Off for ANSI too: the SQL-standard `<language name>` is a bare identifier
2447 /// (`SQL`/`C`/`ADA`/…), not a string. A bit-string (`b'…'`/`x'…'`) or national (`N'…'`)
2448 /// constant is never an `Sconst`, so it stays a reject even where the flag is on (matching
2449 /// PostgreSQL). Off for SQLite too (no stored routines — [`routines`](StatementDdlGates::routines)
2450 /// is already off, so this is moot there).
2451 pub routine_language_string: bool,
2452}
2453
2454/// Dialect-owned `IF [NOT] EXISTS` existence guards on DDL statements.
2455///
2456/// The SQL existence guards — `IF EXISTS` on `DROP`/`ALTER`, `IF NOT EXISTS` on
2457/// `CREATE` — are dialect data: each flag decides whether the parser admits
2458/// the guard at one statement site, and when off the `IF [NOT] EXISTS` is left
2459/// unconsumed and surfaces as a clean parse error (the same reject mechanism the other
2460/// grammar gates use). They live in their own sub-struct rather than scattered through
2461/// the retired `SchemaChangeSyntax` because their bundle membership differs per dialect: leaving a
2462/// coarse `if_exists` bundle in `SchemaChangeSyntax` made every new dialect bolt on a
2463/// separate exception flag (`view_if_not_exists`, `create_database_if_not_exists`), so a
2464/// dedicated per-site table is the shape that does not accrete one exception per dialect.
2465///
2466/// Two-level spelling convention (uniform across the dialect-data knobs): a top-level
2467/// [`FeatureSet`] assembly spells every field explicitly, while a *sub-preset* const of a
2468/// knob like this one may struct-update-derive from a sibling with `..Self::OTHER` (the
2469/// [`SelectSyntax::DUCKDB`] precedent) — the base preset stays the exhaustive source of
2470/// truth and the derived one records only its deltas.
2471///
2472/// [`SelectSyntax::DUCKDB`]: crate::dialect::SelectSyntax
2473#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2474pub struct ExistenceGuards {
2475 /// Accept `IF EXISTS` on `DROP`/`ALTER TABLE` (and on their column/constraint
2476 /// sub-actions) and `IF NOT EXISTS` on `ADD COLUMN` (PostgreSQL, MySQL, SQLite;
2477 /// not ANSI, whose `DROP`/`ALTER` has no existence guard). One flag gates the
2478 /// drop/alter/add-column existence guards together because a dialect that spells one
2479 /// spells them all; the `ALTER TABLE`/`ADD COLUMN` sites additionally require
2480 /// [`IndexAlterSyntax::alter_table_extended`] (SQLite keeps this guard on for its
2481 /// `DROP … IF EXISTS` while its plain-`ALTER` surface stays off).
2482 pub if_exists: bool,
2483 /// Accept `IF NOT EXISTS` on a *plain* (non-materialized) `CREATE VIEW` (SQLite).
2484 /// PostgreSQL admits `IF NOT EXISTS` only on a `CREATE MATERIALIZED VIEW` (that
2485 /// form is always accepted, independent of this flag), and MySQL has no view
2486 /// existence guard at all, so off there; SQLite spells `CREATE VIEW IF NOT EXISTS`
2487 /// over a regular view. When off, the `IF NOT EXISTS` is left unconsumed on a plain
2488 /// view and surfaces as a clean parse error.
2489 pub view_if_not_exists: bool,
2490 /// Accept the `CREATE DATABASE IF NOT EXISTS <name>` existence guard (MySQL; also
2491 /// SQLite has no `CREATE DATABASE` at all, so off there). PostgreSQL's `CREATE
2492 /// DATABASE` has no `IF NOT EXISTS`, so this is a dedicated site rather than a reuse
2493 /// of [`if_exists`](Self::if_exists) — that one is on in PostgreSQL, which must still
2494 /// reject `CREATE DATABASE IF NOT EXISTS`. When off, the `IF NOT EXISTS` is left
2495 /// unconsumed and surfaces as a clean parse error.
2496 pub create_database_if_not_exists: bool,
2497}
2498
2499/// Dialect-owned SELECT-core syntax extensions accepted by the parser.
2500///
2501/// The SELECT-body forms — the projection / select-list, the set-operation-operand, and
2502/// the `VALUES`-constructor surface — after the row-limiting/locking query tail and the
2503/// grouping/ordering clauses split out into [`QueryTailSyntax`] and [`GroupingSyntax`] at
2504/// this struct's 16-field line. Acceptance is explicit dialect data, not a parser-side type
2505/// check: most flags are widening grammar gates (when off, the introducing keyword is left
2506/// unconsumed and the clause surfaces as a clean parse error), and a few are narrowing
2507/// well-formedness enforcements (per the flag-naming rule above).
2508#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2509pub struct SelectSyntax {
2510 /// Accept PostgreSQL `SELECT DISTINCT ON (<expr>, ...)`.
2511 pub distinct_on: bool,
2512 /// Accept PostgreSQL's `SELECT … INTO [TEMP] <table>` create-table form (the
2513 /// `INTO` target sits between the projection and `FROM`, materializing the result
2514 /// into a new relation). When off, `INTO` is left unconsumed and surfaces as a
2515 /// clean parse error. This gates *only* the create-table form: bare standard
2516 /// `SELECT … INTO <variable>` is PSM host/local-variable assignment — a different
2517 /// construct — so ANSI leaves this off, and MySQL has no `SELECT INTO <table>`.
2518 pub select_into: bool,
2519 /// Accept an empty SELECT target list — a projection with zero items (`SELECT`,
2520 /// `SELECT;`, `SELECT FROM t`, `SELECT WHERE …`). libpg_query's raw grammar makes
2521 /// the projection optional before any clause (PostgreSQL rejects it later, at
2522 /// parse-analysis, which is past our parse-level parity contract), so the honest
2523 /// closure accepts it under the PostgreSQL preset only. When off (ANSI/MySQL, which
2524 /// both require ≥1 select item), the projection's first-item requirement stands and
2525 /// a bare `SELECT` is a clean parse error.
2526 pub empty_target_list: bool,
2527 /// Accept DuckDB's `QUALIFY <predicate>` post-window filter clause, written after
2528 /// the `WINDOW` clause (DuckDB's grammar order: `… HAVING … WINDOW … QUALIFY …`;
2529 /// verified against DuckDB 1.5.4). When off (ANSI/PostgreSQL/MySQL/SQLite, none of
2530 /// which have the clause), the `QUALIFY` keyword is left unconsumed and surfaces
2531 /// as a clean parse error — the same reject mechanism the other SELECT gates use.
2532 /// This flag gates only the *clause*; whether `QUALIFY` is usable as a plain
2533 /// identifier is the orthogonal reservation data (the `reserved_*` keyword sets:
2534 /// DuckDB reserves it like `HAVING`, every other shipped dialect leaves it a free
2535 /// identifier). On for DuckDB / Lenient, off elsewhere.
2536 pub qualify: bool,
2537 /// Accept a string literal as a column alias (`SELECT 1 AS 'x'`, and MySQL's
2538 /// `SELECT 1 AS "x"` where `"…"` is a string), MySQL's rule. The alias parser admits
2539 /// a `String` token after `AS`, materialises its value as the alias identifier, and
2540 /// records the source quote ([`QuoteStyle::Single`](crate::ast::QuoteStyle::Single) /
2541 /// [`Double`](crate::ast::QuoteStyle::Double)) so it renders back quoted. When off,
2542 /// a string in alias position is left unconsumed and surfaces as a clean parse error
2543 /// (the standard requires an identifier). On for MySQL / Lenient, off elsewhere.
2544 pub alias_string_literals: bool,
2545 /// Accept a string literal as a *bare* (`AS`-less) column alias (`SELECT 1 'x'`), on
2546 /// top of the `AS`-introduced form [`alias_string_literals`](Self::alias_string_literals)
2547 /// gates. SQLite and MySQL read a string in bare-alias position as the column name
2548 /// (engine-measured: `SELECT 1 'x'` prepares on both, naming the column `x`), while
2549 /// **DuckDB accepts only the `AS 'x'` form and rejects the bare `SELECT 1 'x'`** (probed
2550 /// on 1.5.4) — so the bare position is its own axis rather than a rider on
2551 /// [`alias_string_literals`](Self::alias_string_literals). A separate axis rather than
2552 /// widening that flag because the two enablers split: DuckDB arms the `AS` form here but
2553 /// not the bare one. When off, a string in bare-alias position is left unconsumed and
2554 /// surfaces as a clean parse error. On for SQLite / MySQL / Lenient, off elsewhere.
2555 ///
2556 /// MySQL's bare string alias overlaps same-line adjacent-string concatenation
2557 /// ([`same_line_adjacent_concat`](StringLiteralSyntax::same_line_adjacent_concat)):
2558 /// `SELECT 'a' 'b'` is the single value `'ab'`, while `SELECT 1 'x'` is a bare alias
2559 /// (both engine-measured on mysql:8.4.10). No carve-out flag is needed — parse ordering
2560 /// resolves it: a string primary greedily folds every following unprefixed string
2561 /// continuation into its own value before the alias parser runs, so a trailing string
2562 /// reaches the bare-alias branch only when the preceding expression was not itself a
2563 /// string. A prefixed string (`N'…'`, `_charset'…'`, bit) is never a bare alias
2564 /// (rejected as an identifier), matching the engine's rejects (`SELECT 'a' _utf8'b'`).
2565 pub bare_alias_string_literals: bool,
2566 /// Accept DuckDB's FROM-first SELECT order: a query primary may lead with the
2567 /// `FROM` clause (`FROM <tables> [SELECT [DISTINCT] <projection>] …`), the projection
2568 /// written after it, or omitted entirely — the bare `FROM <tables>` is an implicit
2569 /// `SELECT *`. Semantically the ordinary SELECT (DuckDB serializes `FROM t SELECT x`
2570 /// and `SELECT x FROM t` to the same tree; probed on 1.5.4), so it parses to the
2571 /// canonical [`Select`](crate::ast::Select) tagged
2572 /// [`SelectSpelling::FromFirst`](crate::ast::SelectSpelling). The projection, when
2573 /// present, must sit immediately after the `FROM` clause (DuckDB syntax-errors on
2574 /// `FROM t WHERE x SELECT y` / `FROM t GROUP BY a SELECT a`), so it is parsed only in
2575 /// that position and every following clause parses in its ordinary place. When off
2576 /// (ANSI/PostgreSQL/MySQL/SQLite, none of which admit a statement-position `FROM`), a
2577 /// leading `FROM` is never a query start, so it surfaces as a clean parse error — the
2578 /// over-acceptance guard the differential oracle relies on. The gate is read wherever
2579 /// a query primary may begin (statement, set operand, scalar/`IN` subquery, CTE body,
2580 /// derived table), so the one flag composes everywhere. On for DuckDB / Lenient, off
2581 /// elsewhere.
2582 pub from_first: bool,
2583 /// Accept SQL's `<explicit table>` form `TABLE <name>` (equivalent to
2584 /// `SELECT * FROM <name>`). On for ANSI / PostgreSQL / DuckDB / MySQL / Lenient
2585 /// (engine-measured: libpg_query and libduckdb accept; mysql:8 accepts). Off for
2586 /// SQLite, which syntax-rejects a leading `TABLE` (engine-measured on rusqlite:
2587 /// `near "TABLE": syntax error`). When off, a leading `TABLE` is left undispatched
2588 /// and surfaces as an unknown statement.
2589 pub explicit_table: bool,
2590 /// Accept DuckDB's `UNION [ALL] BY NAME` name-matched set operation: pair the two
2591 /// inputs' columns by name (padding missing columns with NULL) instead of by
2592 /// position ([`SetExpr::SetOperation::by_name`](crate::ast::SetExpr) — a flag on
2593 /// the set-op node, orthogonal to `all`). DuckDB restricts `BY NAME` to `UNION`
2594 /// (`INTERSECT BY NAME` / `EXCEPT BY NAME` are syntax errors; probed on 1.5.4), so
2595 /// the parser consumes the modifier only after `UNION`; after `INTERSECT`/`EXCEPT`
2596 /// the `BY` keyword is left unconsumed and surfaces as the usual operand reject.
2597 /// When off (ANSI/PostgreSQL/MySQL/SQLite, none of which have the modifier), the
2598 /// `BY` after a set operator is likewise left unconsumed and a bare `UNION BY NAME`
2599 /// is a clean parse error — the same reject mechanism the other SELECT gates use.
2600 /// On for DuckDB / Lenient, off elsewhere.
2601 pub union_by_name: bool,
2602 /// Accept DuckDB's `*` / `t.*` wildcard modifiers — the `EXCLUDE (…)`,
2603 /// `REPLACE (expr AS col)`, and `RENAME (col AS new)` tail that rewrites which
2604 /// columns the wildcard expands to ([`WildcardOptions`](crate::ast::WildcardOptions)
2605 /// on the wildcard select item). DuckDB fixes their surface order (`EXCLUDE`, then
2606 /// `REPLACE`, then `RENAME`, each at most once; any other order is a syntax error,
2607 /// probed on 1.5.4), which the parser enforces by parsing them in that sequence.
2608 /// One flag, not three: DuckDB ships the trio as a single grammar production and no
2609 /// shipped dialect adopts a subset (the paired-flag doctrine). This gates the
2610 /// select-list/`RETURNING` wildcard tail; the sibling `COLUMNS(…)` expression form
2611 /// is [`CallSyntax::columns_expression`](CallSyntax::columns_expression),
2612 /// a separate grammar position (expression vs projection item). When off
2613 /// (ANSI/PostgreSQL/MySQL/SQLite), the `EXCLUDE`/`REPLACE`/`RENAME` keyword after a
2614 /// `*` is left unconsumed and surfaces as a clean parse error — the over-acceptance
2615 /// guard the differential oracle relies on. On for DuckDB / Lenient, off elsewhere.
2616 pub wildcard_modifiers: bool,
2617 /// Accept the `REPLACE (<expr> AS <column>, ...)` wildcard tail independently of
2618 /// the `EXCLUDE` and `RENAME` modifier family.
2619 pub wildcard_replace: bool,
2620 /// Accept `INTERSECT ALL`.
2621 pub intersect_all: bool,
2622 /// Accept `EXCEPT ALL`.
2623 pub except_all: bool,
2624 /// Accept a trailing `[AS] alias` on a *qualified* wildcard select item (`SELECT t.* x`,
2625 /// `SELECT t.* AS x`, `SELECT s.t.* x`), folding it onto the
2626 /// [`QualifiedWildcard`](crate::ast::SelectItem::QualifiedWildcard) item's `alias` slot
2627 /// with the source [`AliasSpelling`](crate::ast::AliasSpelling) (bare vs `AS`).
2628 ///
2629 /// PostgreSQL treats `t.*` as an ordinary column-reference expression (`columnref`, an
2630 /// `a_expr`), so it flows through the very same `target_el: a_expr [AS] label` projection
2631 /// alias an ordinary value takes: the bare form admits the `BareColLabel` reserved-word
2632 /// set (`SELECT t.* select` parses, `SELECT t.* from`/`order` reject) and the `AS` form the
2633 /// full `ColLabel` set (`SELECT t.* AS from` parses) — measured against libpg_query, which
2634 /// matches the parser's ordinary projection-alias boundary exactly, so the alias is parsed
2635 /// by reusing it. The bare `*` wildcard is the *separate* non-aliasable `target_el:
2636 /// '*'` production, so this gate never touches it (a bare-`*` alias is DuckDB's rename-all,
2637 /// which rides [`wildcard_modifiers`](Self::wildcard_modifiers)).
2638 ///
2639 /// Deliberately its own axis, *not* folded into [`wildcard_modifiers`](Self::wildcard_modifiers):
2640 /// the two behaviours have different measured boundaries — the `EXCLUDE`/`REPLACE`/`RENAME`
2641 /// modifier tail and the bare-`*` rename-all alias are DuckDB-only, whereas a qualified
2642 /// wildcard's plain alias is accepted by **PostgreSQL and DuckDB** (engine-probed: PG's
2643 /// libpg_query and DuckDB 1.5.4 accept `t.* x`/`t.* AS x`, while SQLite/MySQL and the SQL
2644 /// standard special-case `t.*` as a non-aliasable wildcard production and reject it —
2645 /// measured Reject on rusqlite/mysql:8 with the table provisioned). One behaviour = one flag.
2646 /// When off (ANSI/MySQL/SQLite and the ANSI-derived presets), the alias word is left
2647 /// unconsumed after `t.*` and surfaces as a clean parse error. On for PostgreSQL / DuckDB /
2648 /// Lenient, off elsewhere.
2649 pub qualified_wildcard_alias: bool,
2650 /// Accept a *parenthesized query* as a set-operation / statement / CTE-body / CTAS /
2651 /// `INSERT`-source operand — `(SELECT …) UNION (SELECT …)`, `((SELECT …)) LIMIT 1`,
2652 /// `CREATE TABLE t AS (SELECT …)` (PostgreSQL `select_with_parens`; the canonical
2653 /// set-op AST). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no
2654 /// parenthesized compound operand — a `select-core` is `SELECT …`/`VALUES …`, never
2655 /// `( … )` — so it is off; a leading `(` in operand position is then a syntax error
2656 /// (engine-measured via rusqlite). The parenthesized query that SQLite *does* admit —
2657 /// a `FROM` table-or-subquery grouping (`FROM ((SELECT 1))`) and an expression-position
2658 /// scalar subquery (`SELECT ((SELECT 1))`) — is a complete standalone primary, not an
2659 /// operand, and stays accepted with the flag off (the parser threads a grouping
2660 /// context through those two positions); a paren-query there that is *extended* by a
2661 /// set operator or an `ORDER BY`/`LIMIT` tail (`FROM ((SELECT 1) UNION (SELECT 2))`,
2662 /// `FROM ((SELECT 1) LIMIT 1)`) is the syntax error SQLite reports.
2663 pub parenthesized_query_operands: bool,
2664 /// Reject a `VALUES` table-value constructor whose rows differ in width
2665 /// (`VALUES (1, 2), (3)`) at *parse* time. Unlike the additive gates above, this is a
2666 /// well-formedness *enforcement*: on rejects the ragged constructor, off accepts it.
2667 ///
2668 /// Equal row degree is a universal SQL rule, but engines check it at different phases,
2669 /// and this validator models per-dialect *parse* acceptance. DuckDB checks at parse —
2670 /// `Parser Error: VALUES lists must all be the same length`, in every VALUES position
2671 /// (standalone, derived table, `INSERT`; measured on 1.5.4) — so the DuckDb preset
2672 /// turns it on. PostgreSQL's raw grammar (libpg_query) and MySQL accept a ragged
2673 /// constructor and defer the arity check to parse-analysis / bind, past our parse-level
2674 /// parity contract, so it stays off there (their presets keep accepting it, exactly as
2675 /// `empty_target_list` accepts a bare `SELECT` under the PostgreSQL preset). `LENIENT`
2676 /// is a pure-acceptance superset, so it also leaves this off. Both counts the check
2677 /// compares — the two rows' arities — are present in the parse tree, so this is a
2678 /// shape-level gate, not a semantic one. On for DuckDB only; off elsewhere.
2679 pub values_rows_require_equal_arity: bool,
2680 /// Accept a bare-parenthesized row (`VALUES (1), (2)`) as a `VALUES` table-value
2681 /// constructor in *query* position — a top-level query body, a set-operation operand,
2682 /// a CTE body, or a derived table. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL
2683 /// spells the query-position constructor `VALUES ROW(1), ROW(2)` (the `TABLE`/`VALUES`
2684 /// row-constructor grammar): a bare `(…)` row there is the syntax error MySQL reports
2685 /// (engine-measured on mysql:8 — `VALUES (1)` / `SELECT … FROM (VALUES (1)) …` /
2686 /// `VALUES (1) UNION …` all `ER_PARSE_ERROR`), so it is off there. Scoped to query
2687 /// position only: the `INSERT … VALUES (…)` source list is a distinct grammar that
2688 /// admits bare rows on every dialect (MySQL included), so it is parsed on a separate
2689 /// path this gate does not touch.
2690 pub values_row_constructor: bool,
2691 /// Reject a reserved word as an `AS`-introduced *projection* alias
2692 /// (`SELECT 1 AS <label>`), routing the position to the stricter
2693 /// [`reserved_bare_alias`](FeatureSet::reserved_bare_alias) set instead of the
2694 /// permissive [`reserved_as_label`](FeatureSet::reserved_as_label). Off for
2695 /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient, whose projection `AS` alias is a
2696 /// PostgreSQL-style `ColLabel`: PostgreSQL admits every keyword there
2697 /// (`SELECT a AS select` parses) via its empty `reserved_as_label`, and SQLite draws
2698 /// no `ColId`/`ColLabel` split so its non-empty `reserved_as_label` already rejects
2699 /// them — neither needs this gate. MySQL has no `ColLabel` relaxation: an `AS` alias
2700 /// rejects exactly the words a *bare* alias does (`SELECT 1 AS range`/`AS left`/`AS
2701 /// delete` are `ER_PARSE_ERROR` on mysql:8, while the non-reserved `SELECT 1 AS any`
2702 /// parses), so it is on there. Scoped to the projection `AS` alias only: the
2703 /// dotted-name continuation (`t.range`, a qualified-name label MySQL admits) is a
2704 /// separate position that keeps the permissive `reserved_as_label` set.
2705 pub as_alias_rejects_reserved: bool,
2706 /// Accept a single trailing comma before a list's closing delimiter in the list
2707 /// positions DuckDB tolerates it: the `SELECT` projection list (`SELECT a, b, FROM
2708 /// t`), the query-position and `INSERT` `VALUES` row lists and each parenthesized
2709 /// row (`VALUES (1), (2),` / `VALUES (1, 2,)`), the `[…]` / `ARRAY[…]` list and
2710 /// `{…}` struct and `MAP {…}` collection literals, the `IN (…)` list, the `GROUP BY`
2711 /// key list and its `ROLLUP(…)` / `CUBE(…)` / `GROUPING SETS (…)` sub-lists
2712 /// (`GROUP BY a, b,` / `GROUP BY ROLLUP(a, b,)`), the wildcard-modifier lists
2713 /// (`* EXCLUDE (a,)`, `* REPLACE (e AS c,)`, `* RENAME (a AS b,)`), the
2714 /// `COALESCE` special-form argument list (`coalesce(1, 2,)`), and the
2715 /// `CREATE TABLE` table-element list (`CREATE TABLE t (a INT, b INT,)`), after a
2716 /// column or a constraint element alike. The comma is
2717 /// discarded — the list shape is unchanged, so no AST node carries it and the render
2718 /// drops it (canonical form, a lossy-spelling trade); it is not semantically
2719 /// meaningful.
2720 ///
2721 /// Scoped to those list sites only, because DuckDB is *not* uniform: engine-probed
2722 /// (1.5.4), an ordinary function-argument list (`greatest(1, 2,)`), a bare
2723 /// parenthesized / row constructor (`(1, 2,)`), an `ORDER BY` / `PARTITION BY` list,
2724 /// and an `INSERT` *column* list (`INSERT INTO t (a, b,) …`) all reject the trailing
2725 /// comma, so this gate is applied per accepting list site rather than in the shared
2726 /// `parse_comma_separated`. `COALESCE` is the lone function-call exception —
2727 /// `coalesce(1, 2,)` accepts because DuckDB parses it as a grammar special form, not
2728 /// a `func_application`, whereas every sibling (`greatest`/`least`/`nullif`/`concat`)
2729 /// keeps rejecting the comma. Only a *single* trailing comma is admitted (`[1, 2, ,]`
2730 /// and a leading `[,]` stay parse errors). When off (ANSI/PostgreSQL/MySQL/SQLite,
2731 /// none of which admit a trailing comma), the dangling comma is left for the item
2732 /// parser to reject — the same clean parse error those dialects report. On for DuckDB
2733 /// / Lenient, off elsewhere.
2734 pub trailing_comma: bool,
2735 /// Accept DuckDB's **projection** prefix colon alias — an alias written *before* its
2736 /// value as `<alias> : <value>` on a select-item head only (`SELECT j : 42` → alias `j`
2737 /// on value `42`). Pure sugar for the standard trailing `AS` alias: DuckDB records it
2738 /// in the ordinary alias field and canonically re-emits `AS` (json round-trip on 1.5.4:
2739 /// `SELECT j : 42` → `SELECT 42 AS j`). The FROM-position twin lives on
2740 /// [`TableExpressionSyntax::prefix_colon_alias`] so a dialect can enable one position
2741 /// without the other.
2742 ///
2743 /// Grammar-position gate (not lexical): lone `:` is always `Colon` punctuation. Collides
2744 /// at a value head with [`ExpressionSyntax::semi_structured_access`]
2745 /// ([`GrammarConflict::PrefixColonAliasVersusSemiStructuredAccess`]) when either this
2746 /// flag or the table-position twin is on. On for DuckDB / Lenient; off elsewhere.
2747 pub prefix_colon_alias: bool,
2748 /// Accept the Hive/Spark `LATERAL VIEW [OUTER] <generator>(args) <alias>
2749 /// [AS <col> [, …]]` table-generating clauses, written after the whole `FROM`
2750 /// clause and before `WHERE` and repeatable (Hive LanguageManual LateralView:
2751 /// `fromClause: FROM baseTable (lateralView)*`; Spark `SqlBaseParser.g4` places
2752 /// `lateralView*` at the same fromClause tail), modelled as
2753 /// [`Select::lateral_views`](crate::ast::Select) (see
2754 /// [`LateralView`](crate::ast::LateralView) for the typed shape, the sqlparser-rs
2755 /// parity reshapings, and the recorded acceptance bound — there is no Hive/Spark
2756 /// oracle, so the two engines' published grammars are the acceptance evidence).
2757 ///
2758 /// **Leading-keyword dispatch, position- and follow-token-partitioned against
2759 /// LATERAL derived tables.** `LATERAL` also introduces the standard derived-table /
2760 /// function factor ([`TableFactorSyntax::lateral`]). The two occupy disjoint
2761 /// grammar positions — a table-factor head (after `FROM`/`,`/a join keyword) versus
2762 /// after the *complete* FROM relation list — and split on the follow token (`VIEW`
2763 /// here; `(` or a function/subquery head there), so the dispatch is unambiguous
2764 /// under every preset combination and needs no [`GrammarConflict`] entry: this
2765 /// clause claims a `LATERAL` only when `VIEW` follows and only once the FROM list
2766 /// is complete, a cursor position the factor grammar never holds.
2767 ///
2768 /// **On for Hive, Databricks, and Lenient.** Hive and Databricks have no
2769 /// differential oracle, so this is a no-oracle acceptance addition carried by the
2770 /// two conservative presets and the permissive union: every oracle-compared dialect
2771 /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a
2772 /// post-FROM `LATERAL` as unconsumed input — so conformance sweeps see zero
2773 /// movement.
2774 pub lateral_view_clause: bool,
2775 /// Accept the Oracle-style `[START WITH <cond>] CONNECT BY [NOCYCLE] <cond>`
2776 /// hierarchical query clause, written **after `WHERE` and before `GROUP BY`** with
2777 /// `START WITH` and `CONNECT BY` in either order, modelled as
2778 /// [`Select::connect_by`](crate::ast::Select) (see
2779 /// [`HierarchicalClause`](crate::ast::HierarchicalClause) for the typed shape, the
2780 /// either-order fidelity tag, and the recorded acceptance bounds). The clause also
2781 /// enables the [`UnaryOperator::Prior`](crate::ast::UnaryOperator) operator, but only
2782 /// inside the `CONNECT BY` condition — the global expression grammar is unchanged, so
2783 /// a bare `prior` stays an ordinary column name.
2784 ///
2785 /// **Grammar evidence (no Oracle/Snowflake oracle).** Snowflake's public docs are the
2786 /// citable grammar (there is no Oracle preset): they document
2787 /// `START WITH … CONNECT BY [PRIOR] col = [PRIOR] col`. Oracle contributes the
2788 /// either-order rule, the after-`WHERE` position, and `NOCYCLE` (which Snowflake's
2789 /// docs explicitly omit) — modelling the Oracle superset is a documented
2790 /// conservative-direction over-acceptance, captured on the owning ticket.
2791 ///
2792 /// **On for Snowflake and Lenient.** Snowflake has no differential oracle, so this is
2793 /// a no-oracle acceptance addition carried by the Snowflake preset and the permissive
2794 /// union. Databricks/Spark does *not* get it: Databricks documents recursive CTEs
2795 /// (`WITH RECURSIVE`) instead of `CONNECT BY` and does not support the Oracle
2796 /// `CONNECT BY … START WITH` syntax (Databricks SQL reference), so it stays off there
2797 /// alongside every oracle-compared dialect (ANSI/PostgreSQL/MySQL/SQLite/DuckDB),
2798 /// which parse-reject a post-`WHERE` `CONNECT BY`/`START WITH` as unconsumed input —
2799 /// so conformance sweeps see zero movement.
2800 pub connect_by_clause: bool,
2801}
2802
2803/// Dialect-owned query-tail syntax accepted by the parser.
2804///
2805/// The clauses parsed by the query-tail sequence that runs after the SELECT body. Split
2806/// out of [`SelectSyntax`] at its 16-field line as the query-tail axis, distinct from the
2807/// SELECT-core and grouping axes. Each flag is a grammar gate: when off the introducing
2808/// keyword is left unconsumed and the clause surfaces as a clean parse error.
2809#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2810pub struct QueryTailSyntax {
2811 /// Accept the standard `OFFSET <n> { ROW | ROWS }` and `FETCH { FIRST | NEXT }
2812 /// <count> { ROW | ROWS } ONLY` row-limiting spelling.
2813 pub fetch_first: bool,
2814 /// Accept the MySQL/MariaDB/SQLite `LIMIT <offset>, <count>` two-argument
2815 /// comma form. It is a *spelling* of the same row limit as
2816 /// `LIMIT <count> OFFSET <offset>`, so it folds into the one canonical
2817 /// [`Limit`](crate::ast::Limit) shape tagged
2818 /// [`LimitSyntax::LimitOffset`](crate::ast::LimitSyntax) — never a
2819 /// new node. The comma binds the offset *first*, the count second (the reverse
2820 /// of `LIMIT <count> OFFSET <offset>`), which is exactly why it must be dialect
2821 /// data: reading the arguments in the wrong order silently swaps them.
2822 pub limit_offset_comma: bool,
2823 /// Accept a trailing row-locking clause (`FOR UPDATE`/`FOR SHARE [OF …]
2824 /// [NOWAIT|SKIP LOCKED]`, plus MySQL's legacy `LOCK IN SHARE MODE`), written after
2825 /// `LIMIT`. PostgreSQL and MySQL share this modern surface, so it folds into one
2826 /// canonical [`LockingClause`](crate::ast::LockingClause) on the query,
2827 /// gated here. When off (ANSI/SQLite/DuckDB, none of which admit a query-tail lock
2828 /// clause), the `FOR`/`LOCK` keyword is left unconsumed and surfaces as a clean
2829 /// parse error — the same reject mechanism the other query-tail gates use. On for
2830 /// PostgreSQL / MySQL / Lenient, off elsewhere. The gate covers the shared modern
2831 /// surface — a single `FOR UPDATE`/`FOR SHARE` clause with an `OF <table>, …` list
2832 /// and a wait tail. PostgreSQL's `NO KEY UPDATE`/`KEY SHARE` strengths and its
2833 /// stacked clauses ride the further [`key_lock_strengths`](QueryTailSyntax::key_lock_strengths)
2834 /// and [`stacked_locking_clauses`](QueryTailSyntax::stacked_locking_clauses) gates; the clause
2835 /// *before* `LIMIT` is still deferred.
2836 pub locking_clauses: bool,
2837 /// Accept PostgreSQL's `FOR NO KEY UPDATE` / `FOR KEY SHARE` row-locking strengths —
2838 /// the two levels between `FOR UPDATE` and `FOR SHARE`
2839 /// ([`LockStrength::NoKeyUpdate`](crate::ast::LockStrength) /
2840 /// [`LockStrength::KeyShare`](crate::ast::LockStrength)). Requires
2841 /// [`locking_clauses`](QueryTailSyntax::locking_clauses) (it refines the strength keyword after
2842 /// `FOR`; the dependency is [`FeatureDependencyViolation::KeyLockStrengthsWithoutLockingClauses`]);
2843 /// when off, the `NO`/`KEY` after `FOR` is never a strength lead, so
2844 /// `FOR NO KEY UPDATE` / `FOR KEY SHARE` surface as clean parse errors — the
2845 /// over-acceptance guard MySQL relies on (its grammar has only `UPDATE`/`SHARE`,
2846 /// engine-verified). PostgreSQL alone spells the `KEY`/`NO KEY` refinements
2847 /// (libpg_query `for_locking_strength`), so this is on for PostgreSQL / Lenient, off
2848 /// elsewhere. `FOR KEY UPDATE` and `FOR NO KEY SHARE` stay rejected under both
2849 /// settings — PostgreSQL pairs `NO KEY` only with `UPDATE` and `KEY` only with
2850 /// `SHARE` (probed on libpg_query, pg-locking-clause-strengths-and-stacking).
2851 pub key_lock_strengths: bool,
2852 /// Accept several *stacked* row-locking clauses on one query
2853 /// (`FOR UPDATE OF a FOR SHARE OF b`) — PostgreSQL applies a distinct lock per table
2854 /// group, so [`Query::locking`](crate::ast::Query) holds a list. Requires
2855 /// [`locking_clauses`](QueryTailSyntax::locking_clauses) (it repeats the shared clause; the
2856 /// dependency is [`FeatureDependencyViolation::StackedLockingClausesWithoutLockingClauses`]);
2857 /// when off, exactly one clause is parsed and any following `FOR`/`LOCK` is left
2858 /// unconsumed, surfacing as a trailing-input parse error — the over-acceptance guard
2859 /// MySQL relies on (its grammar admits exactly one locking clause, engine-verified by
2860 /// `mysql-select-tails-locking-hints-partition`). On for PostgreSQL / Lenient, off
2861 /// elsewhere.
2862 pub stacked_locking_clauses: bool,
2863 /// Accept DuckDB's `USING SAMPLE <entry>` query-level sample clause
2864 /// ([`Select::sample`](crate::ast::Select::sample)), written after `QUALIFY` and before
2865 /// the enclosing query's `ORDER BY` (`SELECT … USING SAMPLE 3 ORDER BY …`). The entry is
2866 /// DuckDB's `tablesample_entry`: a count-first `<size> [ROWS|PERCENT|%] [ '(' method
2867 /// [',' seed] ')' ]` or a method-first `method '(' <size> ')' [REPEATABLE '(' seed ')']`.
2868 /// On for DuckDB / Lenient, off elsewhere; when off the `USING` keyword in that position
2869 /// is left to fail as an unexpected statement token (matching the engines that lack the
2870 /// clause). Distinct from the table-factor `TABLESAMPLE`
2871 /// ([`TableExpressionSyntax::table_sample`](TableExpressionSyntax)).
2872 pub using_sample: bool,
2873 /// Accept a *leading* `OFFSET <count> [LIMIT <count>]` row-skip written without a
2874 /// preceding `LIMIT` (PostgreSQL's `[LIMIT …] [OFFSET …]` where either may come
2875 /// first). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite spells the skip only as
2876 /// `LIMIT <count> OFFSET <count>` / `LIMIT <offset>, <count>` — a bare `OFFSET` with no
2877 /// `LIMIT` is a syntax error there — so it is off; the `OFFSET` keyword is then left
2878 /// unconsumed and surfaces as a clean parse error. The `OFFSET` that trails a `LIMIT`
2879 /// is unaffected (parsed by the `LIMIT` branch).
2880 pub leading_offset: bool,
2881 /// Accept an arbitrary *expression* as a `LIMIT`/`OFFSET` row count. On for
2882 /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient, whose row limits admit a general expression
2883 /// (`LIMIT 1 + 1`, `LIMIT (SELECT n FROM cfg)`). MySQL restricts the count to an
2884 /// unsigned integer literal or a `?` placeholder (its `limit_clause` grammar), so it is
2885 /// off there: an operand that parses to anything else is the syntax error MySQL reports
2886 /// (engine-measured-rejected on mysql:8 — `LIMIT 1 + 1` / `LIMIT (SELECT 1)`). The whole
2887 /// operand is still parsed, then rejected on shape, so the diagnostic points at it. The
2888 /// plain `LIMIT <int>` / `LIMIT ?` / comma / `OFFSET <int>` forms are unaffected.
2889 pub limit_expressions: bool,
2890 /// Accept DuckDB's percentage `LIMIT` — a numeric-literal count directly followed by
2891 /// a `%` operator or the `PERCENT` keyword (`LIMIT 40 PERCENT`, `LIMIT 35%`), which
2892 /// returns that fraction of the result rows rather than a row number. On for
2893 /// DuckDB/Lenient only; off for ANSI/PostgreSQL/MySQL/SQLite, whose `LIMIT` has no
2894 /// percentage form. With the flag off the marker is left unconsumed — a `%` folds as
2895 /// modulo (needing a right operand) and a trailing `PERCENT` is leftover input — so
2896 /// both surface as the parse error those dialects report. DuckDB folds the marker
2897 /// only onto a bare numeric literal at a clause boundary: `LIMIT 10 % 3` stays
2898 /// ordinary modulo, and `LIMIT a PERCENT` / `LIMIT (1 + 1) PERCENT` are DuckDB syntax
2899 /// errors (verified on 1.5.4), so the gate does not over-accept them.
2900 pub limit_percent: bool,
2901 /// Enforce PostgreSQL's `gram.y` validity checks on `FETCH FIRST … WITH TIES` — both
2902 /// semantic guards `insertSelectOptions` raises during raw parsing (so `pg_query`, a
2903 /// parse-only oracle, rejects them):
2904 ///
2905 /// 1. **`ORDER BY` required** — `WITH TIES` needs a governing `ORDER BY` at the same
2906 /// query level (`WITH TIES cannot be specified without ORDER BY clause`).
2907 /// 2. **`SKIP LOCKED` rejected** — `WITH TIES` cannot combine with a `SKIP LOCKED`
2908 /// locking clause (`SKIP LOCKED and WITH TIES options cannot be used together`).
2909 ///
2910 /// On for PostgreSQL only; other dialects with `fetch_first` keep accepting both forms
2911 /// unchanged. When off, neither guard fires. The two rules always co-vary in shipped
2912 /// presets (only Postgres enables this flag), so they share one knob rather than a
2913 /// second independently toggled field — see the module-level naming-exception note.
2914 pub with_ties_requires_order_by: bool,
2915 /// Accept BigQuery/ZetaSQL **query pipe syntax**: a trailing chain of `|>` operators
2916 /// that post-process a query result one step at a time
2917 /// (`FROM t |> WHERE x |> SELECT a`), modelled as
2918 /// [`Query::pipe_operators`](crate::ast::Query::pipe_operators). This gate does double
2919 /// duty — it is read by the *tokenizer* to munch `|>` (pipe-arrow) as a single token
2920 /// (off, the two bytes stay `|` then `>`, so no dialect's lexing shifts) and by the
2921 /// *parser* to admit the trailing `|>`-operator chain. When off (every shipped preset),
2922 /// a `|>` after a query is never a pipe separator and surfaces as a clean parse error,
2923 /// exactly as today.
2924 ///
2925 /// **Off for every shipped preset.** This surface belongs to no engine we oracle
2926 /// against — the BigQuery preset that would home it deliberately defers it (a
2927 /// considered judgment; see that preset's module docs) — so with no differential
2928 /// oracle to verify it, every shipped dialect (including DuckDB, which inherits the
2929 /// PostgreSQL value) leaves it off, and conformance sweeps see zero movement. `LENIENT`
2930 /// leaves it off *for now* as well: although the lenient charter admits any
2931 /// conflict-free pure-acceptance form (and `|>` *is* conflict-free — its munch is
2932 /// feature-gated, shadowing nothing), the framework ships only the reference `WHERE`
2933 /// operator, so enabling it in `LENIENT` today would make the "parse anything" preset
2934 /// accept `|> WHERE` while rejecting every other pipe operator — a fragment a reader of
2935 /// the lenient module could not predict, violating that module's honesty bar. Once the
2936 /// pipe-operator surface is coherent (the `planner-parity-pipe-*` tickets land),
2937 /// `LENIENT` should flip this on as a pure-acceptance addition.
2938 pub pipe_syntax: bool,
2939 /// Accept ClickHouse `LIMIT n [OFFSET m] BY expr, …` — per-group row limiting,
2940 /// written after `ORDER BY` and before the ordinary `LIMIT` tail (both may appear
2941 /// in one query), modelled as [`Query::limit_by`](crate::ast::Query::limit_by). The
2942 /// parser reads a leading `LIMIT` speculatively and treats it as `LIMIT BY` only
2943 /// when a `BY` follows the count and optional `OFFSET`; otherwise it rewinds and the
2944 /// token is the ordinary `LIMIT`, so a plain `LIMIT n` / `LIMIT n OFFSET m` parses
2945 /// identically whether this gate is on or off.
2946 ///
2947 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle,
2948 /// so this is a no-oracle acceptance addition carried by the ClickHouse preset and the
2949 /// permissive union (the `apply_join` precedent): every oracle-compared dialect
2950 /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a `BY`
2951 /// after `LIMIT` — so conformance sweeps see zero movement.
2952 pub limit_by_clause: bool,
2953 /// Accept ClickHouse `SETTINGS name = value, …` — query-level setting overrides
2954 /// written after the ordinary `LIMIT` tail, modelled as
2955 /// [`Query::settings`](crate::ast::Query::settings). `SETTINGS` is matched as a
2956 /// contextual keyword (it stays an ordinary identifier elsewhere); each pair is an
2957 /// identifier `=` value, the value a general expression.
2958 ///
2959 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle,
2960 /// so this is a no-oracle acceptance addition carried by the ClickHouse preset and the
2961 /// permissive union (the `limit_by_clause` precedent): every oracle-compared dialect
2962 /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a trailing
2963 /// `SETTINGS …` as unconsumed input — so conformance sweeps see zero movement.
2964 pub settings_clause: bool,
2965 /// Accept ClickHouse `FORMAT <name>` — the output-format clause that closes the
2966 /// query, modelled as [`Query::format`](crate::ast::Query::format). `FORMAT` is
2967 /// matched as a contextual keyword (it stays an ordinary identifier elsewhere); the
2968 /// format name is a bare, case-sensitive identifier (`JSON`, `TabSeparated`, `Null`),
2969 /// not a string literal.
2970 ///
2971 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle,
2972 /// so this is a no-oracle acceptance addition carried by the ClickHouse preset and the
2973 /// permissive union (the `settings_clause` precedent): every oracle-compared dialect
2974 /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a trailing
2975 /// `FORMAT …` as unconsumed input — so conformance sweeps see zero movement.
2976 pub format_clause: bool,
2977 /// Accept MSSQL's `FOR XML {RAW|AUTO|EXPLICIT|PATH} [, …]` and
2978 /// `FOR JSON {AUTO|PATH} [, …]` result-shaping tails, which serialize the result
2979 /// set as XML/JSON rather than a rowset, modelled as
2980 /// [`Query::for_clause`](crate::ast::Query::for_clause) (see [`ForClause`](crate::ast::ForClause)).
2981 /// The modes and their directives (`ELEMENTS [XSINIL|ABSENT]`, `BINARY BASE64`,
2982 /// `TYPE`, `ROOT ['name']`, `INCLUDE_NULL_VALUES`, `WITHOUT_ARRAY_WRAPPER`) are typed
2983 /// data; the accepted grammar follows the MSSQL `FOR XML` / `FOR JSON`
2984 /// documentation (no MSSQL oracle, so that is the recorded acceptance bound).
2985 ///
2986 /// **Leading-keyword dispatch, follow-token partitioned against locking.** `FOR`
2987 /// also introduces the row-locking clauses ([`locking_clauses`](QueryTailSyntax::locking_clauses)).
2988 /// The two share the `FOR` lead but split on the *follow* token — `XML`/`JSON` here
2989 /// versus `UPDATE`/`SHARE`/`NO`/`KEY` for locking — so the dispatch is unambiguous
2990 /// under every preset combination and needs no
2991 /// [`GrammarConflict`] entry: the locking parser
2992 /// declines a `FOR` whose follow token is `XML`/`JSON` (so a stacked
2993 /// `FOR UPDATE … FOR XML` still reaches this clause), and this clause declines a
2994 /// `FOR` whose follow token is anything else.
2995 ///
2996 /// **On for MSSQL and Lenient.** MSSQL has no differential oracle, so this is a
2997 /// no-oracle acceptance addition carried by the MSSQL preset and the permissive
2998 /// union: every oracle-compared dialect (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves
2999 /// it off — they parse-reject a trailing `FOR XML`/`FOR JSON` as unconsumed input
3000 /// (or, where `locking_clauses` is on, reject `XML`/`JSON` after `FOR`) — so
3001 /// conformance sweeps see zero movement.
3002 pub for_xml_json_clause: bool,
3003}
3004
3005/// Dialect-owned `GROUP BY` / `ORDER BY` grouping-and-ordering syntax accepted by the
3006/// parser.
3007///
3008/// The grouping-set constructs and the clause-level grouping/ordering modes and
3009/// quantifiers. Split out of [`SelectSyntax`] at its 16-field line as the
3010/// grouping/ordering axis, distinct from the SELECT-core and query-tail axes. Each
3011/// flag is a grammar gate: when off the keyword falls through to the ordinary
3012/// expression/grouping-item grammar or surfaces as a clean parse error.
3013#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3014pub struct GroupingSyntax {
3015 /// Accept the SQL:1999 (feature T431) grouping-set constructs as `GROUP BY`
3016 /// items: `ROLLUP (…)`, `CUBE (…)`, `GROUPING SETS (…)`, and the empty grouping
3017 /// set `()`. PostgreSQL lowers these in GROUP BY item position for *any* case
3018 /// spelling — an unquoted `rollup (a, b)` is the grouping construct, never a
3019 /// call to a user function `rollup` (quote it, `"rollup"(a, b)`, to call the
3020 /// function) — so the parser models them as [`GroupByItem`](crate::ast::GroupByItem)
3021 /// nodes, not [`FunctionCall`](crate::ast::FunctionCall) expressions. When off,
3022 /// the keywords fall through to the expression grammar (an ordinary function
3023 /// call), which is how MySQL reads them; MySQL's own grouping surface is the
3024 /// distinct trailing `WITH ROLLUP`, not modelled here. On for ANSI (the T431
3025 /// standard) / PostgreSQL / Lenient, off for MySQL.
3026 pub grouping_sets: bool,
3027 /// Accept MySQL's trailing `GROUP BY <keys> WITH ROLLUP` modifier — MySQL's only
3028 /// grouping-set surface (it has no SQL:1999 `ROLLUP (…)` item form). It is a
3029 /// *spelling* of the same super-aggregate as `ROLLUP (…)`, so it canonicalizes
3030 /// into the one [`GroupByItem::Rollup`](crate::ast::GroupByItem) shape tagged
3031 /// [`RollupSpelling::WithRollup`](crate::ast::RollupSpelling) — never a
3032 /// new node. When off, the trailing `WITH ROLLUP` is left unconsumed and surfaces
3033 /// as a clean parse error; PostgreSQL/ANSI spell the construct `ROLLUP (…)`, so
3034 /// accepting `WITH ROLLUP` there would be an over-acceptance. On for MySQL /
3035 /// Lenient, off elsewhere. (MySQL 8.0.1+ also permits `WITH ROLLUP` alongside
3036 /// `ORDER BY`; older versions did not — a version wrinkle we do not model.)
3037 pub with_rollup: bool,
3038 /// Accept PostgreSQL's `ORDER BY <expr> USING <operator>` sort form (`gram.y`
3039 /// `sortby: a_expr USING qual_all_Op opt_nulls_order`), which sorts by a named
3040 /// ordering operator (`USING <`, `USING OPERATOR(schema.op)`) instead of
3041 /// `ASC`/`DESC`. PostgreSQL-only; ANSI and MySQL have only `ASC`/`DESC`, so there
3042 /// the `USING` keyword is left unconsumed and surfaces as a trailing-input parse
3043 /// error — the same reject mechanism the other unsupported clauses use.
3044 pub order_by_using: bool,
3045 /// Accept DuckDB's `GROUP BY ALL` clause mode: group by every non-aggregated
3046 /// projection column, resolved at bind time
3047 /// ([`Select::group_by_all`](crate::ast::Select) — a mode with an empty key
3048 /// list, never a [`GroupByItem`](crate::ast::GroupByItem)). `ALL` cannot mix
3049 /// with explicit keys or grouping sets (DuckDB syntax-errors on `GROUP BY ALL,
3050 /// x` and `GROUP BY ROLLUP(x), ALL`; probed on 1.5.4), so the branch consumes
3051 /// exactly the one keyword. When off, `ALL` after `GROUP BY` falls through to
3052 /// the expression grammar, where every shipped dialect reserves it — a clean
3053 /// parse error. A *separate* flag from [`order_by_all`](GroupingSyntax::order_by_all),
3054 /// not one paired gate: the paired-flag doctrine (the `straight_join` /
3055 /// `table_options` precedent) covers grammar points a dialect only ever ships
3056 /// together, but these are two independent constructs on two clauses that real
3057 /// engines adopt separately (Snowflake ships `GROUP BY ALL` with no
3058 /// `ORDER BY ALL`), so pairing them would bake a DuckDB coincidence into the
3059 /// vocabulary. On for DuckDB / Lenient, off elsewhere.
3060 pub group_by_all: bool,
3061 /// Accept PostgreSQL's `GROUP BY {DISTINCT | ALL} <grouping items>` set-quantifier
3062 /// (SQL:2016 feature T434): a `DISTINCT`/`ALL` prefix on the whole grouping clause
3063 /// that governs deduplication of the generated grouping sets
3064 /// ([`Select::group_by_quantifier`](crate::ast::Select)). The quantifier requires a
3065 /// non-empty grouping list — PostgreSQL rejects a bare `GROUP BY ALL` /
3066 /// `GROUP BY DISTINCT` (probed on pg_query PG-17) — which is what keeps it MECE with
3067 /// [`group_by_all`](GroupingSyntax::group_by_all), DuckDB's mode where `ALL` *is* the entire
3068 /// clause. A *separate* flag from `group_by_all` for that reason: the two constructs
3069 /// spell an overlapping keyword (`ALL`) but mean opposite things (a modifier on a
3070 /// list vs. a standalone mode), and no shipped dialect but Lenient enables both.
3071 /// Under Lenient (both on) the forms stay disambiguated by lookahead — a bare
3072 /// `GROUP BY ALL` is the DuckDB mode, `GROUP BY ALL <items>` is the PostgreSQL
3073 /// quantifier — so the widening is conflict-free. When off, the `DISTINCT`/`ALL`
3074 /// keyword after `GROUP BY` falls through to the grouping-item grammar, where every
3075 /// shipped dialect reserves it — a clean parse error. On for PostgreSQL / Lenient,
3076 /// off elsewhere.
3077 pub group_by_set_quantifier: bool,
3078 /// Accept DuckDB's `ORDER BY ALL [ASC | DESC] [NULLS FIRST | LAST]` clause
3079 /// mode: sort by every projection column, left to right
3080 /// ([`Query::order_by_all`](crate::ast::Query) — a mode of the whole clause,
3081 /// never a sort-key expression). `ALL` cannot mix with explicit keys
3082 /// (`ORDER BY ALL, x` / `ORDER BY x, ALL` syntax-error; probed on 1.5.4) and
3083 /// takes no `USING` tail. The gate covers only the *query-level* clause, the
3084 /// one position DuckDB gives mode semantics: window `ORDER BY ALL` is a
3085 /// dedicated DuckDB parse error ("Cannot ORDER BY ALL in a window
3086 /// expression"), and the aggregate-internal `agg(x ORDER BY ALL)` form —
3087 /// which DuckDB reads as a `COLUMNS(*)` star expansion producing one output
3088 /// per column — is a different grammar position: a sort *key*
3089 /// ([`Expr::Columns`](crate::ast::Expr::Columns)), gated with the node's own
3090 /// [`CallSyntax::columns_expression`](CallSyntax::columns_expression).
3091 /// When off, `ALL` after `ORDER BY` falls through to
3092 /// the expression grammar, where every shipped dialect reserves it — a clean
3093 /// parse error. On for DuckDB / Lenient, off elsewhere; see
3094 /// [`group_by_all`](GroupingSyntax::group_by_all) for why the two are separate flags.
3095 pub order_by_all: bool,
3096}
3097
3098/// Dialect-owned transaction-control (TCL) syntax accepted by the parser.
3099///
3100/// Transaction openers, completers, savepoints, mode lists, and XA distributed-transaction
3101/// forms — the SQL transaction-control family. Split out of [`UtilitySyntax`] so utility
3102/// statement-head gates (COPY/PRAGMA/SHOW siblings) are not mixed with TCL grammar and
3103/// narrowing validators. Statement-head flags that open TCL statements
3104/// (`start_transaction`, `set_transaction`, `xa_transactions`, …) are still consumed by
3105/// `parser::statement_dispatch` / `parser::tcl`; mode-list validators are read by the TCL
3106/// body parser.
3107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3108pub struct TransactionSyntax {
3109 /// Accept the standard `START TRANSACTION` transaction opener. Some engines,
3110 /// notably SQLite, expose only `BEGIN` and reject the `START` spelling.
3111 pub start_transaction: bool,
3112 /// Accept `START [TRANSACTION | WORK]` with the transaction block word omitted or
3113 /// spelled `WORK`. When off, the standard `START TRANSACTION` spelling remains
3114 /// mandatory. DuckDB accepts all three spellings; PostgreSQL requires
3115 /// `TRANSACTION` after `START`.
3116 pub start_transaction_block_optional: bool,
3117 /// Accept `WORK` as the optional transaction block word on `BEGIN`, `COMMIT`, and
3118 /// `ROLLBACK`. `START WORK` is controlled by
3119 /// [`start_transaction_block_optional`](Self::start_transaction_block_optional).
3120 pub transaction_work_keyword: bool,
3121 /// Accept `TRANSACTION` as the optional block word after `BEGIN`.
3122 pub begin_transaction_keyword: bool,
3123 /// Accept `TRANSACTION` as the optional block word after `COMMIT`.
3124 pub commit_transaction_keyword: bool,
3125 /// Accept `TRANSACTION` as the optional block word after `ROLLBACK`.
3126 pub rollback_transaction_keyword: bool,
3127 /// Accept SQLite's optional transaction name immediately after an explicit
3128 /// `TRANSACTION` block word (`BEGIN TRANSACTION name`, `COMMIT TRANSACTION name`,
3129 /// or `ROLLBACK TRANSACTION name`).
3130 pub transaction_name: bool,
3131 /// Accept a standard transaction-mode list after `BEGIN [WORK | TRANSACTION]`.
3132 pub begin_transaction_modes: bool,
3133 /// Accept transaction savepoint statements: `SAVEPOINT`, `RELEASE`, and
3134 /// `ROLLBACK TO`. This controls the complete savepoint statement family.
3135 pub transaction_savepoints: bool,
3136 /// Accept `SET TRANSACTION <mode> [, ...]`.
3137 pub set_transaction: bool,
3138 /// Accept `ISOLATION LEVEL <level>` in a transaction mode list.
3139 pub transaction_isolation_mode: bool,
3140 /// Accept `READ ONLY` / `READ WRITE` in a transaction mode list.
3141 pub transaction_access_mode: bool,
3142 /// Accept `DEFERRABLE` / `NOT DEFERRABLE` in a transaction mode list.
3143 pub transaction_deferrable_mode: bool,
3144 /// Accept `ISOLATION LEVEL <level>` after `START TRANSACTION`. When off, the
3145 /// isolation form can remain available to `SET TRANSACTION`.
3146 pub start_transaction_isolation_mode: bool,
3147 /// Accept `[NOT] DEFERRABLE` after `START TRANSACTION`. When off, the form can
3148 /// remain available to `SET TRANSACTION`.
3149 pub start_transaction_deferrable_mode: bool,
3150 /// Accept MySQL's `WITH CONSISTENT SNAPSHOT` `START TRANSACTION` characteristic.
3151 pub start_transaction_consistent_snapshot: bool,
3152 /// Accept more than one transaction mode, separated by an optional comma.
3153 pub transaction_multiple_modes: bool,
3154 /// ON rejects a missing comma between adjacent transaction modes (MySQL).
3155 /// PostgreSQL permits bare whitespace between modes when this is off.
3156 pub transaction_modes_require_commas: bool,
3157 /// ON rejects a repeated transaction-mode kind in one list (MySQL: each
3158 /// characteristic category at most once).
3159 pub transaction_modes_reject_duplicates: bool,
3160 /// Accept `ABORT` as an exact `ROLLBACK` synonym.
3161 pub abort_transaction_alias: bool,
3162 /// Accept `END` as an exact `COMMIT` synonym.
3163 pub end_transaction_alias: bool,
3164 /// Accept MySQL's `COMMIT|ROLLBACK [NO] RELEASE` completion modifier.
3165 pub transaction_release: bool,
3166 /// Accept `COMMIT AND [NO] CHAIN` and whole-transaction `ROLLBACK AND [NO] CHAIN`.
3167 /// PostgreSQL and SQL-standard transaction chaining; off where the engine grammar lacks it.
3168 pub transaction_chain: bool,
3169 /// Accept `RELEASE <name>` without the `SAVEPOINT` keyword. When off,
3170 /// `RELEASE SAVEPOINT <name>` remains available with transaction savepoints.
3171 pub release_savepoint_keyword_optional: bool,
3172 /// Accept SQLite's `{DEFERRED | IMMEDIATE | EXCLUSIVE}` transaction-mode modifier
3173 /// between `BEGIN` and the optional `TRANSACTION` keyword (stored on
3174 /// [`TransactionStatement::Begin`](crate::ast::TransactionStatement::Begin)'s `mode`
3175 /// field). On for SQLite/Lenient; off elsewhere. PostgreSQL's `BEGIN` takes its own,
3176 /// differently-shaped modifier set (`ISOLATION LEVEL …` / `READ ONLY|WRITE` / `[NOT]
3177 /// DEFERRABLE`, the existing [`TransactionMode`](crate::ast::TransactionMode) list),
3178 /// deliberately not modelled here, so it stays off there and the leading modifier
3179 /// keyword falls through to today's error (engine-probed: `pg_query` rejects `BEGIN
3180 /// DEFERRED`/`BEGIN IMMEDIATE`/`BEGIN EXCLUSIVE`).
3181 pub begin_transaction_mode: bool,
3182 /// Accept the MySQL `XA` distributed-transaction family — `XA {START | BEGIN} xid [JOIN |
3183 /// RESUME]`, `XA END xid [SUSPEND [FOR MIGRATE]]`, `XA PREPARE xid`, `XA COMMIT xid [ONE
3184 /// PHASE]`, `XA ROLLBACK xid`, and `XA RECOVER [CONVERT XID]` (the X/Open two-phase-commit
3185 /// verbs; `sql_yacc.yy` `xa:`). One flag gates the whole family because it is a single
3186 /// dialect unit reached through one unique leading `XA` keyword (the
3187 /// [`kill`](UtilitySyntax::kill) leading-keyword-gate precedent, not a keyword shared with
3188 /// another dialect). On for MySQL and the Lenient superset (a pure addition there — no other
3189 /// dialect claims `XA`); off elsewhere, where the leading `XA` keyword is not dispatched and
3190 /// surfaces as an unknown statement. Live mysql:8.4.10: every grammar-valid form answers
3191 /// `ER_UNSUPPORTED_PS` 1295 (recognized, not preparable over the wire). See
3192 /// [`XaStatement`](crate::ast::XaStatement).
3193 pub xa_transactions: bool,
3194}
3195
3196/// Dialect-owned utility-statement syntax extensions accepted by the parser.
3197///
3198/// Non-TCL utility statements whose leading keyword a dialect dispatches — COPY, PRAGMA,
3199/// ATTACH, prepared statements, locks, replication, … — the statement-keyword analogue of
3200/// [`MutationSyntax::merge`]. Transaction-control grammar lives in [`TransactionSyntax`].
3201/// Whether each statement is dispatched is explicit dialect data: when a flag is off the
3202/// keyword is left undispatched and surfaces as an unknown statement.
3203/// **Statement-head flags on this axis are consumed by the parser's statement-head router**
3204/// (`parser::statement_dispatch`); parse bodies live in family modules (`util`, …).
3205/// `EXPLAIN` is not gated here (accepted dialect-agnostically for now). Introspection,
3206/// physical-maintenance, and access-control statements live in [`ShowSyntax`],
3207/// [`MaintenanceSyntax`], and [`AccessControlSyntax`].
3208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3209pub struct UtilitySyntax {
3210 /// Accept the PostgreSQL `COPY <table>|(<query>) {FROM|TO} <endpoint> ...` bulk
3211 /// data-transfer statement. PostgreSQL-only (also its generic/permissive supersets);
3212 /// off in ANSI and MySQL, which have no `COPY`, so there the leading `COPY` keyword
3213 /// is not dispatched and surfaces as an unknown statement.
3214 pub copy: bool,
3215 /// Accept the Snowflake `COPY INTO <target> FROM <source> [<opt> = <val> ...]` bulk
3216 /// load/unload statement — a distinct grammar from the PostgreSQL `COPY` gated by
3217 /// [`copy`](Self::copy): fixed `INTO … FROM …` direction, `KEY = VALUE` options with a
3218 /// nested `FILE_FORMAT = (…)` list, and the `FILES`/`PATTERN`/`VALIDATION_MODE` clauses.
3219 /// The two share only the leading `COPY` keyword; the dispatcher branches on the `INTO`
3220 /// that follows. On for Snowflake and Lenient; off elsewhere (including PostgreSQL and
3221 /// DuckDB, whose `COPY` is the `copy`-gated `{FROM | TO}` transfer), where a `COPY INTO`
3222 /// surfaces as an unknown statement.
3223 pub copy_into: bool,
3224 /// Accept Snowflake stage references `@stage` / `@~` / `@%table` (with optional
3225 /// `/path` segments) as a dedicated token and as a `COPY INTO` endpoint.
3226 /// On for Snowflake and Lenient; off elsewhere so `@` keeps its other claimants.
3227 pub stage_references: bool,
3228 /// Accept the PostgreSQL `COMMENT ON <object> IS '<text>' | NULL` object-metadata
3229 /// statement. PostgreSQL-only (and its permissive superset); off in ANSI and MySQL,
3230 /// which have no `COMMENT ON`, so there the leading `COMMENT` keyword is not
3231 /// dispatched and surfaces as an unknown statement.
3232 pub comment_on: bool,
3233 /// Accept the front-position `COMMENT IF EXISTS ON ...` guard.
3234 pub comment_if_exists: bool,
3235 /// Accept the SQLite `PRAGMA [<schema> .] <name> [= <value> | (<value>)]`
3236 /// configuration statement. SQLite-only (and the permissive superset); off in
3237 /// ANSI, PostgreSQL, and MySQL, which have no `PRAGMA`, so there the leading
3238 /// `PRAGMA` keyword is not dispatched and surfaces as an unknown statement.
3239 pub pragma: bool,
3240 /// Accept the SQLite `ATTACH [DATABASE] <expr> AS <schema>` statement *and* its
3241 /// `DETACH [DATABASE] <schema>` inverse. One flag gates both leading keywords
3242 /// because they are a single dialect unit — a dialect with `ATTACH` has `DETACH`
3243 /// (mirroring how `existence_guards.if_exists` gates its paired grammar
3244 /// points together). SQLite-only (and the permissive superset); off in ANSI,
3245 /// PostgreSQL, and MySQL, so there the leading keywords are not dispatched and
3246 /// surface as unknown statements.
3247 pub attach: bool,
3248 /// Accept the MySQL `KILL [CONNECTION | QUERY] <id>` thread/query-termination
3249 /// statement. MySQL-only (and the permissive superset); off in ANSI, PostgreSQL, and
3250 /// SQLite, which have no `KILL`, so there the leading keyword is not dispatched and
3251 /// surfaces as an unknown statement — the `copy`/`comment_on` leading-keyword-gate
3252 /// precedent.
3253 pub kill: bool,
3254 /// Accept the MySQL `HANDLER` low-level cursor family — `HANDLER <t> OPEN [[AS] alias]`,
3255 /// `HANDLER <t> READ …`, and `HANDLER <t> CLOSE` — direct index-level storage-engine
3256 /// access that bypasses the optimizer. One flag gates the leading `HANDLER` keyword (all
3257 /// three verbs follow the opened table) — the `copy`/`kill` leading-keyword-gate
3258 /// precedent. MySQL-only among the shipped presets, and a pure addition in the permissive
3259 /// superset (the leading `HANDLER` collides with no other statement); off in ANSI,
3260 /// PostgreSQL, and SQLite, which have no `HANDLER`, so there it is not dispatched and
3261 /// surfaces as an unknown statement. See
3262 /// [`HandlerStatement`](crate::ast::HandlerStatement).
3263 pub handler_statements: bool,
3264 /// Accept the MySQL plugin/component install-management family — `INSTALL PLUGIN <name>
3265 /// SONAME <lib>`, `INSTALL COMPONENT <urn> … [SET …]`, `UNINSTALL PLUGIN <name>`, and
3266 /// `UNINSTALL COMPONENT <urn> …`. One flag gates the leading `INSTALL` and `UNINSTALL`
3267 /// keywords together, an install/uninstall pair kept as one dialect unit like
3268 /// `attach`/`detach`. MySQL-only among the shipped presets, and a pure addition in the
3269 /// permissive superset (the leading `INSTALL`/`UNINSTALL` collide with no other statement);
3270 /// off in ANSI, PostgreSQL, and SQLite, which have neither, so there they are not dispatched
3271 /// and surface as an unknown statement. See
3272 /// [`InstallStatement`](crate::ast::InstallStatement) and
3273 /// [`UninstallStatement`](crate::ast::UninstallStatement).
3274 pub plugin_component_statements: bool,
3275 /// Accept the MySQL `SHUTDOWN` server-shutdown statement — a nullary leading keyword. A
3276 /// leading-keyword gate like `kill`; MySQL-only among the shipped presets and a pure
3277 /// addition in the permissive superset (the leading `SHUTDOWN` collides with no other
3278 /// statement); off in ANSI, PostgreSQL, and SQLite, where it is not dispatched and surfaces
3279 /// as an unknown statement. Separate from [`restart`](UtilitySyntax::restart): `SHUTDOWN`
3280 /// and `RESTART` are distinct statements, not an inverse pair, so each takes its own flag
3281 /// (the `vacuum`/`reindex` precedent). See [`Statement::Shutdown`](crate::ast::Statement).
3282 pub shutdown: bool,
3283 /// Accept the MySQL `RESTART` server-restart statement — a nullary leading keyword. A
3284 /// leading-keyword gate like `kill`; MySQL-only among the shipped presets and a pure
3285 /// addition in the permissive superset; off in ANSI, PostgreSQL, and SQLite, where it
3286 /// surfaces as an unknown statement. Separate from [`shutdown`](UtilitySyntax::shutdown) —
3287 /// distinct behaviours, distinct flags. See [`Statement::Restart`](crate::ast::Statement).
3288 pub restart: bool,
3289 /// Accept the MySQL `CLONE` data-directory provisioning statement — `CLONE LOCAL DATA
3290 /// DIRECTORY [=] '<dir>'` and `CLONE INSTANCE FROM <user>[@<host>]:<port> IDENTIFIED BY
3291 /// '<pw>' [DATA DIRECTORY [=] '<dir>'] [REQUIRE [NO] SSL]`. One flag gates the leading
3292 /// `CLONE` keyword (both `LOCAL`/`INSTANCE` forms follow it — one statement, two forms, like
3293 /// `flush`); MySQL-only and a pure addition in the permissive superset; off in ANSI,
3294 /// PostgreSQL, and SQLite. See [`CloneStatement`](crate::ast::CloneStatement).
3295 pub clone: bool,
3296 /// Accept the MySQL `IMPORT TABLE FROM '<file>' [, …]` tablespace-import statement. A
3297 /// leading-keyword gate on `IMPORT` distinguished from DuckDB's `IMPORT DATABASE`
3298 /// ([`export_import_database`](UtilitySyntax::export_import_database)) by the second keyword
3299 /// (`TABLE` vs `DATABASE`): both may be on in the permissive superset without colliding.
3300 /// MySQL-only among the shipped presets; off in ANSI, PostgreSQL, SQLite, and DuckDB. See
3301 /// [`ImportTableStatement`](crate::ast::ImportTableStatement).
3302 pub import_table: bool,
3303 /// Accept the MySQL `HELP '<topic>'` help-lookup statement — a leading-keyword gate like
3304 /// `kill`. MySQL-only among the shipped presets and a pure addition in the permissive
3305 /// superset (the leading `HELP` collides with no other statement); off in ANSI, PostgreSQL,
3306 /// and SQLite. See [`HelpStatement`](crate::ast::HelpStatement).
3307 pub help_statement: bool,
3308 /// Accept the MySQL `BINLOG '<base64-event>'` binary-log-event replay statement — a
3309 /// leading-keyword gate like `kill`. MySQL-only among the shipped presets and a pure
3310 /// addition in the permissive superset; off in ANSI, PostgreSQL, and SQLite. See
3311 /// [`BinlogStatement`](crate::ast::BinlogStatement).
3312 pub binlog: bool,
3313 /// Accept the MySQL MyISAM key-cache statement pair — `CACHE INDEX <t> [<keys>][, ...]
3314 /// [PARTITION (...)] IN <cache>` and `LOAD INDEX INTO CACHE <t> [PARTITION (...)] [<keys>]
3315 /// [IGNORE LEAVES][, ...]`. One flag gates both leading keywords (`CACHE` and the
3316 /// `LOAD INDEX` lookahead) because they are a single dialect unit — key-cache assignment
3317 /// and preload travel together (the `attach`/`detach`, `prepared_statements` single-flag
3318 /// precedent). MySQL-only among the shipped presets, and a pure addition in the permissive
3319 /// superset; off in ANSI, PostgreSQL, and SQLite, where the leading keywords are not
3320 /// dispatched (a leading `LOAD` without `INDEX` still reaches the `load_extension` gate).
3321 /// See [`CacheIndexStatement`](crate::ast::CacheIndexStatement) and
3322 /// [`LoadIndexStatement`](crate::ast::LoadIndexStatement).
3323 pub key_cache_statements: bool,
3324 /// Accept the `USE` catalog/schema-switch statement — DuckDB `USE <catalog> [. <schema>]`
3325 /// and MySQL `USE <schema>`. A leading-keyword gate like `pragma`: on for DuckDB, MySQL,
3326 /// and the permissive superset; off in ANSI, PostgreSQL, and SQLite, which have no `USE`
3327 /// statement, so there the leading `USE` keyword is not dispatched and surfaces as an
3328 /// unknown statement. (A *non-leading* `USE` is the MySQL index-hint keyword, consumed by
3329 /// the `FROM` grammar, so it never reaches this statement-leading position.) The accepted
3330 /// name arity rides [`use_qualified_name`](UtilitySyntax::use_qualified_name).
3331 pub use_statement: bool,
3332 /// Accept a dotted `USE <catalog> . <schema>` name (DuckDB) rather than the single
3333 /// unqualified `USE <schema>` (MySQL). Refines the name grammar of the base `USE`
3334 /// statement, so it requires [`use_statement`](UtilitySyntax::use_statement): without it
3335 /// the leading `USE` is not dispatched and this flag is inert, the dependency the
3336 /// [`UseQualifiedNameWithoutUseStatement`](crate::dialect::FeatureDependencyViolation::UseQualifiedNameWithoutUseStatement)
3337 /// registry variant records. On for DuckDB and the permissive superset; off for MySQL,
3338 /// whose `USE ident` grammar `ER_PARSE_ERROR`s any dotted name (engine-measured on
3339 /// mysql:8), and off (vacuously) wherever `use_statement` is off. DuckDB still rejects a
3340 /// three-part `USE a.b.c` even with this on — that arity bound is enforced in the parser.
3341 pub use_qualified_name: bool,
3342 /// Accept a string-constant (`Sconst`) spelling of the `USE` target name — DuckDB's
3343 /// `USE 'n'` / `USE E'n'` / `USE $$n$$` single-part form (engine-measured on
3344 /// libduckdb 1.5.4 via `duckdb_extract_statements`). The string is a *single-part*
3345 /// name only: a dotted string name (`USE 'a'.'b'`, `USE a.'b'`) is a parser reject,
3346 /// matching DuckDB. Refines the name grammar of the base `USE` statement, so it
3347 /// requires [`use_statement`](UtilitySyntax::use_statement): without it the leading
3348 /// `USE` is not dispatched and this flag is inert, the dependency the
3349 /// [`UseStringLiteralNameWithoutUseStatement`](crate::dialect::FeatureDependencyViolation::UseStringLiteralNameWithoutUseStatement)
3350 /// registry variant records. On for DuckDB and the permissive superset; off for MySQL,
3351 /// whose `USE ident` grammar `ER_PARSE_ERROR`s a string name (engine-measured on
3352 /// mysql:8), and off (vacuously) wherever `use_statement` is off.
3353 pub use_string_literal_name: bool,
3354 /// Accept the prepared-statement lifecycle: `PREPARE <name> [(<types>)] AS
3355 /// <statement>`, `EXECUTE <name> [(<args>)]`, and `DEALLOCATE [PREPARE] <name>`. One
3356 /// flag gates the three leading keywords because they are a single dialect unit — a
3357 /// dialect that prepares statements executes and frees them too (the
3358 /// `attach`/`detach` single-flag precedent). On for DuckDB, PostgreSQL, and Lenient;
3359 /// off elsewhere, where the leading keywords surface as unknown statements.
3360 /// (`EXECUTE` is only a *leading* keyword here; the `EXECUTE` privilege in `GRANT
3361 /// EXECUTE` is unaffected.) The parenthesized `PREPARE` type list is a separate,
3362 /// differently-shaped surface gated by
3363 /// [`prepare_typed_parameters`](UtilitySyntax::prepare_typed_parameters): this flag alone only
3364 /// admits the bare `PREPARE <name> AS <statement>` form. Never both on with MySQL's
3365 /// [`prepared_statements_from`](UtilitySyntax::prepared_statements_from) — the pair is the
3366 /// registered [`GrammarConflict::PreparedStatementsVersusPreparedStatementsFrom`].
3367 pub prepared_statements: bool,
3368 /// Accept the PostgreSQL `PREPARE name ( <type> [, ...] ) AS <statement>`
3369 /// parenthesized parameter-type list between the name and `AS` — full type names
3370 /// (parameterized like `numeric(10,2)`, arrayed like `int[]`), at least one,
3371 /// PostgreSQL rejects an empty `()`. Depends on
3372 /// [`prepared_statements`](UtilitySyntax::prepared_statements) being on too (the type list is
3373 /// a widening of that grammar's name position, not a standalone surface); the
3374 /// dependency is
3375 /// [`FeatureDependencyViolation::PrepareTypedParametersWithoutPreparedStatements`].
3376 /// On for PostgreSQL and Lenient; off for DuckDB, which structurally rejects the
3377 /// whole typed-parameter-list form ("Prepared statement argument types are not
3378 /// supported, use CAST") — so it keeps `prepared_statements` for the bare form only.
3379 pub prepare_typed_parameters: bool,
3380 /// Accept MySQL's prepared-statement lifecycle: `PREPARE <name> FROM {'<text>' | @<var>}`,
3381 /// `EXECUTE <name> [USING @<var>, ...]`, and `{DEALLOCATE | DROP} PREPARE <name>`. One
3382 /// flag gates the three leading keywords (plus the `DROP PREPARE` synonym) because they
3383 /// are a single dialect unit — the `prepared_statements` single-flag precedent.
3384 ///
3385 /// A *different grammar on the same three keywords* from DuckDB's typed-`AS`
3386 /// [`prepared_statements`](UtilitySyntax::prepared_statements): MySQL prepares from a
3387 /// statement *source* (a string literal or a `@`-variable, never an inline-parsed
3388 /// statement), executes with a `USING` clause of `@`-variable references (never
3389 /// parenthesized expressions), and requires the `PREPARE` keyword after `{DEALLOCATE |
3390 /// DROP}`. The two are mutually exclusive per preset (each arms at most one), the split
3391 /// mirroring [`do_statement`](UtilitySyntax::do_statement) vs
3392 /// [`do_expression_list`](UtilitySyntax::do_expression_list) on the `DO` keyword; a preset
3393 /// never arms both, so the shared leading keywords dispatch unambiguously. MySQL-only among
3394 /// the shipped presets; off elsewhere (the permissive superset keeps the DuckDB typed-`AS`
3395 /// reading, since the `FROM`/`USING` surfaces have no positional-argument spelling). Never
3396 /// both on with [`prepared_statements`](UtilitySyntax::prepared_statements) — the pair is the
3397 /// registered [`GrammarConflict::PreparedStatementsVersusPreparedStatementsFrom`], whose
3398 /// `DEALLOCATE` tail (mandatory `PREPARE` under this flag) is incoherent with the DuckDB-first
3399 /// dispatch of the `PREPARE`/`EXECUTE` heads. See
3400 /// [`PrepareFromStatement`](crate::ast::PrepareFromStatement) and
3401 /// [`ExecuteUsingStatement`](crate::ast::ExecuteUsingStatement).
3402 pub prepared_statements_from: bool,
3403 /// Accept the DuckDB `CALL <name>(<args>)` routine/table-function invocation
3404 /// statement. Its own flag rather than sharing
3405 /// [`prepared_statements`](UtilitySyntax::prepared_statements): `CALL` is an independent
3406 /// procedure-call statement, not part of the prepare/execute/deallocate lifecycle
3407 /// (the `vacuum`/`reindex`/`analyze` separate-flags precedent). DuckDB-only among the
3408 /// shipped fitted presets (and the permissive superset); off elsewhere, where a
3409 /// leading `CALL` surfaces as an unknown statement.
3410 pub call: bool,
3411 /// Accept MySQL's bare `CALL <name>` form — a routine invocation with no parenthesized
3412 /// argument list at all (`CALL_SYM sp_name opt_paren_expr_list`, the `opt_paren_expr_list`
3413 /// %empty alternative) — on top of the base [`call`](UtilitySyntax::call) statement (the
3414 /// dependency is [`FeatureDependencyViolation::CallBareNameWithoutCall`]). MySQL-only among
3415 /// the shipped presets (and Lenient); DuckDB's parentheses are mandatory (a bare
3416 /// `CALL pragma_version` is a syntax error), so it is off there and a `CALL name` with no
3417 /// following `(` rejects.
3418 pub call_bare_name: bool,
3419 /// Accept the `LOAD <string>` extension/shared-library load statement. On for
3420 /// PostgreSQL/DuckDB/Lenient (PostgreSQL `LOAD 'plpgsql'`, DuckDB `LOAD 'tpch'` —
3421 /// both accept a string argument); off in ANSI/MySQL/SQLite, where the leading `LOAD`
3422 /// surfaces as an unknown statement. The DuckDB bare-identifier argument rides the
3423 /// separate [`load_bare_name`](UtilitySyntax::load_bare_name) gate.
3424 pub load_extension: bool,
3425 /// Accept DuckDB's bare-identifier `LOAD <name>` argument (`LOAD tpch`) on top of the
3426 /// base [`load_extension`](UtilitySyntax::load_extension) statement (the dependency is
3427 /// [`FeatureDependencyViolation::LoadBareNameWithoutLoadExtension`]). DuckDB-only among the
3428 /// shipped presets (and Lenient); PostgreSQL's `LOAD` requires a string
3429 /// (`LOAD tpch` is a pg_query parser error), so it is off there.
3430 pub load_bare_name: bool,
3431 /// Accept MySQL's `LOAD {DATA | XML} … INFILE … INTO TABLE …` bulk-import statement — a
3432 /// DIFFERENT behaviour on the leading `LOAD` keyword from the PostgreSQL/DuckDB
3433 /// [`load_extension`](UtilitySyntax::load_extension) statement, dispatched on the two-word
3434 /// `LOAD DATA`/`LOAD XML` lookahead so the two never collide even where both gates are on
3435 /// (the `do_statement`/`do_expression_list` split precedent). MySQL-only among the shipped
3436 /// presets (and Lenient); off elsewhere, where a leading `LOAD DATA` surfaces as an unknown
3437 /// statement. Covers the classic documented clause train (`PARTITION`, `CHARACTER SET`,
3438 /// `FIELDS`/`LINES`, `IGNORE n LINES`, the column/`@var` list, `SET`, and — grammar-shared —
3439 /// `ROWS IDENTIFIED BY`); the MySQL 8.4 secondary-engine bulk-load extension clauses (`URL`/
3440 /// `S3` sources, `COUNT`, `COMPRESSION`, `PARALLEL`, `MEMORY`, `ALGORITHM`) are a separate
3441 /// feature not covered by this gate.
3442 pub load_data: bool,
3443 /// Accept a `SESSION | LOCAL | GLOBAL` scope qualifier before a `RESET <name>` target
3444 /// (DuckDB `RESET SESSION x`, `RESET GLOBAL x`; DuckDB parse-accepts all three, though
3445 /// `RESET LOCAL` is a runtime not-implemented). DuckDB-only among the shipped presets
3446 /// (and Lenient); PostgreSQL's `RESET` takes no scope prefix (`RESET SESSION x` is a
3447 /// pg_query parser error — its `RESET SESSION AUTHORIZATION` is a distinct special
3448 /// form, not modelled), so it is off there and a scope keyword after `RESET` rejects.
3449 pub reset_scope: bool,
3450 /// Accept a DuckDB `IF EXISTS` guard on `DETACH DATABASE IF EXISTS <name>` (on top of
3451 /// the [`attach`](UtilitySyntax::attach) `DETACH` statement; the dependency is
3452 /// [`FeatureDependencyViolation::DetachIfExistsWithoutAttach`]). DuckDB-only among the shipped
3453 /// presets (and Lenient); DuckDB admits the guard only after the `DATABASE` keyword
3454 /// (`DETACH IF EXISTS x` is a parser error, `DETACH DATABASE IF EXISTS x` parses —
3455 /// probed on 1.5.4). SQLite's `DETACH` has no `IF EXISTS`, so it is off there.
3456 pub detach_if_exists: bool,
3457 /// Accept the PostgreSQL `DO [LANGUAGE <lang>] '<body>'` anonymous code block. Its own
3458 /// leading-keyword gate, like [`copy`](UtilitySyntax::copy): PostgreSQL-only among the shipped
3459 /// presets (and Lenient); DuckDB has no `DO` statement (probed on 1.5.4: `DO $$...$$`
3460 /// is a parser error), and MySQL/SQLite have none, so there the leading `DO` keyword is
3461 /// not dispatched and surfaces as an unknown statement. The block body is an opaque
3462 /// procedural-language string, not re-parsed here (see
3463 /// [`DoStatement`](crate::ast::DoStatement)). Never both on with
3464 /// [`do_expression_list`](UtilitySyntax::do_expression_list) — the pair is the registered
3465 /// [`GrammarConflict::DoStatementVersusDoExpressionList`].
3466 pub do_statement: bool,
3467 /// Accept the MySQL `DO <expr> [, <expr> ...]` evaluate-and-discard statement — a
3468 /// *different behaviour on the same `DO` keyword* from the PostgreSQL anonymous code block
3469 /// gated by [`do_statement`](UtilitySyntax::do_statement). The two are mutually exclusive
3470 /// per dialect (each preset arms at most one), the split mirroring transaction-`BEGIN`
3471 /// vs compound-block-`BEGIN`; a preset never arms both, so the shared leading `DO` keyword
3472 /// dispatches unambiguously. MySQL-only among the shipped presets; off elsewhere (the
3473 /// permissive superset keeps the PostgreSQL code-block reading, since the `DO LANGUAGE`
3474 /// form has no expression-list spelling). Enabling both at once is the registered
3475 /// [`GrammarConflict::DoStatementVersusDoExpressionList`] — the code-block branch shadows the
3476 /// expression list, so `DO 'x'` mis-parses and `DO 1, 2` over-rejects. See
3477 /// [`DoExpressionsStatement`](crate::ast::DoExpressionsStatement).
3478 pub do_expression_list: bool,
3479 /// Accept the MySQL `LOCK {TABLES | TABLE} <tbl> [[AS] <alias>] {READ [LOCAL] | WRITE}
3480 /// [, ...]` explicit table-locking statement and its `UNLOCK {TABLES | TABLE}` release
3481 /// counterpart (one gate for the pair, the [`rename_statement`](Self::rename_statement)
3482 /// precedent: MySQL's `lock`/`unlock` grammar rules carry both — a dialect with one has
3483 /// both). This is the *per-table lock-kind* reading of the leading `LOCK` keyword —
3484 /// a [`do_statement`](Self::do_statement)/[`do_expression_list`](Self::do_expression_list)-style
3485 /// behaviour split: PostgreSQL's `LOCK [TABLE] <rel>, … [IN <mode> MODE] [NOWAIT]`
3486 /// statement-level mode-list reading is a different behaviour on the same keyword and,
3487 /// when implemented, takes its own gate — a preset would arm at most one, so the shared
3488 /// leading `LOCK`/`UNLOCK` keywords dispatch unambiguously. MySQL-only among the shipped
3489 /// presets (plus the Lenient superset, where it is a pure addition *today* — no other
3490 /// `LOCK`-keyword statement exists; the union will owe a reading decision when the
3491 /// PostgreSQL form lands). Off elsewhere, where the leading keyword is not dispatched and
3492 /// surfaces as an unknown statement. See
3493 /// [`LockTablesStatement`](crate::ast::LockTablesStatement) /
3494 /// [`UnlockTablesStatement`](crate::ast::UnlockTablesStatement).
3495 pub lock_tables: bool,
3496 /// Accept the MySQL `LOCK INSTANCE FOR BACKUP` / `UNLOCK INSTANCE` instance-wide
3497 /// backup-lock pair (one gate for both, as with [`lock_tables`](Self::lock_tables) —
3498 /// the same `lock`/`unlock` grammar rules carry them). A separate flag from
3499 /// `lock_tables` because the two surfaces are independent behaviours that only happen to
3500 /// share MySQL: the backup lock is a MySQL-8-specific administrative statement with no
3501 /// table list (MariaDB, for one, spells it `BACKUP LOCK` instead while sharing `LOCK
3502 /// TABLES`), and it is additionally collision-free with the future PostgreSQL mode-list
3503 /// reading (`LOCK instance …` continues with `IN`/`NOWAIT`/end there, never `FOR`).
3504 /// MySQL-only among the shipped presets (plus the Lenient superset). See
3505 /// [`InstanceLockStatement`](crate::ast::InstanceLockStatement).
3506 pub lock_instance: bool,
3507 /// Accept the MySQL standalone `RENAME TABLE <a> TO <b>[, ...]` and `RENAME USER <u>
3508 /// TO <v>[, ...]` object-rename statements (both →
3509 /// [`Statement::Rename`](crate::ast::Statement::Rename)). A leading-keyword gate like
3510 /// [`kill`](Self::kill): off outside MySQL (and the Lenient superset), where the
3511 /// leading `RENAME` keyword is not dispatched and surfaces as an unknown statement. The
3512 /// two forms share one gate because MySQL's single `rename` grammar rule carries both —
3513 /// a dialect with one has both. Distinct from the `ALTER TABLE ... RENAME TO`
3514 /// sub-clause, which is consumed by the `ALTER TABLE` grammar and never reaches this
3515 /// leading position.
3516 pub rename_statement: bool,
3517 /// Accept the MySQL diagnostics-area family — `SIGNAL`, `RESIGNAL`, and
3518 /// `GET [CURRENT | STACKED] DIAGNOSTICS` — as top-level statements (a leading-keyword gate
3519 /// like [`kill`](UtilitySyntax::kill)). One flag for all three: they are the single
3520 /// cohesive behaviour of manipulating the diagnostics area (raise / re-raise / read).
3521 ///
3522 /// Its own axis, NOT the body-context
3523 /// [`compound_statements`](StatementDdlGates::compound_statements) flag: these three attach
3524 /// to MySQL's top-level `simple_statement` production and are engine-recognized at top
3525 /// level (measured `1295`/`ER_UNSUPPORTED_PS` over the PREPARE oracle — grammar-valid,
3526 /// merely not preparable), exactly where a compound `BEGIN … END` block is NOT (a bare
3527 /// top-level `BEGIN` is transaction-start). Because the body dispatcher falls through to
3528 /// the top-level one for non-compound keywords, this single gate serves both surfaces. On
3529 /// for MySQL (and Lenient); off elsewhere, where the leading `SIGNAL`/`RESIGNAL`/`GET`
3530 /// keyword is left unconsumed and surfaces as an unknown statement.
3531 pub signal_diagnostics: bool,
3532 /// Accept the DuckDB `EXPORT DATABASE ['<db>' TO] '<path>' [<copy-options>]` catalogue
3533 /// dump *and* its `IMPORT DATABASE '<path>'` inverse. One flag gates both leading
3534 /// keywords because they are a single dialect unit — the two halves of one
3535 /// export/import round-trip, a dialect that dumps a database replays it (the same
3536 /// pairing reasoning [`attach`](Self::attach) uses for `ATTACH`/`DETACH`). DuckDB-only
3537 /// (and the permissive superset); off in ANSI, PostgreSQL, MySQL, and SQLite, which have
3538 /// no `EXPORT`/`IMPORT DATABASE`, so there the leading keywords are not dispatched and
3539 /// surface as unknown statements — the `copy`/`attach` leading-keyword-gate precedent.
3540 pub export_import_database: bool,
3541 /// Accept the DuckDB `UPDATE EXTENSIONS [( <name>, ... )]` extension-refresh statement
3542 /// ([`Statement::UpdateExtensions`](crate::ast::Statement::UpdateExtensions)). A
3543 /// *refinement* of the leading `UPDATE`, not a bare leading-keyword gate: a top-level
3544 /// `UPDATE` whose next word is the DuckDB-unreserved `EXTENSIONS` keyword is this
3545 /// statement only when that word is followed by the parenthesized name list or the
3546 /// statement end; an `UPDATE extensions SET …` (a table literally named `extensions`,
3547 /// or `… AS e SET …`) still routes to the DML `UPDATE`, exactly as DuckDB's own grammar
3548 /// resolves the shared prefix (engine-probed on 1.5.4). Off for every non-DuckDB preset
3549 /// (bar the Lenient superset), where the `EXTENSIONS` lookahead is never taken and every
3550 /// `UPDATE` reaches the DML parser unchanged.
3551 pub update_extensions: bool,
3552 /// Accept the MySQL `FLUSH [NO_WRITE_TO_BINLOG | LOCAL] <target>` server-administration
3553 /// statement ([`Statement::Flush`](crate::ast::Statement::Flush)) — the `{TABLE | TABLES}
3554 /// [<list>] [WITH READ LOCK | FOR EXPORT]` form and the comma-separated keyword-target
3555 /// list (`PRIVILEGES`, `LOGS`, `STATUS`, `RELAY LOGS FOR CHANNEL …`, …). A leading-keyword
3556 /// gate like [`kill`](Self::kill): on for MySQL (and the Lenient superset), off elsewhere,
3557 /// where the leading `FLUSH` keyword is not dispatched and surfaces as an unknown
3558 /// statement.
3559 pub flush: bool,
3560 /// Accept the MySQL `PURGE BINARY LOGS {TO '<log>' | BEFORE <datetime>}` binary-log purge
3561 /// statement ([`Statement::Purge`](crate::ast::Statement::Purge)). A leading-keyword gate
3562 /// like [`kill`](Self::kill): on for MySQL (and the Lenient superset), off elsewhere,
3563 /// where the leading `PURGE` keyword is not dispatched and surfaces as an unknown
3564 /// statement. Named for the only surviving 8.4 form — the deprecated `MASTER` synonym was
3565 /// removed, so this gate carries `BINARY LOGS` alone.
3566 pub purge_binary_logs: bool,
3567 /// Accept the MySQL replication-administration family
3568 /// ([`Statement::Replication`](crate::ast::Statement::Replication)) — `CHANGE REPLICATION
3569 /// SOURCE TO <options>`, `CHANGE REPLICATION FILTER <rules>`, `START`/`STOP REPLICA`, and
3570 /// `START`/`STOP GROUP_REPLICATION`. One gate for all five measured families because they
3571 /// are one cohesive dialect unit: MySQL's replication-control surface, reached through the
3572 /// replication-specific leading-keyword sequences (`CHANGE REPLICATION`, `START`/`STOP
3573 /// REPLICA`, `START`/`STOP GROUP_REPLICATION`). A dialect either implements MySQL
3574 /// replication administration or it does not — Group Replication rides the same unit as
3575 /// classic asynchronous replication (both are the server's replication control, not a
3576 /// severable syntax axis). A *refinement* of the shared `START`/`STOP`/`CHANGE` leading
3577 /// keywords (not a bare leading-keyword gate like [`kill`](Self::kill)): the dispatch
3578 /// claims `START`/`STOP` only when the next word is `REPLICA`/`GROUP_REPLICATION` and
3579 /// `CHANGE` only before `REPLICATION`, so `START TRANSACTION` and every other use of those
3580 /// keywords is untouched. On for MySQL (and the Lenient superset); off elsewhere, where
3581 /// the sequences are not dispatched and surface as unknown statements. MySQL 8.4 removed
3582 /// the legacy `MASTER`/`SLAVE` spellings, so only the `REPLICATION`/`REPLICA` grammar is
3583 /// accepted.
3584 pub replication_statements: bool,
3585}
3586
3587/// Dialect-owned SHOW/DESCRIBE introspection-statement syntax accepted by the parser.
3588///
3589/// The session `SHOW`/`SET`/`RESET` reader, the typed `SHOW <object>` catalogue
3590/// listings, and the MySQL/DuckDB `DESCRIBE`/`SUMMARIZE` introspection statements. Split
3591/// out of [`UtilitySyntax`] at its 16-field line as the introspection axis. Each flag is a
3592/// leading-keyword dispatch gate: when off the keyword is not dispatched and surfaces as
3593/// an unknown statement (or the typed-`SHOW` lookahead falls through to the session reader).
3594/// **Statement-head gates are consumed by `parser::statement_dispatch`**; parse bodies live
3595/// in the SHOW/session family modules.
3596#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3597pub struct ShowSyntax {
3598 /// Accept MySQL's `DESCRIBE`/`DESC` keywords as EXPLAIN synonyms *and* the MySQL
3599 /// `{DESCRIBE | DESC | EXPLAIN} <table> [<column> | '<pattern>']` table-metadata
3600 /// overload. MySQL-only (and the permissive superset). When on: the leading `DESCRIBE`
3601 /// and `DESC` keywords are dispatched into the EXPLAIN grammar (spelling recorded on
3602 /// [`ExplainStatement`](crate::ast::ExplainStatement)), and all three EXPLAIN-family
3603 /// keywords additionally accept a table name in place of an explainable statement
3604 /// (yielding a [`DescribeStatement`](crate::ast::DescribeStatement)). When off
3605 /// (ANSI/PostgreSQL/SQLite): `DESCRIBE`/`DESC` are not statement leaders and `EXPLAIN`
3606 /// keeps its plain query-plan-only grammar, so a table after `EXPLAIN` is rejected as
3607 /// PostgreSQL does. `EXPLAIN` itself stays ungated (accepted everywhere).
3608 pub describe: bool,
3609 /// Accept DuckDB's leading-keyword `{DESCRIBE | SUMMARIZE} <query> | <table>`
3610 /// introspection statement ([`Statement::ShowRef`](crate::ast::Statement::ShowRef)).
3611 /// DuckDB desugars it to `SELECT * FROM (<SHOW_REF>)`, so it reuses the same `SHOW_REF`
3612 /// core ([`ShowRef`](crate::ast::ShowRef)) as the parenthesized table factor, only at
3613 /// statement-leading position. DuckDB-only (and the permissive superset).
3614 ///
3615 /// Distinct from [`describe`](ShowSyntax::describe): that flag is MySQL's overload of the
3616 /// EXPLAIN keyword (`DESCRIBE` as a query-plan synonym, plus the `<table> [<column>]`
3617 /// metadata form) and never touches `SUMMARIZE`; this one is DuckDB's `SHOW_REF`
3618 /// utility, whose `DESCRIBE`/`SUMMARIZE` return the target's schema / summary statistics
3619 /// as a relation. The two share the `DESCRIBE` leader but are *not* a
3620 /// [`GrammarConflict`]: every real-dialect preset enables at most one, and Lenient — the
3621 /// one preset with both on — resolves the shared leader deterministically by dispatch
3622 /// order (`DESCRIBE`/`DESC` route to the MySQL EXPLAIN synonym above; only `SUMMARIZE`
3623 /// reaches this `SHOW_REF` utility), a conflict-free permissive union rather than a mutual
3624 /// exclusion, so no registry variant governs it. Boundary with
3625 /// [`TableFactorSyntax::show_ref`](crate::dialect::TableFactorSyntax::show_ref):
3626 /// that flag owns the same core inside a `FROM (…)` table factor; this one owns the
3627 /// statement-leading spelling — one production, two grammar positions, one flag each.
3628 pub describe_summarize: bool,
3629 /// Accept the session statements `SET <var> …` / `RESET …` / `SHOW …` (PostgreSQL/
3630 /// MySQL; the standard `SET CONSTRAINTS`/`SET SESSION CHARACTERISTICS` also route
3631 /// here). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no session-variable
3632 /// statement, so it is off; the leading keyword is then not dispatched and surfaces as
3633 /// an unknown statement. `SET TRANSACTION` is transaction control (claimed earlier in
3634 /// dispatch), so it is unaffected by this gate.
3635 pub session_statements: bool,
3636 /// Keywords rejected as unquoted values in the generic
3637 /// `SET <parameter> {= | TO} <value> [, ...]` grammar. String and numeric literals,
3638 /// ordinary identifiers, and non-reserved keywords remain accepted. PostgreSQL's
3639 /// `var_value` production and DuckDB's corresponding grammar both enforce this
3640 /// boundary; permissive compatibility dialects may accept any keyword.
3641 pub set_value_reserved_words: KeywordSet,
3642 /// Accept the otherwise-reserved `ON` keyword as a generic `SET` value.
3643 /// PostgreSQL names `ON` explicitly in `opt_boolean_or_string`; DuckDB does not.
3644 /// `TRUE` and `FALSE` are accepted independently as boolean values whenever reserved
3645 /// words are rejected.
3646 pub set_value_on_keyword: bool,
3647 /// Accept the otherwise-reserved `NULL` keyword as a generic `SET` value. DuckDB
3648 /// accepts this spelling; PostgreSQL's `var_value` production does not.
3649 pub set_value_null_keyword: bool,
3650 /// Accept the typed `SHOW [EXTENDED] [FULL] [ALL] TABLES [{FROM | IN} <db>] [LIKE
3651 /// '<pat>' | WHERE <expr>]` catalogue-listing statement
3652 /// ([`Statement::Show`](crate::ast::Statement::Show)). On for MySQL/DuckDB/Lenient;
3653 /// off for ANSI/PostgreSQL/SQLite.
3654 ///
3655 /// This is a *refinement* of the generic-`SHOW` dispatch, so its boundary with the
3656 /// two other `SHOW` seams is MECE:
3657 /// - [`session_statements`](ShowSyntax::session_statements) owns the generic top-level
3658 /// `SHOW <var>` that reads one configuration parameter. When `show_tables` is on,
3659 /// a top-level `SHOW` whose next word (past the optional `EXTENDED`/`FULL`/`ALL`
3660 /// modifiers) is `TABLES` is claimed here instead; every other `SHOW <var>` still
3661 /// falls through to the session statement. So PostgreSQL (this flag off) keeps
3662 /// parsing `SHOW tables` as a generic session `SHOW` — the parse it already accepts
3663 /// — and only MySQL/DuckDB reinterpret the `TABLES` keyword as the catalogue listing.
3664 /// - [`TableFactorSyntax::show_ref`](crate::dialect::TableFactorSyntax::show_ref)
3665 /// owns the parenthesized `(SHOW <name>)` / `(DESCRIBE …)` *table factor* that only
3666 /// appears inside a `FROM (…)` and yields a relation; it never reaches the
3667 /// statement-leading position this flag governs.
3668 ///
3669 /// One flag per typed-`SHOW` subform (the sibling `SHOW COLUMNS`/`SHOW CREATE TABLE`/
3670 /// `SHOW FUNCTIONS` tickets add their own), because their per-dialect availability
3671 /// differs — the `copy`/`comment_on` separate-flags precedent. Once on, the single
3672 /// flag admits the whole modifier union permissively (MySQL's `FULL`/`LIKE`/`WHERE`,
3673 /// DuckDB's `ALL`), the DESCRIBE/PRAGMA single-flag-utility precedent — the corpus
3674 /// verdict gate catches only corpus-present over-acceptance.
3675 pub show_tables: bool,
3676 /// Accept the typed `SHOW [EXTENDED] [FULL] {COLUMNS | FIELDS} {FROM | IN} <tbl>
3677 /// [{FROM | IN} <db>] [LIKE '<pat>' | WHERE <expr>]` column-listing statement
3678 /// ([`Statement::Show`](crate::ast::Statement::Show) with
3679 /// [`ShowTarget::Columns`](crate::ast::ShowTarget)). On for MySQL/Lenient only; off for
3680 /// ANSI/PostgreSQL/SQLite/DuckDB.
3681 ///
3682 /// A separate gate from [`show_tables`](ShowSyntax::show_tables) because its per-dialect
3683 /// availability differs: DuckDB accepts `SHOW [ALL] TABLES` but has *no* `SHOW COLUMNS`
3684 /// grammar at all (every `SHOW {COLUMNS | FIELDS} …` form is `ER_PARSE_ERROR` on DuckDB
3685 /// 1.5.4, engine-probed — it uses `DESCRIBE` / `SHOW <table>` instead), so a shared flag
3686 /// would over-accept there. Same MECE refinement of the generic-`SHOW` dispatch as
3687 /// `show_tables`: a top-level `SHOW` whose next word past the optional `EXTENDED`/`FULL`
3688 /// modifiers is `COLUMNS` or the `FIELDS` synonym is claimed here; every other
3689 /// `SHOW <var>` still falls through to [`session_statements`](ShowSyntax::session_statements),
3690 /// and the parenthesized `(SHOW <name>)`
3691 /// [`show_ref`](crate::dialect::TableFactorSyntax::show_ref) table factor is
3692 /// untouched. Once on, the single flag admits the whole MySQL modifier/filter union
3693 /// permissively (the DESCRIBE/PRAGMA single-flag-utility precedent).
3694 pub show_columns: bool,
3695 /// Accept the typed `SHOW CREATE TABLE <tbl>` statement — the `CREATE TABLE` DDL that
3696 /// would recreate the named table ([`Statement::Show`](crate::ast::Statement::Show) with
3697 /// [`ShowTarget::Create`](crate::ast::ShowTarget) and
3698 /// [`ShowCreateKind::Table`](crate::ast::ShowCreateKind)). On for MySQL/Lenient only; off
3699 /// for ANSI/PostgreSQL/SQLite/DuckDB. The other `SHOW CREATE <kind>` object kinds ride
3700 /// [`show_admin`](ShowSyntax::show_admin).
3701 ///
3702 /// A separate gate from [`show_tables`](ShowSyntax::show_tables)/[`show_columns`](ShowSyntax::show_columns)
3703 /// because its per-dialect availability differs: MySQL has `SHOW CREATE TABLE` but DuckDB
3704 /// has no such grammar (it uses `SELECT sql FROM duckdb_tables` / `.schema`), so a shared
3705 /// flag would over-accept there. Same MECE refinement of the generic-`SHOW` dispatch: a
3706 /// top-level `SHOW` whose next two words are `CREATE TABLE` is claimed here; a bare
3707 /// `SHOW create` (the two-keyword lookahead requires `TABLE` to follow) and every other
3708 /// `SHOW <var>` still fall through to [`session_statements`](ShowSyntax::session_statements),
3709 /// so PostgreSQL's generic `SHOW <var>` reading `create` as the variable name is
3710 /// undisturbed. There are no `EXTENDED`/`FULL` modifiers on this subform (MySQL docs).
3711 /// Only the `TABLE` object kind is modelled; `SHOW CREATE {DATABASE | VIEW | …}` is
3712 /// deferred to sibling tickets.
3713 pub show_create_table: bool,
3714 /// Accept the typed `SHOW [{USER | SYSTEM | ALL}] FUNCTIONS [{FROM | IN} <schema>]
3715 /// [[LIKE] {<function_name> | '<regex>'}]` function-listing statement
3716 /// ([`Statement::Show`](crate::ast::Statement::Show) with
3717 /// [`ShowTarget::Functions`](crate::ast::ShowTarget)). On for Databricks/Lenient only;
3718 /// off for ANSI/PostgreSQL/MySQL/SQLite/DuckDB.
3719 ///
3720 /// A separate gate from the other `show_*` subforms because its per-dialect
3721 /// availability differs — and it is the first typed-`SHOW` flag on under Databricks,
3722 /// where the other three are off. Spark/Databricks are the only shipped engines with a
3723 /// bare `SHOW FUNCTIONS` listing (doc-cited grammar); MySQL's `SHOW FUNCTION STATUS` is
3724 /// a *different* routine-catalogue statement (deferred to its own ticket), and DuckDB
3725 /// has no `SHOW FUNCTIONS` grammar at all — `SHOW <name>` there is a `DESCRIBE` alias,
3726 /// so `SHOW functions` describes a table named `functions` (engine-probed on 1.5.4), a
3727 /// generic-`SHOW` reinterpretation a shared flag would corrupt. Same MECE refinement of
3728 /// the generic-`SHOW` dispatch: a top-level `SHOW` whose next word (past the optional
3729 /// `USER`/`SYSTEM`/`ALL` scope) is `FUNCTIONS` is claimed here; a bare `SHOW <var>`
3730 /// (including `SHOW ALL` with no `FUNCTIONS`) still falls through to
3731 /// [`session_statements`](ShowSyntax::session_statements). Once on, the single flag admits the
3732 /// whole modifier/filter union permissively (the DESCRIBE/PRAGMA single-flag-utility
3733 /// precedent).
3734 pub show_functions: bool,
3735 /// Accept the typed `SHOW {FUNCTION | PROCEDURE} STATUS [LIKE '<pat>' | WHERE <expr>]`
3736 /// stored-routine catalogue listing
3737 /// ([`Statement::Show`](crate::ast::Statement::Show) with
3738 /// [`ShowTarget::RoutineStatus`](crate::ast::ShowTarget)). On for MySQL/Lenient only;
3739 /// off for ANSI/PostgreSQL/SQLite/DuckDB/Databricks.
3740 ///
3741 /// A separate gate from [`show_functions`](ShowSyntax::show_functions) because it is a
3742 /// *different statement*, not the same one under another dialect: MySQL's routine
3743 /// catalogue takes the singular `FUNCTION`/`PROCEDURE` object keyword plus a mandatory
3744 /// `STATUS` and lists a row per stored routine, where the Spark/Databricks
3745 /// `show_functions` listing takes the bare plural `FUNCTIONS` and lists function names.
3746 /// Their per-dialect availability is disjoint (MySQL rejects `SHOW FUNCTIONS`,
3747 /// Databricks rejects `SHOW FUNCTION STATUS`), so a shared flag would over-accept in
3748 /// both directions — the one-flag-per-typed-`SHOW`-subform precedent. Same MECE
3749 /// refinement of the generic-`SHOW` dispatch as the sibling gates: a top-level `SHOW`
3750 /// whose next two words are `FUNCTION STATUS` or `PROCEDURE STATUS` is claimed here. The
3751 /// two-keyword lookahead steals only that full prefix; `FUNCTION`/`PROCEDURE` are reserved
3752 /// keywords, so a bare `SHOW FUNCTION` cannot be a generic session `SHOW <var>` (like
3753 /// `SHOW CREATE`, it is a parse error both ways), and every other `SHOW <var>` still falls
3754 /// through to [`session_statements`](ShowSyntax::session_statements). There is no scope keyword and
3755 /// no `{FROM | IN}` qualifier — `SHOW FUNCTION STATUS FROM db` is `ER_PARSE_ERROR` on
3756 /// mysql:8 (engine-probed) — only the optional `LIKE`/`WHERE` narrowing, which reuses the
3757 /// shared [`ShowFilter`](crate::ast::ShowFilter).
3758 pub show_routine_status: bool,
3759 /// Accept the trailing `VERBOSE` on a generic session `SHOW` — `SHOW ALL VERBOSE`,
3760 /// `SHOW <setting> VERBOSE` — carried on
3761 /// [`SessionStatement::Show::verbose`](crate::ast::SessionStatement). On for Lenient
3762 /// only; off for every oracle-backed dialect.
3763 ///
3764 /// `VERBOSE` here is the sqlparser-rs/DataFusion planner spelling, *not* a database
3765 /// grammar: `pg_query` and DuckDB both reject `SHOW ALL VERBOSE` and
3766 /// `SHOW <setting> VERBOSE` (engine-probed), so no dialect with a real oracle can turn
3767 /// it on without diverging. It refines only the session `SHOW` seam and never the
3768 /// typed-`SHOW` dispatch: the `TABLES`/`COLUMNS`/`CREATE TABLE`/`FUNCTIONS` lookaheads
3769 /// each insist on their keyword after the modifiers, so `SHOW ALL VERBOSE` (no
3770 /// `TABLES`) falls through to the session branch and `SHOW ALL TABLES` stays typed —
3771 /// the seams remain MECE. A behaviour-named tail flag, not gated on
3772 /// [`session_statements`](ShowSyntax::session_statements): where session `SHOW` is off
3773 /// (SQLite) the keyword is never dispatched, so this flag is inert there regardless.
3774 pub show_verbose: bool,
3775 /// Accept the MySQL server-administration / catalogue-introspection `SHOW` sub-command
3776 /// family — the ~40 `SHOW` productions beyond the individually-gated
3777 /// `TABLES`/`COLUMNS`/`CREATE TABLE`/`{FUNCTION|PROCEDURE} STATUS` subforms:
3778 /// `SHOW DATABASES`, `SHOW [GLOBAL|SESSION] {STATUS|VARIABLES}`, `SHOW PLUGINS`,
3779 /// `SHOW [STORAGE] ENGINES`, `SHOW ENGINE <e> {STATUS|MUTEX|LOGS}`, `SHOW PRIVILEGES`,
3780 /// `SHOW {CHARACTER SET|CHARSET}`, `SHOW COLLATION`, `SHOW EVENTS`, `SHOW TABLE STATUS`,
3781 /// `SHOW OPEN TABLES`, `SHOW [FULL] TRIGGERS`, `SHOW [FULL] PROCESSLIST`,
3782 /// `SHOW CREATE {VIEW|DATABASE|EVENT|PROCEDURE|FUNCTION|TRIGGER} <name>`,
3783 /// `SHOW [EXTENDED] {INDEX|INDEXES|KEYS} FROM <t>`, `SHOW GRANTS`,
3784 /// `SHOW {WARNINGS|ERRORS} [LIMIT …]` / `SHOW COUNT(*) {WARNINGS|ERRORS}`,
3785 /// `SHOW BINARY LOGS`, `SHOW REPLICAS`, `SHOW BINARY LOG STATUS`,
3786 /// `SHOW REPLICA STATUS [FOR CHANNEL …]`, `SHOW PROFILES`, and
3787 /// `SHOW {PROCEDURE|FUNCTION} CODE <name>` (all → [`Statement::Show`](crate::ast::Statement::Show)).
3788 /// On for MySQL/Lenient only; off for every other preset.
3789 ///
3790 /// This is a *single* behaviour flag for the whole family rather than one flag per
3791 /// sub-command (the `show_tables`/`show_columns` precedent) because, unlike those, this
3792 /// family's per-dialect availability does not vary — every member is MySQL-only and they
3793 /// travel together; the sub-command identity is DATA (the [`ShowTarget`](crate::ast::ShowTarget)
3794 /// `Listing`/`Bare`/`Create`/`Index`/`Engine`/… axis), not a separate behaviour axis.
3795 /// The parser reaches it through one table-driven dispatch (`parse_show_admin_statement`),
3796 /// not one arm per keyword. Same MECE refinement of the generic-`SHOW` dispatch as the
3797 /// sibling gates: the lookahead insists on one of the family's lead keywords, so any
3798 /// other `SHOW <var>` still falls through to
3799 /// [`session_statements`](ShowSyntax::session_statements). The gate also covers the
3800 /// grammatically heavier subforms with operands (`SHOW GRANTS FOR <user> [USING …]`,
3801 /// `SHOW CREATE USER`, `SHOW PROFILE`, `SHOW {BINLOG | RELAYLOG} EVENTS`); when off, every
3802 /// family keyword falls through unclaimed.
3803 pub show_admin: bool,
3804}
3805
3806/// Dialect-owned physical-maintenance-statement syntax accepted by the parser.
3807///
3808/// The storage-maintenance statements and their operands. Split out of [`UtilitySyntax`]
3809/// at its 16-field line as the maintenance axis, distinct from the introspection and
3810/// access-control axes. Each flag is a leading-keyword dispatch gate: when off the keyword
3811/// is not dispatched and surfaces as an unknown statement. **Statement-head gates are
3812/// consumed by `parser::statement_dispatch`**; parse bodies live in the maintenance family
3813/// modules.
3814#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3815pub struct MaintenanceSyntax {
3816 /// Accept the SQLite `VACUUM [<schema>] [INTO <expr>]` database-compaction
3817 /// statement. SQLite-only (and the permissive superset). It takes its own flag
3818 /// rather than sharing one with `reindex`/`analyze` because the three are
3819 /// independent maintenance statements, not an inverse pair like `ATTACH`/`DETACH`
3820 /// — the `copy`/`comment_on` precedent (separate flags even though every shipped
3821 /// dialect toggles them together). PostgreSQL also has a (differently-shaped)
3822 /// `VACUUM`, which is not modelled, so this stays off there; when off the leading
3823 /// keyword is not dispatched and surfaces as an unknown statement.
3824 pub vacuum: bool,
3825 /// Accept DuckDB's `VACUUM [ANALYZE] [<table> [(<col>, …)]]` statistics/compaction
3826 /// statement — a *separate* leading-`VACUUM` base gate from SQLite's
3827 /// [`vacuum`](MaintenanceSyntax::vacuum) (the two dialects' `VACUUM` operand grammars
3828 /// are disjoint: SQLite's `[<schema>] INTO <expr>` versus DuckDB's `[ANALYZE] <table>
3829 /// (<cols>)`), so the leading keyword dispatches when *either* is on and the parser
3830 /// reads whichever tail its gate admits. On for DuckDB/Lenient; off elsewhere. DuckDB's
3831 /// `VACUUM` admits only the `ANALYZE` option — 1.5.4's transform throws
3832 /// `NotImplementedException` on `FULL`/`FREEZE`/`VERBOSE`/`disable_page_skipping`, so
3833 /// those never parse and this gate never over-accepts them. Independent of
3834 /// [`vacuum`](MaintenanceSyntax::vacuum): a dialect can admit one `VACUUM` grammar
3835 /// without the other.
3836 ///
3837 /// The column list is bundled here rather than split into a
3838 /// `vacuum_analyze_columns` sibling of
3839 /// [`analyze_columns`](MaintenanceSyntax::analyze_columns) — a deliberate
3840 /// granularity asymmetry. The `ANALYZE` split is measured necessity: two engines
3841 /// share the leading `ANALYZE` but differ on the column list (SQLite has
3842 /// [`analyze`](MaintenanceSyntax::analyze) without
3843 /// [`analyze_columns`](MaintenanceSyntax::analyze_columns); DuckDB has both). No
3844 /// second engine shares this `VACUUM` grammar, and DuckDB's grammar ties the column
3845 /// list to the table operand inseparably — engine-measured on 1.5.4, `VACUUM (a)`
3846 /// without a table reads the parens as the PG-legacy *options* list
3847 /// (`Parser Error: unrecognized VACUUM option "a"`), not columns — so a split flag
3848 /// would have no independent surface to gate. Split only when a second dialect's
3849 /// measured grammar demands it.
3850 pub vacuum_analyze: bool,
3851 /// Accept the SQLite `REINDEX [<name>]` index-rebuild statement. SQLite-only (and
3852 /// the permissive superset); its own flag for the same reason as
3853 /// [`vacuum`](MaintenanceSyntax::vacuum). Off elsewhere.
3854 pub reindex: bool,
3855 /// Accept the SQLite `ANALYZE [<name>]` / DuckDB `ANALYZE [<table>]` statistics
3856 /// statement (a *leading* `ANALYZE`; the `ANALYZE` option inside `EXPLAIN` is
3857 /// unaffected). On for SQLite/DuckDB (and the permissive superset); its own flag for
3858 /// the same reason as [`vacuum`](MaintenanceSyntax::vacuum). Off elsewhere.
3859 pub analyze: bool,
3860 /// Accept DuckDB's optional parenthesized column list on top of the base
3861 /// [`analyze`](MaintenanceSyntax::analyze) statement (`ANALYZE <table> (<col>, …)`; the
3862 /// dependency is [`FeatureDependencyViolation::AnalyzeColumnsWithoutAnalyze`]). DuckDB-only
3863 /// among the shipped presets (and Lenient); SQLite's `ANALYZE` takes no column list, so
3864 /// it is off there and the trailing `(` surfaces as a parser error.
3865 ///
3866 /// This split exists because two engines share the base
3867 /// [`analyze`](MaintenanceSyntax::analyze) statement and differ only on the column
3868 /// list; the DuckDB `VACUUM` column list has no such second consumer, so it stays
3869 /// bundled inside [`vacuum_analyze`](MaintenanceSyntax::vacuum_analyze) — see that
3870 /// flag's doc for the measured justification of the granularity asymmetry.
3871 pub analyze_columns: bool,
3872 /// Accept the bare `CHECKPOINT` write-ahead-log flush statement. On for
3873 /// PostgreSQL/DuckDB/Lenient (both engines accept a bare `CHECKPOINT` — measured on
3874 /// pg_query PG-17 and DuckDB 1.5.4); off in ANSI/MySQL/SQLite, where the leading
3875 /// keyword is not dispatched and surfaces as an unknown statement. The DuckDB `FORCE`
3876 /// modifier and database operand ride the separate
3877 /// [`checkpoint_database`](MaintenanceSyntax::checkpoint_database) gate (PostgreSQL rejects both).
3878 pub checkpoint: bool,
3879 /// Accept DuckDB's `[FORCE] CHECKPOINT [<database>]` operands on top of the base
3880 /// [`checkpoint`](MaintenanceSyntax::checkpoint) statement (the dependency is
3881 /// [`FeatureDependencyViolation::CheckpointDatabaseWithoutCheckpoint`]): the optional
3882 /// `FORCE` modifier and the
3883 /// optional single database name. DuckDB-only among the shipped presets (and Lenient);
3884 /// PostgreSQL's `CHECKPOINT` takes no operands (`FORCE CHECKPOINT` and `CHECKPOINT db`
3885 /// are pg_query parser errors), so it is off there and both forms reject.
3886 pub checkpoint_database: bool,
3887 /// Accept the MySQL admin-table maintenance verbs `{ANALYZE | CHECK | CHECKSUM |
3888 /// OPTIMIZE | REPAIR} {TABLE | TABLES} <table-list> [options]` (all →
3889 /// [`Statement::TableMaintenance`](crate::ast::Statement::TableMaintenance)). One
3890 /// behaviour gate for the whole five-verb family rather than one flag per verb: the
3891 /// verb is DATA on the [`TableMaintenanceKind`](crate::ast::TableMaintenanceKind) axis,
3892 /// not a separate behaviour axis — every member is MySQL-only and they travel together
3893 /// (the `show_admin` precedent). On for MySQL/Lenient only; off in every other preset,
3894 /// where the leading verb is not dispatched and surfaces as an unknown statement.
3895 ///
3896 /// The dispatch is MECE with the SQLite/DuckDB leading-`ANALYZE`
3897 /// [`analyze`](MaintenanceSyntax::analyze) gate: MySQL's `ANALYZE` always takes
3898 /// `{TABLE | TABLES}` (optionally after the `NO_WRITE_TO_BINLOG | LOCAL` prefix), so
3899 /// the lookahead insists on that follow-set before claiming the keyword; a bare
3900 /// `ANALYZE` still falls through to the sibling gate.
3901 pub table_maintenance: bool,
3902}
3903
3904/// Dialect-owned access-control-statement syntax accepted by the parser.
3905///
3906/// The `GRANT`/`REVOKE` permission statements and their extended object/prefix grammar.
3907/// Split out of [`UtilitySyntax`] at its 16-field line as the access-control axis. Each
3908/// flag is a grammar gate: when off the keyword is not dispatched, or the extended object
3909/// grammar rejects, as the dialect requires.
3910#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3911pub struct AccessControlSyntax {
3912 /// Accept PostgreSQL `ALTER ROLE <name> RENAME TO <new_name>`.
3913 pub alter_role_rename: bool,
3914 /// Accept the access-control statements `GRANT …` / `REVOKE …` (SQL:2016 E081;
3915 /// PostgreSQL/MySQL). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no
3916 /// permission system, so it is off; the leading keyword is then not dispatched and
3917 /// surfaces as an unknown statement.
3918 pub access_control: bool,
3919 /// Accept the PostgreSQL/standard *extended* `GRANT`/`REVOKE` object and prefix
3920 /// grammar on top of the base [`access_control`](AccessControlSyntax::access_control) statements (the
3921 /// dependency is [`FeatureDependencyViolation::AccessControlExtendedObjectsWithoutAccessControl`]): a
3922 /// schema-scoped object (`ON SCHEMA s`, `ON DATABASE d`), the `ON ALL <kind> IN
3923 /// SCHEMA s` bulk form, and the `{GRANT | ADMIN} OPTION FOR` `REVOKE` prefix. On for
3924 /// ANSI/PostgreSQL/DuckDB/Lenient. MySQL admits `GRANT`/`REVOKE` but not these forms —
3925 /// its object grammar is `ON [TABLE | FUNCTION | PROCEDURE] priv_level`, and `SCHEMA` /
3926 /// `DATABASE` are reserved words that cannot introduce a priv_level, so those objects
3927 /// and the `OPTION FOR` prefix are the syntax error MySQL reports (engine-measured on
3928 /// mysql:8 — `GRANT … ON SCHEMA s`, `REVOKE GRANT OPTION FOR … `, `REVOKE … ON SCHEMA
3929 /// s` all `ER_PARSE_ERROR`), so it is off there. The bare/`TABLE`/`FUNCTION`/`PROCEDURE`
3930 /// objects, `WITH GRANT OPTION`, and the role-membership `GRANT r TO u` / `REVOKE r
3931 /// FROM u` forms are unaffected (MySQL accepts them), so the gate never over-rejects
3932 /// MySQL's supported surface. (SQLite has no permission system, so
3933 /// [`access_control`](AccessControlSyntax::access_control) is already off there and this is moot.)
3934 /// Never both on with the MySQL account route
3935 /// [`access_control_account_grants`](AccessControlSyntax::access_control_account_grants) — the
3936 /// pair is the registered [`GrammarConflict::AccountGrantsVersusExtendedObjects`], where the
3937 /// route dispatches its grammar before this extended-object reading is consulted.
3938 pub access_control_extended_objects: bool,
3939 /// Accept the MySQL account-management DDL family — `CREATE`/`ALTER`/`DROP USER`,
3940 /// `CREATE`/`DROP ROLE` — with its shared account-name (`user@host` / `CURRENT_USER`),
3941 /// authentication (`IDENTIFIED BY`/`WITH`), TLS (`REQUIRE`), resource (`WITH …`), and
3942 /// password/lock option surface. On for MySQL/Lenient. Off elsewhere (no other dialect
3943 /// models MySQL accounts): the leading `USER`/`ROLE` after `CREATE`/`ALTER`/`DROP` is then
3944 /// not dispatched and surfaces as the ordinary `TABLE`-expectation parse error. Independent
3945 /// of [`access_control`](AccessControlSyntax::access_control) (GRANT/REVOKE): a dialect can
3946 /// admit privilege statements without the account-management DDL, and vice versa.
3947 pub user_role_management: bool,
3948 /// Dispatch `GRANT`/`REVOKE` through the MySQL account-based grammar rather than the
3949 /// standard/PostgreSQL one (the dependency is
3950 /// [`FeatureDependencyViolation::AccountGrantsWithoutAccessControl`]): the object is a
3951 /// `priv_level` (`*`, `*.*`, `db.*`, `db.tbl`) rather than a typed object with a name list,
3952 /// every grantee/role is a `user@host` account rather than a role spec, and the grammar adds
3953 /// `PROXY` grants, the `AS <user> [WITH ROLE …]` grantor context, the
3954 /// `[IF EXISTS] … [IGNORE UNKNOWN USER]` `REVOKE` guards, and the `REVOKE ALL PRIVILEGES,
3955 /// GRANT OPTION` form — while dropping `GRANTED BY`, `CASCADE`/`RESTRICT`, and the
3956 /// `{GRANT | ADMIN} OPTION FOR` prefix (all engine-measured `ER_PARSE_ERROR` on mysql:8.4.10).
3957 ///
3958 /// On for MySQL only. It is a *route*, not an additive layer: the MySQL `priv_level`/account
3959 /// object and grantee grammar structurally conflicts with the PostgreSQL typed-object/role-spec
3960 /// grammar (one input, one AST — they cannot both be represented), so a dialect cannot enable
3961 /// both grant grammars at once — enabling this route alongside
3962 /// [`access_control_extended_objects`](Self::access_control_extended_objects) is the registered
3963 /// [`GrammarConflict::AccountGrantsVersusExtendedObjects`], the route deadening the
3964 /// extended-object reading. That is why the Lenient permissive superset keeps this *off* —
3965 /// it retains the richer PostgreSQL-extended grant grammar (schema objects, `GRANTED BY`,
3966 /// `CASCADE`, routine signatures) that
3967 /// [`access_control_extended_objects`](Self::access_control_extended_objects) governs, which a
3968 /// route to the MySQL grammar would forfeit. Requires
3969 /// [`access_control`](Self::access_control): the MySQL forms are still `GRANT`/`REVOKE`
3970 /// statements, unreachable without the base dispatch.
3971 pub access_control_account_grants: bool,
3972}
3973
3974/// Dialect-owned type-name vocabulary extensions accepted by the parser.
3975///
3976/// The standard/PostgreSQL scalar type names are always recognized; these flags
3977/// gate type-name surfaces which the shared vocabulary does not cover.
3978/// Each is a recognition gate, not a parser-side dialect check: when off,
3979/// a name like `TINYINT` is not matched as a built-in and falls through to the
3980/// user-defined-type path (so ANSI/PostgreSQL read it as an ordinary type name),
3981/// while a structural form like `ENUM('a','b')` surfaces as a clean parse error
3982/// (its value list is not a numeric type modifier). The modelling — new
3983/// [`DataType`](crate::ast::DataType) variants, spelling tags, and value-list/wrapper
3984/// shapes — lives with the AST; this struct only decides *which dialects recognize it*.
3985#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3986pub struct TypeNameSyntax {
3987 /// Recognize extended scalar type names: the `TINYINT`/`MEDIUMINT`
3988 /// integer widths, bare `DOUBLE`, `DATETIME`, the `TINYTEXT`/`MEDIUMTEXT`/
3989 /// `LONGTEXT` character-LOB family, and the `TINYBLOB`/`BLOB`/`MEDIUMBLOB`/
3990 /// `LONGBLOB` binary-LOB family.
3991 pub extended_scalar_type_names: bool,
3992 /// Recognize the `ENUM(...)` value-list type in data-type position — MySQL's column
3993 /// type and DuckDB's `x::ENUM('a', 'b')` cast target (which rides the same
3994 /// [`DataType::Enum`](crate::ast::DataType) shape; DuckDB's *`CREATE TYPE ... AS ENUM`*
3995 /// statement uses a separate dedicated production, see
3996 /// [`CreateTypeDefinition`](crate::ast::CreateTypeDefinition)). Split from
3997 /// [`set_type`](Self::set_type) because DuckDB has `ENUM` but no `SET` type
3998 /// (`x::SET('a','b')` is an unknown-type error there, not a value-list type).
3999 pub enum_type: bool,
4000 /// Recognize the `SET(...)` value-list type in data-type position (MySQL only). Kept
4001 /// distinct from [`enum_type`](Self::enum_type): the two share one value-list shape but
4002 /// `SET` is MySQL-specific, so DuckDB enables only `ENUM`.
4003 pub set_type: bool,
4004 /// Recognize the `SIGNED`/`UNSIGNED`/`ZEROFILL` numeric modifiers (as a postfix
4005 /// on a numeric type) and the standalone `SIGNED`/`UNSIGNED` integer cast
4006 /// targets, e.g. `CAST(x AS UNSIGNED)` (MySQL).
4007 pub numeric_modifiers: bool,
4008 /// Recognize an optional display width `(M)` on a built-in integer type name —
4009 /// `INT(11)`, `TINYINT(1)`, `BIGINT(20)` — stored on the integer
4010 /// [`DataType`](crate::ast::DataType) variant's `display_width` field. Canonical
4011 /// to MySQL (deprecated in 8.0.17+ but ubiquitous in dumps); SQLite accepts it
4012 /// through affinity type-name absorption. When off, the trailing `(` on a
4013 /// built-in integer is not consumed and surfaces as a clean parse error, so
4014 /// ANSI/PostgreSQL reject `INT(11)` (verified against `pg_query`). Independent of
4015 /// [`extended_scalar_type_names`](Self::extended_scalar_type_names) (SQLite wants the width
4016 /// without the `TINYINT`/`MEDIUMINT` scalar names) and of
4017 /// [`numeric_modifiers`](Self::numeric_modifiers) (`(M)` is a prefix arg on the
4018 /// type name; `UNSIGNED`/`ZEROFILL` are a separate postfix).
4019 pub integer_display_width: bool,
4020 /// Recognize DuckDB's anonymous composite / nested type constructors in type
4021 /// position: `STRUCT(a INT, ...)` and the standard `ROW(...)` spelling of the same
4022 /// shape, the tagged `UNION(tag T, ...)`, and `MAP(K, V)`. A grammar-position gate
4023 /// keyed on the keyword immediately followed by `(`; when off, the leading word falls
4024 /// through to the user-defined-type path (a bare `struct`/`map` name still resolves),
4025 /// so ANSI/PostgreSQL reject the anonymous form — PostgreSQL has only *named*
4026 /// composite types and spells `ROW` as a value constructor, never a type (verified
4027 /// against a live server: `x::STRUCT(a int)` / `x::ROW(a int)` / `x::MAP(int,text)`
4028 /// each syntax-error). On for DuckDb/Lenient. The array-type suffixes (`T[]`/`T[n]`/
4029 /// `T ARRAY[n]`) are *not* gated here — PostgreSQL accepts them too — they ride the
4030 /// always-parsed array-suffix grammar.
4031 pub composite_types: bool,
4032 /// Accept BigQuery angle-bracket type forms `STRUCT<field TYPE, …>` and `ARRAY<T>`
4033 /// in type position (`CAST(x AS STRUCT<a INT64>)`, column definitions). On for
4034 /// BigQuery/Lenient. Distinct from [`composite_types`](Self::composite_types)
4035 /// (DuckDB paren form `STRUCT(a INT)`) and from expression-position
4036 /// [`struct_constructor`](crate::dialect::ExpressionSyntax::struct_constructor).
4037 pub angle_bracket_types: bool,
4038 /// Require an explicit length parameter on `VARCHAR` and `VARBINARY`
4039 /// (`VARCHAR(255)`, `VARBINARY(16)`). On for MySQL, whose `VARCHAR`/`VARBINARY` are a
4040 /// syntax error without a length (`CREATE TABLE t (a VARCHAR)` is an `ER_PARSE_ERROR` on
4041 /// mysql:8, while the fixed-width `CHAR`/`BINARY` default to length 1 and stay valid).
4042 /// Off for ANSI/PostgreSQL/SQLite/DuckDB/Lenient, where a length-less `VARCHAR` is
4043 /// accepted; when off the missing size is simply left `None`.
4044 pub varchar_requires_length: bool,
4045 /// Accept time-zone-aware temporal type names — `TIMESTAMPTZ` / `TIMESTAMP WITH TIME
4046 /// ZONE`, `TIMETZ` / `TIME WITH TIME ZONE`. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient.
4047 /// MySQL has no zoned temporal type (its `TIMESTAMP` stores UTC but the *type* carries no
4048 /// zone qualifier), so `TIMESTAMPTZ` and the `WITH TIME ZONE` spellings are an
4049 /// `ER_PARSE_ERROR` on mysql:8; it is off there and a parsed temporal type carrying a
4050 /// zone qualifier is rejected. The zone-less `TIMESTAMP`/`TIME`/`DATETIME` forms are
4051 /// unaffected.
4052 pub zoned_temporal_types: bool,
4053 /// Accept an EMPTY type-parameter parenthesis list on the `DECIMAL`/`DEC`/`NUMERIC`
4054 /// type names — `DECIMAL()`, `DEC()`, `NUMERIC()` — meaning the default precision/scale.
4055 /// DuckDB normalizes `DECIMAL()` to `DECIMAL(18,3)`, byte-identical to a bare `DECIMAL`
4056 /// (probed on 1.5.4: `typeof(x::DECIMAL())` == `typeof(x::DECIMAL)` == `DECIMAL(18,3)`),
4057 /// so the empty form carries no information and folds onto the same `precision: None,
4058 /// scale: None` [`DataType::Decimal`](crate::ast::DataType) shape — the canonical render
4059 /// drops the parens (an ADR-0011 spelling trade; the verbatim `()` survives on the node
4060 /// span). On for DuckDb/Lenient. Off elsewhere, where the empty `(` on a `DECIMAL` needs
4061 /// a precision and the missing modifier surfaces as a clean parse error (verified against
4062 /// `pg_query`: PostgreSQL rejects `DECIMAL()`).
4063 ///
4064 /// Scoped to the DECIMAL family (the surface the core-tranche corpus exercises). DuckDB
4065 /// also admits empty parens on its generic/user-resolved type names and a handful of
4066 /// dedicated built-ins (`DOUBLE()`, `TEXT()`, `DATE()`, `JSON()`, `TIMESTAMPTZ()`,
4067 /// `UUID()`, `HUGEINT()`, …) via the same `opt_type_modifiers` grammar, while the other
4068 /// hard-coded keyword types keep rejecting it (`VARCHAR()`/`TIMESTAMP()`/`FLOAT()` need a
4069 /// value; `INT()`/`REAL()`/`BOOLEAN()` admit no parens at all) — an asymmetric,
4070 /// separately-testable extension deferred (probe matrix on `duckdb-empty-type-parens`).
4071 pub empty_type_parens: bool,
4072 /// Recognize MySQL's character-set annotation on a char-family type — the grammar's
4073 /// `opt_charset_with_opt_binary` production: `CHARACTER SET <name>`, the `CHARSET`
4074 /// synonym, the `ASCII`/`UNICODE`/`BYTE` shortcuts, and/or the `BINARY` binary-collation
4075 /// modifier, in either order (`CHAR CHARACTER SET x BINARY`, `CHAR BINARY ASCII`). Stored
4076 /// on the [`DataType::Character`](crate::ast::DataType) node's `charset` field because it
4077 /// is part of the *type* — it must immediately follow the type and its length, and is an
4078 /// `ER_PARSE_ERROR` on mysql:8 once a column attribute intervenes (`CHAR(5) NOT NULL
4079 /// CHARACTER SET x`), unlike the free-floating `COLLATE` column attribute. Admitted in
4080 /// both the cast target (`CAST(x AS CHAR(5) CHARACTER SET utf8mb4)`) and column-definition
4081 /// positions that funnel through the shared type grammar, on the non-national spellings
4082 /// only (`CHAR`/`CHARACTER`/`VARCHAR`; the `NCHAR`/`NATIONAL` forms fix their own charset
4083 /// and reject it). On for MySQL/Lenient. Off elsewhere — PostgreSQL rejects `CHARACTER
4084 /// SET` in its modern grammar (verified against `pg_query`: `CHAR(5) CHARACTER SET utf8`
4085 /// is a syntax error), so when off the annotation keyword is left unconsumed and surfaces
4086 /// as a clean parse error.
4087 pub character_set_annotation: bool,
4088 /// Accept a leading sign on a `numeric`/`decimal` precision/scale type modifier
4089 /// (`numeric(5, -2)`, `numeric(-3, 6)`). PostgreSQL parses the modifier arguments as a
4090 /// general expression list at raw-parse time, so a signed integer is accepted and only
4091 /// validated later. On for PostgreSQL/Lenient. Off elsewhere (ANSI/MySQL/SQLite/DuckDB
4092 /// require an unsigned modifier), where the leading `-` on a modifier surfaces as a clean
4093 /// parse error.
4094 pub signed_type_modifier: bool,
4095 /// Recognize ClickHouse's `Nullable(T)` parametric type combinator in type position —
4096 /// the inner type extended with a `NULL` value, carried on the
4097 /// [`DataType::Wrapped`](crate::ast::DataType) shape. A grammar-position gate keyed on
4098 /// the keyword immediately followed by `(` (the composite-type precedent), so a bare
4099 /// `Nullable` with no `(` stays an ordinary type/column name and, when off, the whole
4100 /// `Nullable(...)` head falls through to the user-defined-type path. The inner type is a
4101 /// full recursive type, so `Nullable(DECIMAL(10, 2))` / `Nullable(String)[]` parse;
4102 /// ClickHouse's `Nullable(Nullable(T))` / `Nullable(Array(T))` composability rejects are
4103 /// a bind-time `DB::Exception`, not a grammar error, so they parse-accept here.
4104 ///
4105 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
4106 /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
4107 /// `composite_types` / `format_clause` precedent): off for every oracle-compared preset,
4108 /// which parse-reject `Nullable(...)` (its head resolves to a user-defined type name whose
4109 /// `(String)` modifier list then fails to parse).
4110 pub nullable_type: bool,
4111 /// Recognize ClickHouse's `LowCardinality(T)` parametric type combinator in type
4112 /// position — a dictionary-encoding wrapper transparent to query semantics, carried on
4113 /// the [`DataType::Wrapped`](crate::ast::DataType) shape (the same single-inner-type
4114 /// wrapper as `nullable_type`, its own flag per one-behaviour-one-flag). A
4115 /// grammar-position gate keyed on the keyword immediately followed by `(`, so a bare
4116 /// `LowCardinality` with no `(` stays an ordinary type/column name and, when off, the
4117 /// whole `LowCardinality(...)` head falls through to the user-defined-type path. The
4118 /// inner type is a full recursive type, so the canonical `LowCardinality(Nullable(String))`
4119 /// composition and `LowCardinality(DECIMAL(10, 2))` parse; ClickHouse constrains which
4120 /// inner `T` is valid at type resolution (a bind-time `DB::Exception`, not a grammar
4121 /// error), so any single inner type parse-accepts here.
4122 ///
4123 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
4124 /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
4125 /// `composite_types` / `nullable_type` precedent): off for every oracle-compared preset,
4126 /// which parse-reject `LowCardinality(...)` (its head resolves to a user-defined type
4127 /// name whose `(String)` modifier list then fails to parse).
4128 pub low_cardinality_type: bool,
4129 /// Recognize ClickHouse's `FixedString(N)` type constructor in type position — a
4130 /// fixed-length byte string of exactly `N` bytes, carried on the
4131 /// [`DataType::FixedString`](crate::ast::DataType) shape. Unlike the `Nullable`/
4132 /// `LowCardinality` wrappers its argument is a scalar length, not an inner type, so it
4133 /// is its own variant, not a [`WrappedTypeKind`](crate::ast::WrappedTypeKind) arm; its
4134 /// own flag per one-behaviour-one-flag. A grammar-position gate keyed on the keyword
4135 /// immediately followed by `(` (the composite/wrapper precedent), so a bare
4136 /// `FixedString` with no `(` stays an ordinary type/column name and, when off, the whole
4137 /// `FixedString(...)` head falls through to the user-defined-type path. `N` is mandatory
4138 /// (a bare `FixedString` is an invalid ClickHouse spelling) and parsed as any `u32`
4139 /// literal; ClickHouse's positive-length requirement (`FixedString(0)` reject) is a
4140 /// bind-time `DB::Exception`, not a grammar error, so it parse-accepts here.
4141 ///
4142 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
4143 /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
4144 /// `nullable_type` / `low_cardinality_type` precedent): off for every oracle-compared
4145 /// preset, which parse-reject `FixedString(...)` (its head resolves to a user-defined
4146 /// type name whose `(N)` modifier list then fails to parse).
4147 pub fixed_string_type: bool,
4148 /// Recognize ClickHouse's `DateTime64(P[, 'timezone'])` type constructor in type
4149 /// position — a sub-second timestamp carried on the
4150 /// [`DataType::DateTime64`](crate::ast::DataType) shape. Its own flag per
4151 /// one-behaviour-one-flag; like [`fixed_string_type`](Self::fixed_string_type) its
4152 /// leading argument is a mandatory scalar (the precision `P`), not an inner type, so it
4153 /// is a dedicated variant, not a [`WrappedTypeKind`](crate::ast::WrappedTypeKind) arm.
4154 /// The optional second argument is a single-quoted time-zone string literal, not the
4155 /// ANSI `WITH TIME ZONE` flag. A grammar-position gate keyed on the keyword immediately
4156 /// followed by `(` (the composite/wrapper precedent), so a bare `DateTime64` with no `(`
4157 /// stays an ordinary type/column name and, when off, the whole `DateTime64(...)` head
4158 /// falls through to the user-defined-type path. `P` is parsed as any `u32` literal;
4159 /// ClickHouse's documented `0..=9` range is a bind-time reject, not a grammar error, so
4160 /// it parse-accepts here.
4161 ///
4162 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
4163 /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
4164 /// `fixed_string_type` precedent): off for every oracle-compared preset. Off-gate the
4165 /// boundary is asymmetric — `DateTime64(3)` still parse-accepts as a user-defined type
4166 /// name with a `(3)` numeric-modifier list, but `DateTime64(3, 'UTC')` parse-*rejects*,
4167 /// because the string second argument does not fit the `u32`-only modifier grammar.
4168 pub datetime64_type: bool,
4169 /// Recognize ClickHouse's `Nested(name1 Type1, name2 Type2, ...)` named-field composite
4170 /// type in type position — a repeated group carried on the
4171 /// [`DataType::Nested`](crate::ast::DataType) shape (the named-field
4172 /// [`StructTypeField`](crate::ast::StructTypeField) list of `composite_types`, but a
4173 /// distinct variant and its own flag per one-behaviour-one-flag: `Nested` round-trips as
4174 /// `Nested`, never `STRUCT`, and its semantics are a repeated structure, not a product).
4175 /// A grammar-position gate keyed on the keyword immediately followed by `(` (the
4176 /// composite/wrapper precedent), so a bare `Nested` with no `(` stays an ordinary
4177 /// type/column name and, when off, the whole `Nested(...)` head falls through to the
4178 /// user-defined-type path. A field type is a full recursive type, so `Nested(x
4179 /// Nested(...))` parses; ClickHouse's nesting-level limit is a `flatten_nested` setting /
4180 /// bind concern, not a grammar error, so it parse-accepts here.
4181 ///
4182 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
4183 /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
4184 /// `datetime64_type` precedent): off for every oracle-compared preset, which parse-reject
4185 /// `Nested(a UInt8)` — its head resolves to a user-defined type name whose modifier list
4186 /// is `u32`-only, so the two-word `a UInt8` field has no grammar to fit (the wrapper
4187 /// off-gate reject, not the asymmetric `DateTime64(3)` accept).
4188 pub nested_type: bool,
4189 /// Recognize ClickHouse's fixed-bit-width integer type names — the signed
4190 /// `Int8`/`Int16`/`Int32`/`Int64`/`Int128`/`Int256` family and their unsigned
4191 /// `UInt8`…`UInt256` siblings — carried on the
4192 /// [`DataType::FixedWidthInt`](crate::ast::DataType) shape. One flag for the whole
4193 /// bit-width family: the names always travel together in a dialect, exactly as MySQL's
4194 /// `TINYINT`/`MEDIUMINT`/… ride one [`extended_scalar_type_names`](Self::extended_scalar_type_names)
4195 /// gate, so this is one behaviour, not one flag per width. Unlike the
4196 /// `Nullable`/`FixedString` constructors these names take no arguments, so recognition is a
4197 /// bare-name gate (the `TINYINT`/`extended_scalar` precedent, not the keyword-then-`(`
4198 /// lookahead): when off, a bare `Int256` simply falls through to the user-defined-type path
4199 /// (its trivial off-gate boundary, like a bare `Nullable`).
4200 ///
4201 /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so the
4202 /// no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the `nullable_type`
4203 /// / `fixed_string_type` / `datetime64_type` precedent): off for every oracle-compared
4204 /// preset, which read `Int256` as an ordinary user-defined type name.
4205 pub bit_width_integer_names: bool,
4206 /// Widen the type-name grammar to SQLite's liberal affinity form: a column/cast type is
4207 /// any run of one-or-more space-separated words (`UNSIGNED BIG INT`, `LONG INTEGER`, the
4208 /// misspelled `INTEGEB PRIMARI KEY`) with an optional *two*-argument parenthesized
4209 /// modifier (`VARCHAR(123,456)`, `FLOATING POINT(5,10)`), carried on the
4210 /// [`DataType::Liberal`](crate::ast::DataType) shape. SQLite has no closed type
4211 /// vocabulary — every declared type is affinity text — so its grammar's `typename`
4212 /// accepts an arbitrary `ids ...` token run terminated by a column-constraint keyword, a
4213 /// comma, or a close paren (engine-probed on rusqlite/sqlite3 3.53.2 & 3.43.2).
4214 ///
4215 /// A strict FALLBACK: the typed variants and the single-word user-defined path win
4216 /// wherever they can faithfully represent the input, so a bare `INT`, `DOUBLE PRECISION`,
4217 /// `VARCHAR(255)`, `NATIONAL CHARACTER(15)`, or single-word affinity `BANANA` keep their
4218 /// existing shapes with the flag on; only a trailing type-word or a two-argument paren
4219 /// list that no typed / user-defined parse can hold falls to `DataType::Liberal`. The
4220 /// word run terminates at a column-constraint keyword (`PRIMARY`/`NOT`/`NULL`/`UNIQUE`/
4221 /// `CHECK`/`DEFAULT`/`COLLATE`/`REFERENCES`/`CONSTRAINT`/`AS`/`GENERATED`), so
4222 /// `GENERATED ALWAYS AS` generated columns are unaffected.
4223 ///
4224 /// **On for SQLite and Lenient.** Off elsewhere, where a multi-word type name or a
4225 /// two-argument built-in modifier surfaces as a clean parse error (the standard/
4226 /// PostgreSQL/MySQL/DuckDB have a closed type vocabulary; `pg_query` rejects
4227 /// `LONG INTEGER` and `VARCHAR(123,456)`).
4228 pub liberal_type_names: bool,
4229 /// Admit a string-literal argument in a user-defined type name's modifier list —
4230 /// DuckDB's `GEOMETRY('OGC:CRS84')` coordinate-system annotation, and more generally
4231 /// any `type_name('constant', ...)` where DuckDB's grammar accepts constants (string or
4232 /// numeric) as type modifiers (engine-measured on DuckDB 1.5.4: `MYTYPE('abc')` reaches
4233 /// the binder — a parse-accept — while a non-constant like `(['abc'])` stays a parser
4234 /// error). The modifiers ride the [`DataType::UserDefined`](crate::ast::DataType)
4235 /// shape's `modifiers` list as [`Literal`](crate::ast::Literal)s.
4236 ///
4237 /// When off, only unsigned-integer modifiers parse (`FOO(3)`), so a string modifier
4238 /// surfaces as a clean parse error — the standard/PostgreSQL/MySQL user-type grammar
4239 /// admits no string modifier there. **On for DuckDB only.** Numeric modifiers parse
4240 /// under every dialect regardless of this flag; it gates *only* the string form.
4241 pub string_type_modifiers: bool,
4242}
4243
4244/// Dialect-owned expression *postfix and constructor* syntax accepted by the parser.
4245///
4246/// The postfix operators that navigate a value and the constructor / typed-literal forms
4247/// that build one, each gated by dialect data: a flag decides whether the parser *admits*
4248/// the form, and when off the leading punctuation/keyword surfaces as a clean parse error.
4249/// The infix/prefix operator spellings and the function-call-tail forms
4250/// split out into [`OperatorSyntax`] and [`CallSyntax`] once this struct crossed its
4251/// documented 16-field line; this half keeps only the shapes that build or navigate a
4252/// single expression value. Most gates are over lexemes that always tokenize (a lone `:`
4253/// and `::` are structural punctuation); a flag whose form needs a dialect-gated *lexeme*
4254/// says "also gates the tokenizer" in its first paragraph.
4255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4256pub struct ExpressionSyntax {
4257 /// Accept the `expr::type` typecast operator.
4258 pub typecast_operator: bool,
4259 /// Accept `base[index]` element access and `base[lower:upper]` slicing.
4260 pub subscript: bool,
4261 /// Accept DuckDB's three-bound slice `base[lower:upper:step]` on top of the two-bound
4262 /// slice [`subscript`](Self::subscript) admits. A pure parser gate (the `:` separators
4263 /// and the `-` placeholder always tokenize): reachable only once
4264 /// [`subscript`](Self::subscript) has opened the brackets (the dependency is
4265 /// [`FeatureDependencyViolation::SliceStepWithoutSubscript`]), it admits the second `:` and,
4266 /// as the middle bound, DuckDB's bare `-`
4267 /// open-upper placeholder (`base[lower:-:step]`) — an empty middle `base[lower::step]`
4268 /// stays a parse error. On for DuckDB only; PostgreSQL slices are two-bound, so it keeps
4269 /// this off despite `subscript`, and a `base[a:b:c]` there is a clean parse error at the
4270 /// second `:`.
4271 pub slice_step: bool,
4272 /// Accept `expr COLLATE collation`.
4273 pub collate: bool,
4274 /// Accept `expr AT TIME ZONE zone`.
4275 pub at_time_zone: bool,
4276 /// Accept semi-structured value paths written as `base:key[0].field`.
4277 ///
4278 /// A postfix grammar gate over `:` followed by an identifier-like key, then optional
4279 /// `.` and `[...]` path suffixes. It contends with
4280 /// [`ParameterSyntax::named_colon`] on the same `:`+identifier trigger, so enabling
4281 /// both is a [`LexicalConflict::ColonParameterVersusSliceBound`] conflict: the scanner
4282 /// would otherwise turn `:key` into a parameter before the postfix parser could read
4283 /// the path. It also contends one layer up, at the *grammar* head, with
4284 /// [`SelectSyntax::prefix_colon_alias`] — whose alias-before-value form reads the same
4285 /// leading `<ident> :` — so enabling both is a
4286 /// [`GrammarConflict::PrefixColonAliasVersusSemiStructuredAccess`] conflict (no shipped
4287 /// preset pairs them).
4288 pub semi_structured_access: bool,
4289 /// Accept the `ARRAY[...]` / `ARRAY(<query>)` array constructors.
4290 pub array_constructor: bool,
4291 /// Accept PostgreSQL multidimensional array literals — a bare-bracket sub-row
4292 /// `[...]` as an element inside an [`array_constructor`](Self::array_constructor),
4293 /// as in `ARRAY[[1,2],[3,4]]` and deeper nestings.
4294 ///
4295 /// A pure grammar gate on the array-constructor element position (its dependency on
4296 /// [`array_constructor`](Self::array_constructor) is
4297 /// [`FeatureDependencyViolation::MultidimArrayLiteralsWithoutArrayConstructor`]), independent of
4298 /// [`collection_literals`](Self::collection_literals): the bare-bracket row is only
4299 /// a value in an array context (a top-level `[1,2]` is still rejected under
4300 /// PostgreSQL), and PostgreSQL enforces that each bracket level is uniform — every
4301 /// element is a sub-row or every element is a scalar, never a mix
4302 /// (`ARRAY[[1,2],3]`, `ARRAY[1,[2,3]]`, and `ARRAY[[1,2],ARRAY[3,4]]` are parse
4303 /// errors, matching PG's `expr_list` / `array_expr_list` split). Ragged nestings
4304 /// (`ARRAY[[1,2],[3]]`) parse-accept — PostgreSQL rejects them at bind time, not in
4305 /// the grammar. A sub-row is represented as an ordinary bracket-spelled
4306 /// [`ArrayExpr::Elements`](crate::ast::ArrayExpr), so it renders and shapes exactly
4307 /// like a DuckDB list level. DuckDB reaches the same surface through
4308 /// [`collection_literals`](Self::collection_literals) (a top-level list *is* a value
4309 /// there, and levels may mix), so it keeps this off and overrides the POSTGRES
4310 /// spread.
4311 pub multidim_array_literals: bool,
4312 /// Accept the DuckDB collection literals: the bare-bracket list `[a, b, …]`, the
4313 /// struct `{'k': v, …}`, and the map `MAP {k: v, …}` / `MAP(<keys>, <values>)`.
4314 ///
4315 /// A pure grammar/lexical gate on the primary-expression `[`, `{`, and `MAP`
4316 /// leads. The bracket list contends for the same `[` trigger as an
4317 /// [`identifier_quotes`](FeatureSet::identifier_quotes) style opening with `[`
4318 /// (T-SQL/SQLite/Lenient bracket identifiers), so enabling both is the
4319 /// [`LexicalConflict::BracketIdentifierVersusArraySyntax`] conflict — a dialect
4320 /// picks bracket identifiers *or* `[` collection/array syntax. The `key: value`
4321 /// separator likewise contends with [`ParameterSyntax::named_colon`] on the
4322 /// `:`+identifier trigger
4323 /// ([`LexicalConflict::ColonParameterVersusSliceBound`], shared with the slice
4324 /// bound). When off, `[`/`{` in primary position surface as a clean parse error
4325 /// (`MAP` falls back to an ordinary name).
4326 pub collection_literals: bool,
4327 /// Accept explicit `ROW(...)` and implicit `(a, b, …)` row constructors.
4328 pub row_constructor: bool,
4329 /// Accept BigQuery's `STRUCT(...)` value constructor — the typeless `STRUCT(1, 2)`
4330 /// and named `STRUCT(x AS a, y AS b)` forms and the typed
4331 /// `STRUCT<a INT64, b STRING>(1, 'x')` form — on the canonical
4332 /// [`StructConstructorExpr`](crate::ast::StructConstructorExpr) shape.
4333 ///
4334 /// A pure parser lookahead gate, the sibling of
4335 /// [`row_constructor`](Self::row_constructor)/[`array_constructor`](Self::array_constructor):
4336 /// the (contextual, non-reserved) `STRUCT` word opens the constructor only when
4337 /// immediately followed by `(` (typeless) or `<` (typed), mirroring the `ROW(`/`ARRAY[`
4338 /// disambiguation — the tokenizer is untouched, and the `<` opener is committed on a
4339 /// single-token lookahead (no rewind), sound because in a preset that admits this form
4340 /// `STRUCT` is not a bare column so `struct < x` is not a competing comparison. When
4341 /// off, `STRUCT(...)` is left to the ordinary call path and stays an
4342 /// [`Expr::Function`](crate::ast::Expr::Function) catalog-function call — the
4343 /// non-interference boundary every non-BigQuery preset (PostgreSQL included) keeps.
4344 ///
4345 /// **On for BigQuery and Lenient.** BigQuery documents the form and has no differential
4346 /// oracle here (self-consistency + gate-off rejection are the tests); Lenient unions it
4347 /// in. Off for every other preset: DuckDB builds structs with the `{...}` literal /
4348 /// `struct_pack()` / `row()` rather than a `STRUCT(...)` keyword form, and the
4349 /// Spark-family `struct(...)` builtin is not verifiable without an engine, so those
4350 /// presets keep `struct(...)` an ordinary call rather than risk reshaping it. The typed
4351 /// field list here is parsed inline; a bare `STRUCT<...>` in *type* position
4352 /// (`CAST(x AS STRUCT<...>)`) is a separate type-name surface not gated by this flag.
4353 pub struct_constructor: bool,
4354 /// Accept `(expr).field` composite field selection.
4355 pub field_selection: bool,
4356 /// Accept the `.*` composite/whole-row star selector in a *value* position: the
4357 /// composite expansion `(expr).*` off a parenthesized primary, and a whole-row
4358 /// `tbl.*` used as a value (inside a `ROW(...)` field, a function argument, a
4359 /// comparison, or a `tbl.*::type` cast) rather than as a select-list projection
4360 /// target. PostgreSQL admits this `.*` indirection wherever an `a_expr` is, at the
4361 /// same tight precedence as [`field_selection`](Self::field_selection) (so
4362 /// `(a).* + 1` groups as `((a).*) + 1`); engine-probed on pg_query PG-17. Gated
4363 /// apart from `field_selection` because DuckDB parse-accepts `(struct).field` but
4364 /// parse-rejects every `.*` value expansion (engine-probed on DuckDB 1.5.4), so it
4365 /// is off there and on for PostgreSQL/Lenient. When off, a `.` followed by `*` is
4366 /// left unconsumed and surfaces as the usual downstream parse error. This governs
4367 /// only the *value*-position star; a plain select-list/`RETURNING` `tbl.*` remains
4368 /// a [`SelectItem::QualifiedWildcard`](crate::ast::SelectItem::QualifiedWildcard)
4369 /// under every preset.
4370 pub field_wildcard: bool,
4371 /// Accept the prefix-typed string literal — a type name whose reading becomes a
4372 /// literal when a string constant follows the type prefix: the standard temporal
4373 /// forms `DATE '…'` / `TIME '…'` / `TIMESTAMP '…'` and the PostgreSQL generalized
4374 /// `<type> '…'` (`float8 '1.5'`, folded onto a
4375 /// [`CastSyntax::PrefixTyped`](crate::ast::CastSyntax) cast). One flag covers both,
4376 /// because the two travel together — a dialect with the temporal forms has the
4377 /// generalized one. On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has neither
4378 /// (`date '…'` juxtaposes a name and a string, a clean parse error there), so it is
4379 /// off; the type keyword then falls back to its ordinary column/function reading and
4380 /// the trailing string surfaces as the usual parse error.
4381 ///
4382 /// The prefix-typed `INTERVAL '…' <fields>` literal is split onto its own
4383 /// [`typed_interval_literal`](Self::typed_interval_literal) refinement, because MySQL
4384 /// has the other temporal literals but no first-class interval literal.
4385 pub typed_string_literals: bool,
4386 /// Arm the prefix-typed `INTERVAL '<amount>' <fields>` literal — the ANSI/PostgreSQL
4387 /// interval literal that folds onto
4388 /// [`LiteralKind::Interval`](crate::ast::LiteralKind::Interval), including the ANSI
4389 /// `HOUR TO SECOND` composite and the `SECOND(p)` unit precision. A refinement of
4390 /// [`typed_string_literals`](Self::typed_string_literals) (the interval literal is only
4391 /// reached when that is on), split out because MySQL admits the other prefix-typed
4392 /// temporal literals (`DATE`/`TIME`/`TIMESTAMP`) yet has no interval literal at all:
4393 /// every typed `INTERVAL '…'` form — standalone *or* in a `+`/`-` operand — is
4394 /// `ER_PARSE_ERROR` (1064) on mysql:8.4.10 (engine-measured), the only valid MySQL
4395 /// interval being the operator-position `INTERVAL <expr> <unit>` modelled by
4396 /// [`mysql_interval_operator`](Self::mysql_interval_operator).
4397 ///
4398 /// On for ANSI/PostgreSQL/DuckDB/Lenient. Off for MySQL (the operator reader owns its
4399 /// valid unit-bearing forms; every form that reaches this literal path there — the ANSI
4400 /// `TO`/precision spellings the operator reader declines, and the unit-less
4401 /// `INTERVAL '1'` — is one MySQL rejects, so the literal path declines and the
4402 /// `INTERVAL` keyword falls back to its ordinary reading, a parse error on the trailing
4403 /// string). Off for SQLite, where [`typed_string_literals`](Self::typed_string_literals)
4404 /// is already off and no prefix-typed literal is reached.
4405 pub typed_interval_literal: bool,
4406 /// Accept DuckDB's relaxed `INTERVAL` literal spellings on top of the standard
4407 /// quoted `INTERVAL '1' DAY` form: an unquoted integer amount (`INTERVAL 3 DAY`),
4408 /// a parenthesized-expression amount (`INTERVAL (days) DAY`), and plural unit
4409 /// spellings (`INTERVAL 3 DAYS`, `INTERVAL '1' hours`). All three fold onto the one
4410 /// [`LiteralKind::Interval`](crate::ast::LiteralKind::Interval) shape: the
4411 /// amount round-trips from the literal's span exactly as the quoted string
4412 /// does, so only the unit qualifier lands on the tag, and a plural unit folds
4413 /// onto its singular [`IntervalFields`](crate::ast::IntervalFields) — the plural `s`
4414 /// round-trips from the span (the documented spelling trade). The unquoted/parenthesized
4415 /// amount forms require a trailing unit (a bare `INTERVAL 3` is a DuckDB *binding*
4416 /// error, not a syntax one). On for DuckDB and Lenient; off elsewhere, where the
4417 /// non-standard spellings surface as the usual parse error.
4418 pub relaxed_interval_syntax: bool,
4419 /// Accept the MySQL operator-position interval quantity `INTERVAL <expr> <unit>` —
4420 /// the `INTERVAL 3 DAY` operand of MySQL date arithmetic — as an
4421 /// [`Expr::Interval`](crate::ast::Expr::Interval) node.
4422 ///
4423 /// A behaviour distinct from the ANSI/PostgreSQL/DuckDB typed-string interval *literal*
4424 /// ([`typed_string_literals`](Self::typed_string_literals) /
4425 /// [`relaxed_interval_syntax`](Self::relaxed_interval_syntax), both folding onto
4426 /// [`LiteralKind::Interval`](crate::ast::LiteralKind::Interval)): MySQL's `INTERVAL` is
4427 /// not a first-class value but the second argument of the `Item_date_add_interval`
4428 /// production, so it carries an arbitrary amount *expression* (integer, decimal, string,
4429 /// `?`, `@var`, `n + 1`, `(expr)`, negative) and a **mandatory** unit keyword drawn from
4430 /// MySQL's underscore vocabulary — the simple units `MICROSECOND`…`YEAR` (plus `WEEK`,
4431 /// `QUARTER`) and the composites `SECOND_MICROSECOND`, `MINUTE_SECOND`, `DAY_HOUR`,
4432 /// `YEAR_MONTH`, … (engine-measured on mysql:8.4.10). It admits **no** ANSI `TO`
4433 /// composite and **no** `(p)` unit precision (`INTERVAL '1' HOUR TO SECOND` and
4434 /// `INTERVAL '1' SECOND(3)` are `ER_PARSE_ERROR` there); a `TO`/`(` after the unit makes
4435 /// the operator reader decline so the typed-string interval literal path
4436 /// ([`typed_interval_literal`](Self::typed_interval_literal)) owns those spellings under
4437 /// Lenient/PostgreSQL. Under MySQL that literal path is off, so the declined ANSI
4438 /// spellings reject (matching the engine).
4439 ///
4440 /// **On for MySQL and Lenient.** When on, an `INTERVAL` in expression-prefix position is
4441 /// read as this node before the typed-string literal path (MySQL has no first-class
4442 /// interval literal); a form that is not a valid MySQL operator interval — unit-less, an
4443 /// ANSI `TO`/precision spelling, a bare `INTERVAL(a, b)` index function — rewinds and
4444 /// falls through, so under Lenient the DuckDB relaxed amounts, plural units, unit-less
4445 /// PostgreSQL literals, and the ANSI `TO`/precision interval literals still parse (under
4446 /// MySQL they reject, [`typed_interval_literal`](Self::typed_interval_literal) being off).
4447 /// The node is modelled as a primary (highest binding power), so MySQL's *position*
4448 /// restriction is deliberately not enforced: a standalone `SELECT INTERVAL 3 DAY`, a
4449 /// leading `INTERVAL 3 DAY - x` (only `+` leads on mysql:8), and `INTERVAL 3 DAY IS NULL`
4450 /// over-accept rather than over-reject the valid operand/`DATE_ADD`/frame positions a
4451 /// general grammar cannot distinguish. Off elsewhere, where `INTERVAL` keeps its
4452 /// literal-or-column reading.
4453 pub mysql_interval_operator: bool,
4454 /// Accept DuckDB's `#n` positional column reference — a select-list column named by
4455 /// its 1-based output position ([`Expr::PositionalColumn`](crate::ast::Expr::PositionalColumn)),
4456 /// used mainly in `ORDER BY #1` / `GROUP BY #2` but valid wherever a value expression
4457 /// is. Also gates the tokenizer: the `#<digits>` lexeme is scanned only under a
4458 /// dialect that sets this (DuckDB), so elsewhere `#` stays a stray byte, a MySQL line
4459 /// comment, or PostgreSQL's XOR operator per that dialect's data. Because it claims
4460 /// the `#` trigger, it is mutually exclusive with the two other `#` claimants — the
4461 /// [`hash_bitwise_xor`](FeatureSet::hash_bitwise_xor) XOR operator
4462 /// ([`LexicalConflict::HashXorOperatorVersusPositionalColumn`]) and a
4463 /// [`CommentSyntax::line_comment_hash`] line comment
4464 /// ([`LexicalConflict::HashCommentVersusPositionalColumn`]); a `#`-led identifier byte
4465 /// class instead resolves by scan order (the identifier scan precedes this arm), like
4466 /// the XOR case. On for DuckDB only. Lenient, which already commits `#` to a MySQL
4467 /// line comment, cannot also enable it. When off, `#1` surfaces per the dialect's
4468 /// other `#` reading (a clean parse error in ANSI/SQLite).
4469 pub positional_column: bool,
4470 /// Accept DuckDB's python-style keyword lambda `lambda x, y: body` — a prefix
4471 /// production, distinct from the [`OperatorSyntax::lambda_expressions`] single-arrow
4472 /// form and folded onto the same [`Expr::Lambda`](crate::ast::Expr::Lambda) node with
4473 /// a [`LambdaParamSpelling::Keyword`](crate::ast::LambdaParamSpelling) spelling tag.
4474 /// DuckDB 1.3.0 introduced it and prefers it over the deprecated arrow; the two
4475 /// spellings are separate flags because DuckDB's roadmap keeps the keyword while
4476 /// dropping the arrow. When on, a `lambda` word in expression-prefix position opens
4477 /// the production unconditionally rather than reading as an ordinary column — matching
4478 /// DuckDB, which reserves `lambda` — so a bare `lambda`, or one not followed by
4479 /// `<params>:`, is a parse error. On for DuckDB and Lenient.
4480 pub lambda_keyword: bool,
4481}
4482
4483/// Dialect-owned infix/prefix *operator* syntax accepted by the parser.
4484///
4485/// The operator-acceptance and operator-spelling family, split out of [`ExpressionSyntax`]
4486/// when it crossed its 16-field line. Each flag decides whether the parser *admits* the
4487/// operator, while the binding powers that order them live in [`BindingPowerTable`]; a
4488/// spelling that folds onto an existing operator carries a spelling tag so it round-trips.
4489/// A flag whose form needs a dialect-gated *lexeme* says "also gates the tokenizer" in its
4490/// first paragraph — a flag crossing the lexer/parser boundary must declare it there.
4491#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4492pub struct OperatorSyntax {
4493 /// Accept the PostgreSQL explicit-operator infix form `a OPERATOR(schema.op) b`.
4494 pub operator_construct: bool,
4495 /// Accept PostgreSQL's containment operators — infix `@>` (contains) and `<@`
4496 /// (contained by). Also gates the tokenizer: the `@>`/`<@` lexemes are recognised
4497 /// only under a dialect that sets this (PostgreSQL), so elsewhere `@>` stays a stray
4498 /// `@` then `>`, and `<@` a `<` then a stray `@`. The `@>` munch requires the
4499 /// following `>`, so a bare `@` is always a stray byte here — the prefix `@`
4500 /// absolute-value operator is a scoped follow-up, since its bare-`@` lexeme contends
4501 /// with the T-SQL/MySQL `@name` sigils and needs a tracked conflict. The `<@` munch,
4502 /// by contrast, shadows an abutting `@name` sigil (`a<@x` meaning `a < @x`) whenever
4503 /// this is on together with a single-`@` form — the tracked
4504 /// [`LexicalConflict::ContainmentOperatorVersusAtName`]. The operators bind at
4505 /// PostgreSQL's "any other operator" precedence ([`BindingPowerTable::any_operator`]).
4506 ///
4507 /// [`BindingPowerTable::any_operator`]: crate::precedence::BindingPowerTable::any_operator
4508 pub containment_operators: bool,
4509 /// Accept PostgreSQL's JSON access operators — infix `->` (field/element as
4510 /// `json`) and `->>` (as `text`). Also gates the tokenizer: the `->`/`->>`
4511 /// lexemes are munched only under a dialect that sets this, so elsewhere `a->b`
4512 /// stays a `-` then a `>`. MySQL spells the same JSON accessors and can enable
4513 /// this independently of [`containment_operators`](Self::containment_operators),
4514 /// which is why the two are separate flags. Same "any other operator" precedence.
4515 pub json_arrow_operators: bool,
4516 /// Accept PostgreSQL's `jsonb` existence / path / search operators as one family: `?`
4517 /// (key exists), `?|` (any key exists), `?&` (all keys exist), `@?` (`jsonpath` returns
4518 /// any item), `@@` (`jsonpath` predicate match, also the `tsvector @@ tsquery` full-text
4519 /// match), `#>` (extract at path), `#>>` (extract at path as `text`), and `#-` (delete at
4520 /// path). Also gates the tokenizer: these lexemes are munched only under a dialect that
4521 /// sets this. One flag for the whole family (the family-level granularity of
4522 /// [`json_arrow_operators`](Self::json_arrow_operators) / [`containment_operators`](Self::containment_operators)):
4523 /// PostgreSQL enables the set as a unit and every other dialect leaves it off. All eight
4524 /// bind at the "any other operator" precedence ([`BindingPowerTable::any_operator`]),
4525 /// left-associative — engine-measured on pg_query (tighter than comparison, looser than
4526 /// additive). The operators fold onto the dedicated
4527 /// [`BinaryOperator`] `Json…` keys.
4528 ///
4529 /// Three lead bytes are shared triggers the tokenizer partitions by follow byte:
4530 /// - `?` is otherwise the anonymous placeholder ([`ParameterSyntax::anonymous_question`]);
4531 /// the two never co-enable in a shipped preset (PostgreSQL has no `?` parameter), and a
4532 /// feature set enabling both is the tracked
4533 /// [`LexicalConflict::JsonbKeyExistsVersusAnonymousParameter`].
4534 /// - `@@` is otherwise the MySQL system-variable sigil
4535 /// ([`SessionVariableSyntax::system_variables`]); the tracked
4536 /// [`LexicalConflict::JsonbSearchOperatorVersusSystemVariable`]. `@?` is disjoint from
4537 /// every other `@` claimant by its second byte, so it adds no conflict.
4538 /// - `#>`/`#>>`/`#-` ride the `#` byte, which reaches the operator scanner only under
4539 /// PostgreSQL's `#` bitwise-XOR ([`hash_bitwise_xor`](FeatureSet::hash_bitwise_xor), on together with this in the
4540 /// PostgreSQL preset); they are munched ahead of the bare `#` and stay disjoint from
4541 /// DuckDB's `#n` positional column (`#`+digit) by follow byte.
4542 ///
4543 /// The sibling regex/geometric/network operator surface builds on the same `?`/`@`/`#`
4544 /// lexeme foundation under its own flag(s): bare prefix `@` (absolute value) stays a
4545 /// stray byte here (the `@?`/`@@` munch requires a follow byte), left for that work.
4546 ///
4547 /// [`BindingPowerTable::any_operator`]: crate::precedence::BindingPowerTable::any_operator
4548 pub jsonb_operators: bool,
4549 /// Accept SQLite's `==` equality spelling as a synonym for `=`. Also gates the
4550 /// tokenizer: the doubled-`=` lexeme is munched to the equality operator only
4551 /// under a dialect that sets this, so elsewhere `a == b` stays `a` `=` `=` `b`
4552 /// and surfaces as a clean parse error. The two spellings fold onto the one
4553 /// canonical [`BinaryOperator::Eq`] operator; the
4554 /// [`EqualsSpelling`](crate::ast::EqualsSpelling) tag records
4555 /// which the source used so `==`/`=` round-trip exactly, the same pattern as
4556 /// `MOD`/`RLIKE`/`REGEXP`.
4557 pub double_equals: bool,
4558 /// Accept DuckDB's `//` integer-division spelling. Also gates the tokenizer: the
4559 /// doubled-`/` lexeme is munched to the integer-division operator only under a dialect
4560 /// that sets this, so elsewhere `a // b` stays `a` `/` `/` `b` and surfaces as a clean
4561 /// parse error. No shipped preset lexes `//` as a line comment, so the doubled munch
4562 /// never shadows a comment mode. The symbol folds onto the one canonical
4563 /// [`BinaryOperator::IntegerDivide`] operator; the [`IntegerDivideSpelling`]
4564 /// tag records the `//` spelling so it round-trips (this is load-bearing for validity,
4565 /// not only fidelity: DuckDB has no `DIV` keyword and MySQL no `//` operator, so the
4566 /// spelling cannot be normalized away). Distinct from MySQL's `DIV`, which rides
4567 /// [`KeywordOperators::MySql`].
4568 pub integer_divide_slash: bool,
4569 /// Accept DuckDB's `^@` "starts with" infix operator (`'hello' ^@ 'he'`). Also
4570 /// gates the tokenizer: `^@` is munched only when this is on; elsewhere `^` then
4571 /// `@` stay separate (and `@` is often a stray byte).
4572 pub starts_with_operator: bool,
4573 /// Accept SQLite's `IS` / `IS NOT` as a *general* null-safe equality over
4574 /// arbitrary operands (`1 IS 1`, `1 IS NOT 2`), not just `IS NULL` /
4575 /// `IS [NOT] DISTINCT FROM`. When on, an `IS [NOT] <expr>` whose right operand is
4576 /// neither `NULL` nor `DISTINCT FROM …` folds onto the existing null-safe
4577 /// [`IsNotDistinctFrom`](crate::ast::BinaryOperator::IsNotDistinctFrom) /
4578 /// [`IsDistinctFrom`](crate::ast::BinaryOperator::IsDistinctFrom) operators —
4579 /// SQLite's `IS` is exactly `IS NOT DISTINCT FROM`. When off (ANSI/PostgreSQL/
4580 /// MySQL), `IS` requires `NULL` or `DISTINCT FROM`, so `1 IS 1` is a clean parse
4581 /// error.
4582 pub is_general_equality: bool,
4583 /// Accept the SQL:2016 truth-value tests `<expr> IS [NOT] {TRUE | FALSE | UNKNOWN}`
4584 /// (F571), parsed to the postfix [`Expr::IsTruth`](crate::ast::Expr::IsTruth) predicate.
4585 /// On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient, all of which accept the three-valued
4586 /// `UNKNOWN` (engine-measured on pg_query, MySQL 8, DuckDB). Off for SQLite, whose `IS`
4587 /// is a general null-safe equality ([`is_general_equality`](Self::is_general_equality)):
4588 /// there `IS TRUE`/`IS FALSE` fold onto the boolean literal and `IS UNKNOWN` reads as
4589 /// equality against an identifier `unknown`, so SQLite has no truth-value predicate and
4590 /// this stays off (turning it on would over-accept `IS UNKNOWN`, which SQLite rejects
4591 /// unless `unknown` is a bound column). Checked ahead of the general-equality reading so
4592 /// that a dialect enabling both keeps the standard truth predicate.
4593 pub truth_value_tests: bool,
4594 /// Accept MySQL's `<=>` null-safe equality operator (`a <=> b` ≡
4595 /// `a IS NOT DISTINCT FROM b`). Also gates the tokenizer: the `<=>` lexeme is munched
4596 /// (ahead of `<=`) only under a dialect that sets this, so elsewhere `a <=> b` stays
4597 /// `a` `<=` `>` `b` and surfaces as a clean parse error. It folds onto the canonical
4598 /// [`BinaryOperator::IsNotDistinctFrom`] operator; the
4599 /// [`IsNotDistinctFromSpelling`](crate::ast::IsNotDistinctFromSpelling) tag records the
4600 /// `<=>` spelling so it round-trips (MySQL rejects the keyword `IS NOT DISTINCT FROM`,
4601 /// so the spelling cannot be normalized away). Binds at comparison precedence, riding
4602 /// the shared comparison row like the other comparison operators.
4603 pub null_safe_equals: bool,
4604 /// Read an infix `->` whose left operand is a lambda-parameter list — a bare
4605 /// unqualified name, a parenthesized name list `(x, y)`, or the equivalent
4606 /// `ROW(x, y)` — as a DuckDB single-arrow lambda
4607 /// ([`Expr::Lambda`](crate::ast::Expr::Lambda)) instead of the JSON-arrow binary
4608 /// operator; any other left operand keeps the
4609 /// [`JsonGet`](crate::ast::BinaryOperator::JsonGet) reading.
4610 ///
4611 /// A grammar-position gate over a token another flag lexes: the `->` lexeme is
4612 /// munched only under [`json_arrow_operators`](Self::json_arrow_operators), so
4613 /// this flag is inert without it (the dependency is
4614 /// [`FeatureDependencyViolation::LambdaExpressionsWithoutJsonArrowOperators`]; no lexical
4615 /// trigger of its own, hence no [`LexicalConflict`] entry — the lexical registry covers
4616 /// shared *tokenizer* triggers, this one covers grammar dependencies). The node split mirrors the
4617 /// engine exactly (probed on DuckDB 1.5.4): DuckDB parses *every* `->` as a
4618 /// `LAMBDA` tree node — even `1 -> 2` or `t.a -> 'k'` — and defers the
4619 /// lambda-vs-JSON decision to bind time, where a lambda-consuming argument
4620 /// requires exactly the parameter shape above ("Parameters must be unqualified
4621 /// comma-separated names like x or (x, y)") and any other `->` is re-read as JSON
4622 /// extraction. Applying that bind-time shape test at parse time is a pure
4623 /// node-label choice: lambda `->` and JSON `->` share one token, one binding
4624 /// power, and one associativity, so acceptance is unchanged either way and the
4625 /// text round-trips identically. Position-independent, like the engine: a lambda
4626 /// parses anywhere an expression does (`SELECT x -> x + 1` parses in DuckDB; only
4627 /// the *binder* rejects an unconsumed lambda). Multi-parameter lists ride the
4628 /// implicit-row parse, so they additionally need
4629 /// [`ExpressionSyntax::row_constructor`] (on in every preset that sets this). On
4630 /// for DuckDB only.
4631 pub lambda_expressions: bool,
4632 /// Accept the shared bitwise operators — binary `|` (OR), `&` (AND), `<<`/`>>`
4633 /// (shift), and prefix `~` (complement) — over integers. On in PostgreSQL, MySQL,
4634 /// SQLite, and DuckDB (the family is cross-dialect); off in ANSI. A parser-acceptance
4635 /// gate over lexemes that always tokenize (`|`/`&`/`~` are operator-class bytes and
4636 /// `<<`/`>>` are maximal-munched unconditionally), so when off the operator ends the
4637 /// expression and the trailing operand surfaces as a clean parse error. Each operator's
4638 /// binding power lives in [`BindingPowerTable`], where the ranks diverge per dialect
4639 /// (MySQL splits `|` < `&` < `<<`/`>>`; PostgreSQL/SQLite/DuckDB share one rank).
4640 /// Bitwise *XOR* is a separate pair of knobs — [`FeatureSet::caret_operator`] for the
4641 /// `^` spelling and [`FeatureSet::hash_bitwise_xor`] for `#` — because XOR's spelling,
4642 /// lexing, and precedence all diverge (`#` vs `^`).
4643 pub bitwise_operators: bool,
4644 /// Accept the quantified subquery comparison `<expr> <cmp> {ANY | ALL | SOME}
4645 /// (<subquery>)` (SQL-92 F291), as in `a = ANY (SELECT …)` / `a > ALL (SELECT …)`.
4646 /// On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no quantified comparison
4647 /// (it spells the same intent with `IN`/`EXISTS`), so it is off; the `ANY`/`ALL`/
4648 /// `SOME` keyword is then not read as a quantifier and surfaces as a clean parse
4649 /// error. Gates only the subquery-quantifier reading; `ALL`/`DISTINCT` as an
4650 /// aggregate-argument quantifier is the separate always-parsed set-quantifier.
4651 pub quantified_comparisons: bool,
4652 /// Accept the *list*-operand form of a quantified comparison — `<expr> <cmp>
4653 /// {ANY | ALL | SOME} (<value>)` where the parenthesized operand is a scalar
4654 /// list/array value rather than a subquery, as in DuckDB `ax = ANY (b)` /
4655 /// `x = ANY ([1, 2, 3])` and PostgreSQL `x = ANY (ARRAY[…])`. On for
4656 /// PostgreSQL/DuckDB/Lenient (which model it as PostgreSQL's `ScalarArrayOpExpr`,
4657 /// parsed to [`Expr::QuantifiedList`](crate::ast::Expr::QuantifiedList)); off for
4658 /// ANSI/MySQL, which admit only the subquery quantifier, and vacuously off for
4659 /// SQLite, which has no quantified comparison at all. Rides on top of
4660 /// [`quantified_comparisons`](Self::quantified_comparisons): when that gate leaves
4661 /// `ANY`/`ALL`/`SOME` unread this flag is unreachable, so it is only meaningfully
4662 /// set alongside it (the dependency is
4663 /// [`FeatureDependencyViolation::QuantifiedComparisonListsWithoutQuantifiedComparisons`]).
4664 /// When off, a non-subquery operand surfaces as the same clean
4665 /// "a subquery" parse error the standard form has always produced.
4666 pub quantified_comparison_lists: bool,
4667 /// Extend the quantifier `{ANY | ALL | SOME} (…)` past the six comparison
4668 /// operators to *any* infix operator — `<expr> <op> {ANY | ALL | SOME} (…)`
4669 /// where `<op>` is arithmetic (`+ - * / % ^`), string concatenation (`||`),
4670 /// bitwise (`& | # << >>`), or a comparison. PostgreSQL's grammar admits every
4671 /// operator in `MathOp`/`Op` here, only the boolean keywords `AND`/`OR` are
4672 /// excluded (engine-probed); the standard and the other dialects restrict the
4673 /// quantifier to the comparison operators. On for PostgreSQL/Lenient; off for
4674 /// ANSI/MySQL/DuckDB/SQLite. Rides on
4675 /// [`quantified_comparisons`](Self::quantified_comparisons) (and
4676 /// [`quantified_comparison_lists`](Self::quantified_comparison_lists) for the
4677 /// array-operand form): when the quantifier is unread this flag is unreachable (the
4678 /// dependency is [`FeatureDependencyViolation::QuantifiedArbitraryOperatorWithoutQuantifiedComparisons`]).
4679 /// When off, a non-comparison operator before `ANY`/`ALL`/`SOME` folds as an
4680 /// ordinary binary operator and the quantifier keyword then rejects as usual.
4681 pub quantified_arbitrary_operator: bool,
4682 /// Accept the general symbolic-operator surface — ANY operator drawn from the `Op`
4683 /// character class (`~ ! @ # ^ & | ? + - * / % < > =`), in both infix and prefix
4684 /// position, over a user-extensible operator set. A dialect-neutral capability any preset
4685 /// can enable; PostgreSQL (its `pg_operator`) is the current enabler, and the model
4686 /// follows its grammar. This is the ONE model for the whole
4687 /// tail (regex `~`/`!~`/`~*`/`!~*`; geometric/network/text-search `&&`/`&<`/`&>`/`<->`/
4688 /// `<<|`/`|>>`/`^@`/`##`/`<^`/`<%`/`@-@`; negator spellings `*<>`/`*>=`; the prefix
4689 /// `@`/`@@`/`|/`/`||/`/`!!`; and a fully user-defined `@#@`) rather than an enumerated
4690 /// per-lexeme set: the grammar admits every one as the same `Op`/`qual_Op`
4691 /// production, so a bare `a ~ b` is exactly `a OPERATOR(~) b`
4692 /// ([`Expr::NamedOperator`](crate::ast::Expr::NamedOperator) with the
4693 /// [`Bare`](crate::ast::NamedOperatorSpelling::Bare) spelling), and a prefix `@ x` is
4694 /// [`Expr::PrefixOperator`](crate::ast::Expr::PrefixOperator). Known operators still
4695 /// fold onto their dedicated [`BinaryOperator`] keys; only
4696 /// the remainder becomes a named operator.
4697 ///
4698 /// **Also gates the tokenizer.** Under this flag the operator scanner switches to
4699 /// PostgreSQL's maximal-munch lexer rule: a run of `Op` characters is one operator,
4700 /// truncated at an embedded `--`/`/*` comment start and with a trailing `+`/`-` stripped
4701 /// unless the run holds one of `~ ! @ # ^ & | ? %` (engine-measured: `a +- b` is
4702 /// `a + (- b)` but `a @- b` is one `@-` operator; `a <-- b` is `a <` then a `--`
4703 /// comment). The run that matches no built-in operator becomes an
4704 /// `Operator::Custom` token (the parser crate's tokenizer) carrying its span. Off leaves
4705 /// the fixed-form lexer untouched, so every other dialect's operator lexing is
4706 /// unchanged. The bare operators bind at the "any other operator" rank
4707 /// ([`BindingPowerTable::any_operator`](crate::precedence::BindingPowerTable::any_operator)),
4708 /// left-associative. On for PostgreSQL and DuckDB.
4709 ///
4710 /// The `Op` character class is not identical across enablers: DuckDB drops `#` and `?`
4711 /// (its positional-column `#1` and anonymous-parameter `?` sigils), so its operator runs
4712 /// stop at those bytes where PostgreSQL's do not — the lexer's `is_operator_char` reads
4713 /// the [`ExpressionSyntax::positional_column`] / [`ParameterSyntax::anonymous_question`]
4714 /// flags rather than hard-coding one charset. DuckDB *postfix* symbolic operators (`1 !`,
4715 /// removed from PostgreSQL in 14) are a separate axis this flag does not carry.
4716 ///
4717 /// The bare `@` operator shares its lead byte with the T-SQL `@name` / MySQL
4718 /// `@var` / `@@sysvar` sigils; the sigil arms win where a dialect enables them (the
4719 /// tracked [`LexicalConflict::CustomOperatorVersusAtName`] /
4720 /// [`LexicalConflict::CustomOperatorVersusSystemVariable`]), and no shipped preset
4721 /// enables both, so under PostgreSQL a bare `@` is always the operator.
4722 pub custom_operators: bool,
4723 /// Accept PostgreSQL/SQLite's one-word postfix null-test synonyms `<expr> ISNULL` and
4724 /// `<expr> NOTNULL` (for `IS NULL` / `IS NOT NULL`), folded onto
4725 /// [`Expr::IsNull`](crate::ast::Expr::IsNull) with a
4726 /// [`NullTestSpelling::Postfix`](crate::ast::NullTestSpelling) tag so they round-trip.
4727 /// A pure grammar gate at comparison precedence (the same non-associative rank as `IS
4728 /// NULL`): when off, the trailing `ISNULL`/`NOTNULL` keyword is left unconsumed and
4729 /// surfaces as a clean parse error (MySQL, which has no such synonym — `ISNULL(x)` is a
4730 /// *function* there, unaffected by this postfix gate). On for
4731 /// PostgreSQL/DuckDB/SQLite/Lenient.
4732 pub null_test_postfix: bool,
4733 /// Read a trailing symbolic operator with no following operand as a postfix operator
4734 /// application ([`Expr::PostfixOperator`](crate::ast::Expr::PostfixOperator)): `10!`
4735 /// (factorial), `1 ~`, `1 <->`, `1 &`. DuckDB keeps the generalized postfix reading
4736 /// PostgreSQL removed in version 14 (`a_expr Op %prec POSTFIXOP`), so it is the only
4737 /// enabler; PostgreSQL 14+ and every other preset reject the trailing operator.
4738 ///
4739 /// A pure parser-position gate, MECE against [`custom_operators`](Self::custom_operators):
4740 /// that flag owns the tokenizer's maximal-munch lexer and the *infix*/*prefix* general
4741 /// operator surface, while this flag owns only the *postfix* reduction of an already-lexed
4742 /// `Op`-class token. The infix reading still wins whenever an operand follows the operator
4743 /// (`1 ! + 2` is the infix `1 ! (+2)`); the postfix reduction fires only in the
4744 /// operand-absent position (`1 ! < 2`, `10!`, `1 ! FROM t`), engine-measured on DuckDB
4745 /// 1.5.4 via `duckdb_extract_statements` (parse-accept) — the unknown postfix operators
4746 /// then bind-reject (`Scalar Function !~__postfix does not exist`), an under-acceptance our
4747 /// parse-only parser closes by accepting the parse. The postfix binds at the "any other
4748 /// operator" left rank (looser than the arithmetic operators: `2 * 3 !` is `(2 * 3)!`).
4749 /// The postfix-eligible tokens are the general symbolic operators — the `Custom` residue,
4750 /// the lone `~`/`!`/`&&`, and the dedicated `& | << >> || <@ @> ^@`; the JSON arrows
4751 /// `->`/`->>` are excluded (DuckDB rejects them postfix). On for DuckDB, and for the
4752 /// permissive Lenient union (a pure additive parser position with no contended trigger,
4753 /// though only the always-lexed `Op` tokens reach it there — `custom_operators` is off
4754 /// under Lenient for the `@`-sigil conflict).
4755 pub postfix_operators: bool,
4756}
4757
4758/// Dialect-owned function-*call* syntax accepted by the parser.
4759///
4760/// The call-tail and special-call-form family, split out of [`ExpressionSyntax`] when it
4761/// crossed its 16-field line; the aggregate/window call forms and the `StringFunc` keyword
4762/// special forms later split out again into [`AggregateCallSyntax`] and [`StringFuncForms`]
4763/// at their own 16-field lines. Each flag decides whether the parser *admits* the form at
4764/// its call-grammar position, and when off the introducing keyword/arrow is left unconsumed
4765/// and surfaces as a clean parse error (`named_argument` additionally gates the `=>`/`:=`
4766/// tokenizer lexemes).
4767#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4768pub struct CallSyntax {
4769 /// Accept PostgreSQL named function arguments `f(name => value)` and the
4770 /// deprecated `f(name := value)`. Also gates the tokenizer for the `=>` / `:=`
4771 /// arrow lexemes. **Shared `:=` claim:** MySQL's
4772 /// [`SessionVariableSyntax::variable_assignment`] also enables `:=` lexing for
4773 /// `SET @v := …`; the two never coexist in a shipped preset, so one
4774 /// `Operator::ColonEquals` token stays unambiguous per
4775 /// dialect (see tokenizer `:=` munch comments).
4776 pub named_argument: bool,
4777 /// Accept MySQL's `UTC_DATE` / `UTC_TIME` / `UTC_TIMESTAMP` niladic date/time
4778 /// functions — the UTC-clock analogues of the `CURRENT_*` special value functions,
4779 /// sharing the same nullary (plus optional precision on the time forms) grammar
4780 /// production. A parser-acceptance gate only: the keywords always tokenize (they are
4781 /// non-reserved outside MySQL), so when off they stay ordinary column/function names.
4782 /// Expression position only — MySQL has no PostgreSQL-style `func_table` promotion.
4783 pub utc_special_functions: bool,
4784 /// Read `COLUMNS(<selector>)` as DuckDB's star-expression column selector
4785 /// ([`Expr::Columns`](crate::ast::Expr::Columns)) rather than an ordinary call to a
4786 /// function named `columns`. A grammar-position gate keyed on the (non-reserved)
4787 /// `COLUMNS` keyword immediately followed by `(`: DuckDB has no user function of
4788 /// that spelling, so the form is unambiguous once the flag is on (the tokenizer is
4789 /// untouched — the disambiguation is a parser lookahead, mirroring `ARRAY[`/`ROW(`).
4790 /// A bare `columns` with no `(` stays an identifier in every dialect. Separate from
4791 /// [`SelectSyntax::wildcard_modifiers`](SelectSyntax::wildcard_modifiers) because
4792 /// the surfaces sit in different grammar positions — `COLUMNS(…)` is an expression
4793 /// (it nests in `sum(COLUMNS(*))`, `COLUMNS(*)::JSON`), the `*` modifiers are a
4794 /// projection-item tail — and are read by different parser code (expression vs
4795 /// select-item). When off, `COLUMNS(x)` parses as a plain function call, matching
4796 /// every non-DuckDB dialect. On for DuckDB / Lenient, off elsewhere.
4797 pub columns_expression: bool,
4798 /// Accept the `EXTRACT(<field> FROM <source>)` datetime-field extraction special
4799 /// form (SQL-92 F052; PostgreSQL). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient.
4800 /// SQLite has no `EXTRACT` (its date functions are `strftime`/`date`/…), so it is
4801 /// off; `EXTRACT` then reads as an ordinary identifier/function name and the `FROM`
4802 /// inside its parentheses surfaces as a clean parse error. Gates only the
4803 /// `FROM`-separated form — an ordinary `extract(a, b)` call is unaffected.
4804 pub extract_from_syntax: bool,
4805 /// Read `TRY_CAST(<expr> AS <type>)` as DuckDB's null-on-failure cast — the same
4806 /// [`Expr::Cast`](crate::ast::Expr::Cast) shape as `CAST`, distinguished by the
4807 /// canonical `try` flag (DuckDB's own serialized tree carries `try_cast: true`), not
4808 /// a spelling tag: null-on-failure is different *semantics*, not a different spelling
4809 /// of `CAST`. A grammar-position gate keyed on the (DuckDB-only) `TRY_CAST`
4810 /// keyword immediately followed by `(`; when off, `TRY_CAST` reads as an ordinary
4811 /// function name and the `AS` inside its parentheses surfaces as a clean parse error,
4812 /// matching every non-DuckDB dialect (verified: PostgreSQL syntax-errors at `AS`). On
4813 /// for DuckDb/Lenient.
4814 pub try_cast: bool,
4815 /// Restrict the `CAST(<expr> AS <target>)` target to MySQL's narrow `cast_type`
4816 /// grammar rather than the full column-type vocabulary. Off for
4817 /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient, whose casts admit any type name. On for
4818 /// MySQL, whose `CAST`/`CONVERT` target is a closed set —
4819 /// `SIGNED`/`UNSIGNED [INTEGER|INT]`, `CHAR`/`NCHAR`/`CHARACTER`/`NATIONAL CHAR`,
4820 /// `BINARY`, `DATE`, `DATETIME`, `TIME`, `DECIMAL`/`DEC`, `DOUBLE`/`DOUBLE PRECISION`,
4821 /// `FLOAT`, `REAL`, `JSON`, `YEAR`, and the spatial types (`POINT`, `LINESTRING`,
4822 /// `POLYGON`, `MULTIPOINT`, `MULTILINESTRING`, `MULTIPOLYGON`, `GEOMETRYCOLLECTION`, and
4823 /// the `GEOMCOLLECTION` alias) — so the common column types `INT`/`INTEGER`/
4824 /// `SMALLINT`/`BIGINT`/`TINYINT`, `VARCHAR`/`TEXT`, `TIMESTAMP`, `NUMERIC`, `BOOLEAN`,
4825 /// `VARBINARY`/`BLOB`/`BIT`, bare `GEOMETRY`, and any user-defined name are the syntax
4826 /// error MySQL reports in cast position (while remaining valid as column types). When on,
4827 /// the parser parses the target type as usual, then rejects it unless it is one of the
4828 /// MySQL cast targets (the whole set engine-measured on mysql:8 —
4829 /// `mysql-faithful-cast-type-production`).
4830 ///
4831 /// `YEAR` and the spatial cast targets parse as user-defined names, so the faithful
4832 /// production layers a name allowlist over the shape check (the parser's
4833 /// `is_mysql_cast_target`); the inert trailing `INTEGER`/`INT` of `SIGNED`/`UNSIGNED` is
4834 /// folded onto the standalone numeric modifier. The one engine-measured residual is the
4835 /// `CHAR` charset annotation
4836 /// (`CAST(x AS CHAR CHARACTER SET utf8mb4)`, and the `ASCII`/`UNICODE`/trailing-`BINARY`
4837 /// shorthands): MySQL accepts these, but type-name charset/collation is a general MySQL
4838 /// feature the shared type grammar does not model at all (columns take the same
4839 /// annotation), so it is over-rejected here — a separate type-grammar feature, not a
4840 /// cast-target-membership gap, and invisible to every corpus.
4841 pub restricted_cast_targets: bool,
4842 /// Accept a string literal as the `EXTRACT('<field>' FROM <source>)` field, as DuckDB
4843 /// admits (`extract('year' FROM x)`), storing the quoted field as an
4844 /// [`Ident`](crate::ast::Ident) with [`QuoteStyle::Single`](crate::ast::QuoteStyle) so
4845 /// it round-trips. On for Postgres/DuckDb/Lenient. When off the field is a bare
4846 /// identifier only, so a leading string surfaces as a clean parse error (the standard
4847 /// `EXTRACT` field is an identifier). PostgreSQL admits the same quoted field (`Sconst`
4848 /// in its `extract_arg`) — engine-verified against pg_query, including the reject
4849 /// boundary (a non-string non-identifier field rejects on both) — so the flag ships on
4850 /// under the `Postgres` preset too.
4851 pub extract_string_field: bool,
4852 /// Accept DuckDB's dot-method call chaining on a value — a postfix `.<method>(<args>)`
4853 /// on a non-name receiver (`list(forecast).list_transform(x -> x + 10)`) that desugars
4854 /// to the ordinary function call `<method>(<receiver>, <args>)`. On for DuckDb/Lenient.
4855 /// A plain `name.method(args)` is already the schema-qualified call the object-name
4856 /// grammar reads, so the postfix fires only on a receiver that is not a bare name (a
4857 /// function-call result, a parenthesized expression, …); there is no ambiguity with a
4858 /// qualified call. When off, a `.method(` after a value surfaces as a clean parse error.
4859 pub method_chaining: bool,
4860 /// Reject an empty argument list on PostgreSQL's SQL/JSON constructor keywords — the
4861 /// closed set `PG_SQLJSON_EMPTY_REJECTING_CONSTRUCTORS` in the parser crate (`JSON`,
4862 /// `JSON_SCALAR`, `JSON_SERIALIZE`). PostgreSQL parses these through dedicated `gram.y`
4863 /// productions that require the context-item / value argument, so `JSON()` /
4864 /// `JSON_SCALAR()` / `JSON_SERIALIZE()` is a syntax error, whereas we would otherwise
4865 /// admit them as ordinary niladic calls. Keyed on a single *unquoted* name, so a quoted
4866 /// `"json"()` stays an ordinary call PostgreSQL accepts (rejected only at name
4867 /// resolution). On for PostgreSQL only. The set is deliberately narrow and extension-
4868 /// shaped: the JSON_VALUE/JSON_QUERY grammar ([[pg-sqljson-expression-functions]])
4869 /// carries the same arity floor via [`sqljson_expression_functions`](CallSyntax::sqljson_expression_functions).
4870 pub sqljson_constructors_require_argument: bool,
4871 /// Parse the SQL:2016/2023 SQL/JSON expression functions as dedicated special forms:
4872 /// the `JSON_VALUE`/`JSON_QUERY`/`JSON_EXISTS` query functions with their
4873 /// `PASSING`/`RETURNING`/`FORMAT JSON`/wrapper/quotes/`ON EMPTY`/`ON ERROR` clause
4874 /// tails, the `JSON_OBJECT`/`JSON_ARRAY` constructors and their `JSON_OBJECTAGG`/
4875 /// `JSON_ARRAYAGG` aggregates (with `[ABSENT|NULL] ON NULL` and `[WITH|WITHOUT]
4876 /// UNIQUE [KEYS]`), the bare `JSON`/`JSON_SCALAR`/`JSON_SERIALIZE` constructors, and
4877 /// the `IS [NOT] JSON [VALUE|ARRAY|OBJECT|SCALAR] [WITH|WITHOUT UNIQUE [KEYS]]`
4878 /// predicate. On for PostgreSQL/Lenient (engine-verified against `pg_query`); the
4879 /// clause grammar is PostgreSQL's raw-parse surface (per-function legality such as
4880 /// `JSON_VALUE` rejecting a wrapper is enforced, while the shared behaviour set that
4881 /// PostgreSQL only narrows during parse *analysis* is admitted uniformly). Off for
4882 /// MySQL/DuckDB/SQLite/ANSI: MySQL has its own JSON functions with a *different*
4883 /// grammar, DuckDB/SQLite have no SQL/JSON standard special forms, and those
4884 /// dialects keep the keywords as ordinary function/column names. When off, the
4885 /// keyword heads fall through to the ordinary call/name path exactly as before.
4886 /// Composes with [`sqljson_constructors_require_argument`](CallSyntax::sqljson_constructors_require_argument):
4887 /// the empty `JSON()`/`JSON_SCALAR()`/`JSON_SERIALIZE()` reject is enforced by these
4888 /// special forms requiring their argument (PostgreSQL), while a dialect with the
4889 /// arity floor *off* (Lenient) falls the empty form back to an ordinary niladic call.
4890 pub sqljson_expression_functions: bool,
4891 /// Parse the SQL:2006 SQL/XML expression functions as dedicated special forms: the
4892 /// `xmlelement`/`xmlforest`/`xmlconcat`/`xmlparse`/`xmlpi`/`xmlroot`/`xmlserialize`/
4893 /// `xmlexists` constructors — with their keyword-clause grammar inside the parens
4894 /// (`NAME <label>`, `xmlattributes(…)`, `{DOCUMENT|CONTENT}`, `{PRESERVE|STRIP}
4895 /// WHITESPACE`, `VERSION {…|NO VALUE}`, `STANDALONE {YES|NO|NO VALUE}`, `AS <type>
4896 /// [[NO] INDENT]`, `PASSING [BY {REF|VALUE}] …`) — and the `IS [NOT] DOCUMENT`
4897 /// predicate. On for PostgreSQL/Lenient (engine-verified against `pg_query`); the
4898 /// clause grammar is PostgreSQL's raw-parse surface. Off for MySQL/DuckDB/SQLite/ANSI:
4899 /// none of those dialects have the SQL/XML standard special forms, and they keep the
4900 /// `xml*` keywords as ordinary function/column names. When off, the keyword heads fall
4901 /// through to the ordinary call/name path exactly as before. The `xmlagg` aggregate is
4902 /// *not* gated here — it is an ordinary keyword-free aggregate name that already parses
4903 /// through the ordinary aggregate call path in every dialect.
4904 pub xml_expression_functions: bool,
4905 /// Accept the call-site `VARIADIC` argument marker that spreads an array over a
4906 /// variadic parameter (`f(a, VARIADIC arr)`, `f(VARIADIC name => arr)`), riding the
4907 /// shared [`FunctionArg`](crate::ast::FunctionArg)'s
4908 /// [`variadic`](crate::ast::FunctionArg::variadic) flag. On for PostgreSQL/DuckDb/
4909 /// Lenient (both engines parse-accept it with identical rules — engine-probed on
4910 /// pg_query PG-17 and DuckDB 1.5.4). A parse-layer gate: the parser admits the
4911 /// `VARIADIC` prefix only on the *last* argument of the list and rejects it alongside
4912 /// an `ALL`/`DISTINCT` quantifier, mirroring both engines' `gram.y` productions
4913 /// (`func_application: … ',' VARIADIC func_arg_expr`), which carry no quantifier and
4914 /// place `VARIADIC` last. When off (ANSI/MySQL/SQLite), the `VARIADIC` keyword is left
4915 /// unconsumed and surfaces as a clean parse error. The argument-type check (the spread
4916 /// value must be an array) is a binding concern neither parser enforces.
4917 pub variadic_argument: bool,
4918 /// Accept PostgreSQL's `merge_action()` — the zero-argument special function that
4919 /// reports which `MERGE` branch produced a row (`'INSERT'`/`'UPDATE'`/`'DELETE'`),
4920 /// valid only in a `MERGE ... RETURNING` list. PostgreSQL gives it a dedicated
4921 /// `func_expr_common_subexpr` production (`MERGE_ACTION '(' ')'`), so at raw parse it
4922 /// is accepted *anywhere* an expression is (the MERGE-RETURNING-only restriction is a
4923 /// parse-*analysis* check, engine-verified against `pg_query`: `SELECT merge_action()`
4924 /// raw-parse-accepts) but takes strictly empty parens — `merge_action(1)` and
4925 /// `merge_action() OVER ()` are both syntax errors (probed). The keyword is reserved
4926 /// against ordinary calls (`POSTGRES_NON_GENERIC_FUNCTION_KEYWORDS`), so when off it
4927 /// stays the "no call form" reject it already was; when on, the strictly niladic form
4928 /// parses to the canonical [`Expr::Function`](crate::ast::Expr::Function) shape (name
4929 /// `merge_action`, no arguments). On for PostgreSQL/Lenient; off elsewhere (no other
4930 /// shipped dialect has the form). A bare `merge_action` with no `(` is untouched.
4931 pub merge_action_function: bool,
4932 /// Accept MySQL's `CONVERT` special-form function — both the comma-form cast
4933 /// `CONVERT(<expr>, <type>)` and the transcoding `CONVERT(<expr> USING <charset>)`
4934 /// form (grammar's one `CONVERT '(' … ')'` production, two shapes). Only `CONVERT`
4935 /// immediately followed by `(` opens it; when off, `CONVERT` keeps its ordinary
4936 /// function-name reading (PostgreSQL's plain `convert(bytea, name, name)` call is
4937 /// unaffected — engine-verified against `pg_query`, which parses `CONVERT('x', 'a', 'b')`
4938 /// as an ordinary call and rejects the `USING` form). The comma form folds onto the
4939 /// [`Expr::Cast`](crate::ast::Expr::Cast) node as [`CastSyntax::Convert`](crate::ast::CastSyntax)
4940 /// and shares [`restricted_cast_targets`](CallSyntax::restricted_cast_targets)' `cast_type`
4941 /// gate (so `CONVERT(1, INT)` rejects wherever `CAST(1 AS INT)` does); the USING form
4942 /// is a [`StringFunc::ConvertUsing`](crate::ast::StringFunc::ConvertUsing) whose charset
4943 /// operand is a MySQL `charset_name` (`ident_or_text` or the `BINARY` transcoding name).
4944 /// On for MySQL/Lenient; off elsewhere (no other shipped dialect has the special form).
4945 pub convert_function: bool,
4946}
4947
4948/// Dialect-owned keyword string/scalar special-form syntax accepted by the parser.
4949///
4950/// The keyword special forms that parse to the [`StringFunc`](crate::ast::StringFunc)
4951/// AST family — the SQL-standard string special forms and the sibling scalar keyword forms
4952/// sharing that node. Split out of [`CallSyntax`] at its 16-field line on the grammar
4953/// boundary "the flag's sole AST product is a `StringFunc` variant" — so the dual
4954/// cast/transcode `CONVERT` stays with the cast core. Each flag is a grammar gate: when off
4955/// the keyword head falls through to the ordinary call path or the trailing keyword
4956/// surfaces as a clean parse error.
4957#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4958pub struct StringFuncForms {
4959 /// Accept the `SUBSTRING(<expr> FROM <start> [FOR <count>])` keyword special form
4960 /// (SQL-92 E021-06). On for ANSI/PostgreSQL/DuckDB/MySQL/Lenient (each engine
4961 /// probed accepting); off for SQLite, which has no keyword string forms at all
4962 /// (probed: `near "FROM": syntax error`) — there the head falls through to the
4963 /// ordinary call path and the inner `FROM` is a clean parse error. The comma
4964 /// plain-call spelling `substring(x, 1, 2)` is untouched by this flag: it keeps
4965 /// parsing as an ordinary [`FunctionCall`](crate::ast::FunctionCall) everywhere
4966 /// (every probed engine accepts it). MySQL additionally requires the `(` adjacent
4967 /// to the head for the keyword form — the same `IGNORE_SPACE`-off demotion the
4968 /// aggregates and `EXTRACT` follow, composed via
4969 /// [`aggregate_args_require_adjacent_paren`](AggregateCallSyntax::aggregate_args_require_adjacent_paren)
4970 /// (probed: spaced `SUBSTRING ('a' FROM 2)` is 1064 while spaced
4971 /// `SUBSTRING ('a', 2)` parse-accepts through the demoted generic path).
4972 pub substring_from_for: bool,
4973 /// Accept the `FOR`-leading `SUBSTRING` spellings: the bare
4974 /// `SUBSTRING(<expr> FOR <count>)` and the reversed
4975 /// `SUBSTRING(<expr> FOR <count> FROM <start>)` order. On for
4976 /// PostgreSQL/DuckDB/Lenient (both engines probed accepting both orders); off for
4977 /// MySQL (probed 1064 on both — its grammar is strictly `FROM`-first), ANSI
4978 /// (SQL-92's `<character substring function>` is `FROM`-first only), and SQLite.
4979 pub substring_leading_for: bool,
4980 /// Accept PostgreSQL's `SUBSTRING(<expr> SIMILAR <pattern> ESCAPE <escape>)`
4981 /// regex form (SQL:1999's regular-expression substring function). On for
4982 /// PostgreSQL/Lenient; off for DuckDB (probed: parser error — its PG-fork grammar
4983 /// dropped the production), MySQL, SQLite, and ANSI (an optional-feature form only
4984 /// PostgreSQL ships). The `ESCAPE` operand is mandatory, matching pg_query
4985 /// (`SUBSTRING(x SIMILAR p)` rejects, probed).
4986 pub substring_similar: bool,
4987 /// Reject a `substring(…)`/`substr(…)` plain (comma) call whose argument count is
4988 /// not 2 or 3 — MySQL's dedicated grammar admits exactly `(str, pos)`,
4989 /// `(str, pos, len)`, and the `FROM`/`FOR` keyword forms, so `SUBSTRING('a')`,
4990 /// `SUBSTRING()`, and `SUBSTRING('a', 2, 3, 4)` are `ER_PARSE_ERROR` (1064) on
4991 /// mysql:8.4 (all probed) while PostgreSQL parse-accepts any arity
4992 /// (`SUBSTRING()` probed accepted — arity is a catalog lookup there, not
4993 /// grammar). On for MySQL only. Only the *adjacent-paren* call is checked: a
4994 /// spaced `SUBSTRING ('a')` is MySQL's demoted stored-function path, which
4995 /// parse-accepts any arity (probed 1046, a binding-class reject).
4996 pub substring_plain_call_requires_2_or_3_args: bool,
4997 /// Accept the `SUBSTR` head for the same `FROM`/`FOR` keyword forms
4998 /// (`SUBSTR(str FROM 2 FOR 3)`). On for MySQL/Lenient: MySQL's `SUBSTR` is a
4999 /// full synonym of `SUBSTRING` including the keyword grammar (probed accepted),
5000 /// while PostgreSQL and DuckDB have no `SUBSTR` keyword at all — their `substr`
5001 /// is an ordinary catalog function, so the keyword form parse-rejects (both
5002 /// probed) and the flag stays off. `substr` is not a keyword in any dialect's
5003 /// inventory, so the head is matched textually on an unquoted call only — a
5004 /// quoted `"substr"(…)` stays an ordinary call, and every plain `substr(a, b)`
5005 /// call is untouched.
5006 pub substr_from_for: bool,
5007 /// Accept the `POSITION(<substr> IN <string>)` keyword special form (SQL-92
5008 /// E021-11). On for ANSI/PostgreSQL/DuckDB/MySQL/Lenient; off for SQLite (no
5009 /// keyword form; its `position(a, b)` stays an ordinary call that fails only at
5010 /// binding). There is NO plain-call fallback where the flag is on:
5011 /// `position('b', 'abc')` is a parse error on PostgreSQL, DuckDB, *and* MySQL
5012 /// (all probed), so a comma after the first operand surfaces as a clean parse
5013 /// error rather than re-reading as a generic call. The operands are the
5014 /// restricted `b_expr` (PostgreSQL's `position_list`, DuckDB inheriting it —
5015 /// `POSITION('a' = 'b' IN 'c')` parse-accepts on both while
5016 /// `POSITION(1 IN 2 OR 3)` rejects, both probed); MySQL's asymmetric grammar is
5017 /// [`position_asymmetric_operands`](StringFuncForms::position_asymmetric_operands).
5018 pub position_in: bool,
5019 /// Use MySQL's asymmetric `POSITION` operand grammar — `bit_expr IN expr` — in
5020 /// place of the standard symmetric `b_expr IN b_expr`. The needle tightens to
5021 /// MySQL's `bit_expr` (arithmetic/bit operators only — no comparisons:
5022 /// `POSITION('a' = 'b' IN 'c')` is 1064, probed) and the haystack widens to a
5023 /// full expression (`POSITION(1 IN 2 OR 3)` accepts, probed). On for MySQL only.
5024 pub position_asymmetric_operands: bool,
5025 /// Accept the `OVERLAY(<target> PLACING <replacement> FROM <start> [FOR <count>])`
5026 /// keyword special form (SQL:1999 T312). On for ANSI/PostgreSQL/DuckDB/Lenient;
5027 /// off for MySQL (no `OVERLAY` at all — the keyword form is 1064 and a plain
5028 /// `overlay(…)` call parses as a stored-function reference, probed) and SQLite.
5029 /// `FROM <start>` is mandatory: `OVERLAY(x PLACING y)` and
5030 /// `OVERLAY(x PLACING y FOR 1)` parse-reject on PostgreSQL and DuckDB (probed).
5031 pub overlay_placing: bool,
5032 /// Require the `PLACING` form after `OVERLAY(` — i.e. drop the plain-call
5033 /// fallback. DuckDB's grammar has *only* the `PLACING` production:
5034 /// `overlay('abc', 'X', 2, 1)`, `overlay('abc')`, and `overlay()` are all parser
5035 /// errors there (probed), while PostgreSQL keeps its `func_arg_list_opt`
5036 /// alternative so the same spellings parse-accept (probed; arity is a catalog
5037 /// concern there). On for DuckDB and ANSI (the standard defines no plain
5038 /// `overlay` call); off for PostgreSQL/Lenient, where a non-`PLACING` argument
5039 /// list falls back to the ordinary call path.
5040 pub overlay_requires_placing: bool,
5041 /// Accept the restricted `TRIM([{BOTH | LEADING | TRAILING}] [<chars>] FROM
5042 /// <source>)` keyword special form (SQL-92 E021-09): a side and/or a
5043 /// trim-character expression, then `FROM` and exactly one source. On for
5044 /// ANSI/PostgreSQL/DuckDB/MySQL/Lenient; off for SQLite (probed: syntax error —
5045 /// its two-argument `trim(x, y)` plain call is the only spelling). The bare
5046 /// single-argument `TRIM(x)` stays an ordinary call everywhere, and `TRIM()`
5047 /// rejects wherever the flag is on (PostgreSQL/DuckDB/MySQL all probed rejecting
5048 /// the empty form). MySQL requires at least one of side/chars before `FROM`
5049 /// (`TRIM(FROM 'x')` is 1064, probed) and holds the keyword form to the adjacent
5050 /// `(` like `SUBSTRING` (spaced `TRIM (LEADING …)` is 1064 while spaced
5051 /// `TRIM ('abc')` parse-accepts through the demoted generic path, both probed);
5052 /// the looser PostgreSQL tails are [`trim_list_syntax`](StringFuncForms::trim_list_syntax).
5053 pub trim_from: bool,
5054 /// Accept PostgreSQL's loose `trim_list` tails on the `TRIM` special form: the
5055 /// bare `TRIM(FROM <list>)` (no side, no chars), a side without `FROM`
5056 /// (`TRIM(TRAILING ' foo ')`, `TRIM(LEADING 'x', 'y')`), a multi-expression
5057 /// source list (`TRIM('a' FROM 'b', 'c')`, `TRIM(BOTH FROM 'a', 'b')`), and the
5058 /// comma plain-call spelling `trim('a', 'b')` (which PostgreSQL parses through
5059 /// the same production and we keep as an ordinary call). On for
5060 /// PostgreSQL/DuckDB/Lenient (every listed form probed parse-accepting on both
5061 /// engines — DuckDB's rejects here are binder arity, not grammar); off for MySQL
5062 /// (each probed 1064), ANSI (the standard's trim operand takes one source), and
5063 /// SQLite. Where this is off but [`trim_from`](StringFuncForms::trim_from) is on, a comma
5064 /// after the first `TRIM` operand is a clean parse error — matching MySQL, whose
5065 /// `trim('a', 'b')` is 1064 (probed).
5066 pub trim_list_syntax: bool,
5067 /// Accept PostgreSQL's `COLLATION FOR (<expr>)` common-subexpr — the special form that
5068 /// reports the collation name derived for its operand. PostgreSQL gives it a dedicated
5069 /// `COLLATION FOR '(' a_expr ')'` production (lowered to a
5070 /// `pg_catalog.pg_collation_for(<expr>)` call), so only `COLLATION` immediately trailed
5071 /// by `FOR (` opens it; the parentheses and single `a_expr` operand are mandatory —
5072 /// `COLLATION FOR 'x'`, `COLLATION FOR ()`, and a two-argument list all reject
5073 /// (engine-verified against `pg_query`). When off, `COLLATION` keeps its ordinary
5074 /// `type_func_name` reading (a plain `collation(x)` call is unaffected either way). On
5075 /// for PostgreSQL/Lenient; off elsewhere (no other shipped dialect has the form —
5076 /// DuckDB overrides PostgreSQL's `true` back to `false`, its `COLLATION FOR` surface
5077 /// unprobed). The parsed form keeps its keyword shape as
5078 /// [`StringFunc::CollationFor`](crate::ast::StringFunc::CollationFor) so it round-trips
5079 /// as written rather than as the lowered call.
5080 pub collation_for_expression: bool,
5081 /// Accept the `CEIL`/`CEILING` rounding-field keyword form: `CEIL(<expr> TO <field>)`
5082 /// (and the `CEILING` spelling). No probed oracle grammar admits the `TO` tail —
5083 /// engine-verified against `pg_query` (`syntax error at or near "TO"`), DuckDB, and
5084 /// mysql:8.4.10 (all reject) — so this is sqlparser-rs-parity surface only, not a
5085 /// real-engine grammar; on for Lenient, off for every shipped engine preset. Only
5086 /// `CEIL`/`CEILING` immediately followed by `(` opens the speculative read (mirroring
5087 /// [`substring_from_for`](StringFuncForms::substring_from_for)'s shape): the first operand parses
5088 /// as an ordinary expression, and only a following `TO` commits to the special form —
5089 /// a first operand with no `TO` tail rewinds to the ordinary call path, so the comma
5090 /// scale spelling `CEIL(<expr>, <scale>)` is untouched and keeps parsing as a plain
5091 /// [`FunctionCall`](crate::ast::FunctionCall) in every dialect regardless of this flag.
5092 /// When off, `CEIL(x TO DAY)` is the same clean parse error it is today (an
5093 /// unexpected `TO` where a `,` or `)` is expected). The field (`DAY`, `HOUR`, …) is
5094 /// stored as a written [`Ident`](crate::ast::Ident), validated (if at all) by the
5095 /// consuming engine at analysis time, not parse. The parsed form is
5096 /// [`StringFunc::CeilTo`](crate::ast::StringFunc::CeilTo).
5097 pub ceil_to_field: bool,
5098 /// Accept the `FLOOR` rounding-field keyword form: `FLOOR(<expr> TO <field>)`. No
5099 /// probed oracle grammar admits the `TO` tail — engine-verified against `pg_query`
5100 /// (`syntax error at or near "TO"`), DuckDB, and mysql:8.4.10 (all reject) — so this
5101 /// is sqlparser-rs-parity surface only, not a real-engine grammar; on for Lenient,
5102 /// off for every shipped engine preset. Unlike `CEIL`/`CEILING`, `FLOOR` has no
5103 /// synonym spelling to track. Only `FLOOR` immediately followed by `(` opens the
5104 /// speculative read (mirroring [`ceil_to_field`](StringFuncForms::ceil_to_field)'s shape): the
5105 /// first operand parses as an ordinary expression, and only a following `TO` commits
5106 /// to the special form — a first operand with no `TO` tail rewinds to the ordinary
5107 /// call path, so the comma scale spelling `FLOOR(<expr>, <scale>)` is untouched and
5108 /// keeps parsing as a plain [`FunctionCall`](crate::ast::FunctionCall) in every
5109 /// dialect regardless of this flag. When off, `FLOOR(x TO DAY)` is the same clean
5110 /// parse error it is today (an unexpected `TO` where a `,` or `)` is expected). The
5111 /// field (`DAY`, `HOUR`, …) is stored as a written [`Ident`](crate::ast::Ident),
5112 /// validated (if at all) by the consuming engine at analysis time, not parse. The
5113 /// parsed form is [`StringFunc::FloorTo`](crate::ast::StringFunc::FloorTo).
5114 pub floor_to_field: bool,
5115 /// Accept MySQL's full-text `MATCH (<col>, …) AGAINST (<expr> [<modifier>])` special-form
5116 /// expression (grammar's `MATCH ident_list_arg AGAINST '(' bit_expr fulltext_options ')'`).
5117 /// Only `MATCH` immediately followed by `(` opens it; the column list is comma-separated
5118 /// column references (a general expression, literal, function call, or empty list all
5119 /// parse-reject), the `AGAINST` operand is a `bit_expr` (so a trailing `IN`/`WITH` opens the
5120 /// modifier rather than an `IN` predicate), and the optional modifier is exactly one of
5121 /// `IN NATURAL LANGUAGE MODE`, `IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION`,
5122 /// `IN BOOLEAN MODE`, or `WITH QUERY EXPANSION` (all engine-verified on mysql:8.4.10; the
5123 /// non-reserved `AGAINST`/`QUERY`/`EXPANSION` words are matched contextually, MySQL's
5124 /// reserved-only inventory design). The parsed form is
5125 /// [`StringFunc::MatchAgainst`](crate::ast::StringFunc::MatchAgainst). Distinct from SQLite's
5126 /// infix `<expr> MATCH <expr>` operator (a binding-power table entry, not this gate). On for
5127 /// MySQL/Lenient; off elsewhere (no other shipped dialect has the special form).
5128 pub match_against: bool,
5129}
5130
5131/// Dialect-owned aggregate/window function-call syntax accepted by the parser.
5132///
5133/// The call-grammar forms specific to aggregate and window function calls — the
5134/// in-parenthesis and post-`)` tails, the argument-shape and arity restrictions, and
5135/// the `OVER`-eligibility gate. Split out of [`CallSyntax`] at its 16-field line as
5136/// the aggregate/window axis, distinct from the scalar special-form and general
5137/// call-tail cores. Each flag is a grammar gate: when off the tail keyword is left
5138/// unconsumed and surfaces as a clean parse error, or the restriction does not fire.
5139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5140pub struct AggregateCallSyntax {
5141 /// Accept the MySQL `GROUP_CONCAT(<args> [ORDER BY …] SEPARATOR <string>)` delimiter
5142 /// tail — the trailing `SEPARATOR '<sep>'` inside an aggregate call's parentheses,
5143 /// after any in-parenthesis `ORDER BY`. It rides the shared
5144 /// [`FunctionCall`](crate::ast::FunctionCall) shape's new
5145 /// [`separator`](crate::ast::FunctionCall::separator) field, gated by
5146 /// grammar position like the always-parsed in-parenthesis `ORDER BY`. When off
5147 /// (ANSI/PostgreSQL, which write the delimiter as an ordinary `string_agg` argument),
5148 /// the `SEPARATOR` keyword is left unconsumed and the unmatched `)` surfaces as a
5149 /// clean parse error.
5150 pub group_concat_separator: bool,
5151 /// Accept the `WITHIN GROUP (ORDER BY …)` ordered-set-aggregate tail (SQL:2008
5152 /// T612/T614), as in `percentile_cont(0.5) WITHIN GROUP (ORDER BY x)`. On for
5153 /// ANSI/PostgreSQL/DuckDB/Lenient. Neither SQLite nor MySQL has ordered-set
5154 /// aggregates (both engine-measured-rejected), so it is off for both; the `WITHIN`
5155 /// keyword is then left unconsumed and surfaces as a clean parse error. Only the
5156 /// `WITHIN GROUP` pair opens the clause, so a bare `within` stays a usable alias
5157 /// regardless.
5158 pub within_group: bool,
5159 /// Accept the `FILTER (WHERE <predicate>)` aggregate-filter tail (SQL:2003 T612), as
5160 /// in `sum(x) FILTER (WHERE x > 1)`. On for ANSI/PostgreSQL/SQLite (3.30+)/DuckDB/
5161 /// Lenient. MySQL has no aggregate `FILTER` clause (engine-measured-rejected on
5162 /// mysql:8), so it is off there; the `FILTER` keyword is then left unconsumed and
5163 /// surfaces as a clean parse error. Only `FILTER` immediately followed by `(` opens the
5164 /// clause, so a bare `filter` after a call stays a usable alias.
5165 pub aggregate_filter: bool,
5166 /// Accept an aggregate `FILTER (…)` tail whose predicate is *not* preceded by the
5167 /// SQL-standard `WHERE` keyword, as in DuckDB's `sum(x) FILTER (x > 1)` (probed on
5168 /// 1.5.4). On for DuckDb/Lenient; the presence of the keyword round-trips via
5169 /// [`FunctionCall::filter_where`](crate::ast::FunctionCall::filter_where). Off for
5170 /// ANSI/PostgreSQL/SQLite, which require `FILTER (WHERE …)` — a keyword-less body then
5171 /// surfaces as a clean "expected `WHERE`" parse error. Independent of
5172 /// [`aggregate_filter`](Self::aggregate_filter): this only widens the *body* of a
5173 /// clause that gate already admits, so it is inert when the filter clause itself is off
5174 /// (MySQL).
5175 pub filter_optional_where: bool,
5176 /// Require a built-in aggregate's argument parentheses to be *adjacent* to its name
5177 /// for the aggregate-only argument forms — a leading `*`, a `DISTINCT`/`ALL` quantifier,
5178 /// the in-parenthesis `ORDER BY`, or the `SEPARATOR` tail. On for MySQL, off everywhere
5179 /// else. MySQL's default (`IGNORE_SPACE` off) tokenizer treats a space before the `(`
5180 /// as demoting a built-in aggregate to an ordinary/stored-function reference, where that
5181 /// aggregate-only argument grammar is illegal: `COUNT ( * )` / `MAX ( ALL 1 )` /
5182 /// `COUNT ( DISTINCT 1 )` are engine-measured `ER_PARSE_ERROR` (1064) on mysql:8, while
5183 /// the adjacent `COUNT(*)` accepts. A *normal*-argument spaced call is unaffected —
5184 /// `count (1)` still parses (it fails only at name resolution, a binding not a syntax
5185 /// error), so this narrowly rejects the aggregate-only forms rather than blanket-forbidding
5186 /// a space (which would over-reject the valid general-call form). When off, the name/paren
5187 /// gap is irrelevant and every dialect admits the aggregate forms with or without the space.
5188 /// Only the default `IGNORE_SPACE`-off mode is modelled; the runtime `sql_mode` toggle to
5189 /// `IGNORE_SPACE` on (which would accept the spaced aggregate forms) is out of scope.
5190 pub aggregate_args_require_adjacent_paren: bool,
5191 /// Accept the `IGNORE NULLS` / `RESPECT NULLS` null-treatment written *inside* a
5192 /// window/aggregate call's parentheses (DuckDB's `last(s IGNORE NULLS) OVER (…)`),
5193 /// riding the shared [`FunctionCall`](crate::ast::FunctionCall)'s
5194 /// [`null_treatment`](crate::ast::FunctionCall::null_treatment) field. On for
5195 /// DuckDb/Lenient. DuckDB spells it inside the parentheses (the SQL:2016 post-`)`
5196 /// position engine-rejects on 1.5.4), so it is parsed at the in-parenthesis tail after
5197 /// any `ORDER BY`. When off, `IGNORE`/`RESPECT` is left unconsumed and the unmatched `)`
5198 /// surfaces as a clean parse error. PostgreSQL has no null-treatment clause, so this
5199 /// stays a DuckDB extension rather than a shared PG form.
5200 pub null_treatment: bool,
5201 /// Reject an empty argument list `f()` on a MySQL built-in *aggregate* function —
5202 /// the closed set `MYSQL_AGGREGATE_FUNCTIONS` in the parser crate (`COUNT`, `SUM`,
5203 /// `AVG`, `MIN`, `MAX`, the `BIT_*`/`STD*`/`VAR*`/`VARIANCE` family, `GROUP_CONCAT`,
5204 /// `JSON_ARRAYAGG`/`JSON_OBJECTAGG`). MySQL's dedicated aggregate grammar requires at
5205 /// least one argument (or the `COUNT(*)` wildcard), so `COUNT()` is `ER_PARSE_ERROR`
5206 /// (1064) on mysql:8, while a niladic *non*-aggregate built-in (`NOW()`, `UUID()`,
5207 /// `PI()`) and an empty user-function call are accepted (the latter fails only at
5208 /// name resolution — a binding, not a syntax error — and a `CONCAT()`/`ABS()` empty
5209 /// call is a `wrong-parameter-count` semantic reject, also not a syntax error). On for
5210 /// MySQL, off elsewhere. When off, an empty aggregate call parses as an ordinary empty
5211 /// function call (every non-MySQL dialect admits it). Only a single *unquoted* name in
5212 /// the set matches — a backtick-quoted `` `count`() `` is a general call MySQL rejects
5213 /// at binding, not a syntax error, so it stays accepted — and the `COUNT(*)` wildcard
5214 /// and any argumented/quantified call are unaffected.
5215 pub aggregate_calls_reject_empty_arguments: bool,
5216 /// Restrict the `OVER (…)` window clause to MySQL's *windowable* functions — the
5217 /// built-in aggregates (`MYSQL_AGGREGATE_FUNCTIONS`) ∪ the dedicated window functions
5218 /// (`MYSQL_WINDOW_FUNCTIONS`: `ROW_NUMBER`, `RANK`, `DENSE_RANK`, `PERCENT_RANK`,
5219 /// `CUME_DIST`, `NTILE`, `LEAD`, `LAG`, `FIRST_VALUE`, `LAST_VALUE`, `NTH_VALUE`), both
5220 /// in the parser crate. MySQL grammatically admits `OVER` only on this set:
5221 /// `OVER` on an ordinary scalar built-in or a user function (`ABS(x) OVER ()`,
5222 /// `PERCENTILE_CONT(x, 0.5) OVER ()`, `ANY_VALUE(x) OVER ()`) is `ER_PARSE_ERROR`
5223 /// (1064) on mysql:8, while `SUM(x) OVER ()` / `ROW_NUMBER() OVER ()` /
5224 /// `GROUP_CONCAT(x) OVER ()` parse (they fail only at binding or a not-supported-yet
5225 /// semantic reject). On for MySQL, off elsewhere.
5226 ///
5227 /// The vocabulary must stay *complete*: an omission would over-*reject* a valid
5228 /// windowed call (the worse failure — no coverage-gap pin catches an over-rejection),
5229 /// so it is the engine-verified full MySQL 8.0 aggregate + window function list. A
5230 /// qualified name (`db.f(x) OVER ()`, engine-rejected too) is not a single-part member
5231 /// and is likewise rejected. When off, `OVER` attaches to any call, matching every
5232 /// non-MySQL dialect.
5233 ///
5234 /// This flag also gates the *converse* half of MySQL's dedicated window-function
5235 /// grammar — the requirements the pure window functions (`MYSQL_WINDOW_FUNCTIONS`,
5236 /// admitted as call heads by carving them out of `MYSQL_RESERVED_FUNCTION_NAME`) carry
5237 /// once admitted. Unlike the aggregates (whose `OVER` is optional), each window
5238 /// function *requires* an `OVER` clause, takes a *fixed* positional argument arity
5239 /// (`ROW_NUMBER`/`RANK`/`DENSE_RANK`/`PERCENT_RANK`/`CUME_DIST` exactly 0, `NTILE`/
5240 /// `FIRST_VALUE`/`LAST_VALUE` exactly 1, `LEAD`/`LAG` 1–3, `NTH_VALUE` exactly 2), and
5241 /// rejects the aggregate-only argument forms (`*`, a `DISTINCT`/`ALL` quantifier, an
5242 /// in-parenthesis `ORDER BY`, a `SEPARATOR`) — each violation an `ER_PARSE_ERROR`
5243 /// (1064) on mysql:8 (`ROW_NUMBER()` without `OVER`, `ROW_NUMBER(1) OVER ()`,
5244 /// `NTILE() OVER ()`, `RANK(DISTINCT a) OVER ()`). These are one indivisible dialect
5245 /// grammar, so they share the flag rather than a second knob that would have to
5246 /// co-vary with it. The parser enforces them in `parse_function_call`.
5247 pub over_requires_windowable_function: bool,
5248 /// Accept MySQL's window-function post-`)` tail — the SQL:2016
5249 /// `[FROM {FIRST | LAST}] [{RESPECT | IGNORE} NULLS]` clauses written *between* a
5250 /// null-treatment window function's argument `)` and its `OVER` clause, riding the
5251 /// shared [`FunctionCall`](crate::ast::FunctionCall)'s
5252 /// [`window_tail`](crate::ast::FunctionCall::window_tail) field. On for MySQL, off
5253 /// everywhere else. Engine-verified on mysql:8, the accepted surface is narrow: the
5254 /// null treatment is admitted only on `LEAD`/`LAG`/`FIRST_VALUE`/`LAST_VALUE`/
5255 /// `NTH_VALUE` and only as `RESPECT NULLS` (`IGNORE NULLS` grammar-admits but
5256 /// feature-rejects, `ER_NOT_SUPPORTED_YET` 1235); `FROM {FIRST | LAST}` is admitted
5257 /// only on `NTH_VALUE` and only as `FROM FIRST` (`FROM LAST` likewise 1235); the two
5258 /// clauses appear in that fixed order (the reverse is `ER_PARSE_ERROR`, 1064), and
5259 /// both sit strictly after the `)` (the in-paren spelling MySQL rejects, unlike
5260 /// DuckDB's [`null_treatment`](AggregateCallSyntax::null_treatment)). Keyed on a single
5261 /// *unquoted* window-function name — a quoted `` `nth_value` `` or qualified
5262 /// `db.nth_value` takes the general-call path (rejected there), matching the engine.
5263 /// When off, the tail keywords are left unconsumed and the trailing text surfaces as
5264 /// a clean parse error. The parser enforces the per-function admission in
5265 /// `parse_window_function_tail`.
5266 pub window_function_tail: bool,
5267 /// Accept a bare in-parenthesis `ORDER BY` as the sole content of a call's argument
5268 /// list — no positional argument preceding it (`rank(ORDER BY x) OVER w`,
5269 /// `cume_dist(ORDER BY x DESC) OVER w`). DuckDB lets a window/rank function carry its
5270 /// ordering inside the call parentheses instead of the `OVER (ORDER BY …)` clause; the
5271 /// [`order_by`](crate::ast::FunctionCall::order_by) list is the same field the
5272 /// arguments-then-`ORDER BY` form (`array_agg(x ORDER BY y)`) already fills, so the
5273 /// call shape is unchanged — only the empty *positional* list is new. On for
5274 /// DuckDb/Lenient, off elsewhere: standard SQL / PostgreSQL / MySQL require at least
5275 /// one argument before an aggregate `ORDER BY` (engine-verified), so when off the
5276 /// leading `ORDER` keyword falls into the argument-expression grammar where the
5277 /// reserved word surfaces as a clean parse error.
5278 ///
5279 /// A parse-level gate only. The per-function validity of the standalone form is a
5280 /// binding concern DuckDB enforces after parsing — `sum`/`array_agg`/`string_agg` and
5281 /// a bare `rank(ORDER BY x)` with no `OVER` are DuckDB *binder*/*catalog* rejects (the
5282 /// standalone form parses), so a parse-only parser correctly accepts them here; the one
5283 /// exception is DuckDB's parser-level "ORDER BY is not supported for the window function
5284 /// `dense_rank`", a fine-grained per-function restriction this gate deliberately does not
5285 /// model (see `duckdb-order-by-in-agg-args-trailing-comma`).
5286 pub standalone_argument_order_by: bool,
5287}
5288
5289/// Dialect-owned predicate-form syntax accepted by the parser.
5290///
5291/// The dialect-gated predicate forms beyond the always-available comparison / `IS` / `IN`
5292/// baseline. They are predicates (non-chaining, comparison precedence), distinct from the
5293/// [`ExpressionSyntax`] postfix/constructor forms, and — unlike those — not all PostgreSQL
5294/// extensions: some are SQL Core surface on in every shipped dialect (`LIKE`, SQL-92 /
5295/// SQL:2016 **Core E021-08**), while the rest are gated per dialect. Each is a pure grammar
5296/// gate: when off, the keyword is left unconsumed and the trailing operand surfaces as a
5297/// clean parse error — the same reject mechanism the other syntax gates use.
5298#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5299pub struct PredicateSyntax {
5300 /// Accept `<expr> IS [NOT] DISTINCT FROM <expr>`.
5301 pub is_distinct_from: bool,
5302 /// Accept `<expr> [NOT] LIKE <pattern> [ESCAPE <c>]` (SQL-92 core E021-08). On in
5303 /// every dialect — this is the standard pattern-match predicate.
5304 pub like: bool,
5305 /// Accept `<expr> [NOT] ILIKE <pattern> [ESCAPE <c>]` case-insensitive matching
5306 /// (PostgreSQL).
5307 pub ilike: bool,
5308 /// Accept `<expr> [NOT] SIMILAR TO <pattern> [ESCAPE <c>]` regex matching
5309 /// (SQL:1999 F841; PostgreSQL).
5310 pub similar_to: bool,
5311 /// Accept the SQL-standard `(s1, e1) OVERLAPS (s2, e2)` period predicate (SQL:2016
5312 /// F251) — the `row OVERLAPS row` form yielding a boolean, both operands
5313 /// exactly-two-element rows (a bare parenthesized pair or `ROW(...)`). A pure grammar
5314 /// gate: when off, the `OVERLAPS` keyword is left unconsumed and surfaces as a clean
5315 /// parse error. The parser enforces the two-element-row operand shape on both sides
5316 /// (a scalar, a single-element grouping `(a)`, or a three-element row is rejected,
5317 /// matching PostgreSQL's grammar-level wrong-arity error), so a value stored here only
5318 /// opens the predicate — validity of the operands is still checked. PostgreSQL-only
5319 /// among the shipped presets (DuckDB, MySQL, and SQLite all reject the form,
5320 /// engine-probed); the Lenient union carries it too.
5321 pub overlaps_period_predicate: bool,
5322 /// Accept DuckDB's unparenthesized `<expr> [NOT] IN <value>` list-membership operator
5323 /// ([`Expr::InExpr`](crate::ast::Expr::InExpr)) — `z IN y`, distinct from the standard
5324 /// parenthesized `IN (list)` / `IN (subquery)`. DuckDB-only: the right operand is a
5325 /// restricted `c_expr` that may not begin with a constant or unary sign (`IN 4` / `IN
5326 /// -5` are DuckDB parser errors), so the parser gates on the leading token. Off in
5327 /// every non-DuckDB preset (PostgreSQL and the standard require the parentheses).
5328 pub unparenthesized_in_list: bool,
5329 /// Accept a pattern-match predicate quantified over an array operand: `<expr>
5330 /// [NOT] LIKE|ILIKE {ANY | ALL | SOME} (<array>)`
5331 /// ([`Expr::QuantifiedLike`](crate::ast::Expr::QuantifiedLike)) — PostgreSQL's
5332 /// `ScalarArrayOpExpr` over the `~~`/`~~*` operator. `SIMILAR TO` has no
5333 /// quantified form (PostgreSQL rejects it, engine-probed), so only `LIKE`/`ILIKE`
5334 /// open it. A pure grammar gate: when off, the `ANY`/`ALL`/`SOME` head after a
5335 /// pattern operator is left unconsumed and surfaces as the usual reject at the
5336 /// reserved quantifier keyword. PostgreSQL-only among the shipped presets; the
5337 /// Lenient union carries it too.
5338 pub pattern_match_quantifier: bool,
5339 /// Accept the SQL-standard `SYMMETRIC`/`ASYMMETRIC` modifier on the range predicate:
5340 /// `<expr> [NOT] BETWEEN {SYMMETRIC | ASYMMETRIC} <low> AND <high>` (SQL:2016 T461).
5341 /// `SYMMETRIC` is load-bearing — it permits `low > high` by testing against the ordered
5342 /// pair — and is kept on [`Expr::Between`](crate::ast::Expr::Between)'s `symmetric` flag;
5343 /// the default `ASYMMETRIC` is a noise word dropped on parse. A pure grammar gate: when
5344 /// off, the modifier keyword after `BETWEEN` is left unconsumed and surfaces as a clean
5345 /// parse error. `SYMMETRIC` is an *optional* standard feature (T461), so it stays off in
5346 /// the strict ANSI baseline (and thus in MySQL/SQLite/ClickHouse/Snowflake, which reuse
5347 /// `PredicateSyntax::ANSI` and reject the modifier); on for PostgreSQL (engine-probed on
5348 /// pg_query) and the Lenient union.
5349 pub between_symmetric: bool,
5350 /// Accept the SQL-standard Unicode-normalization test `<expr> IS [NOT]
5351 /// [NFC|NFD|NFKC|NFKD] NORMALIZED` (SQL:2016 T061), parsed to the postfix
5352 /// [`Expr::IsNormalized`](crate::ast::Expr::IsNormalized) predicate. A pure grammar gate:
5353 /// when off, the `NORMALIZED` continuation after `IS [NOT]` is left unconsumed and the
5354 /// null/truth reading rejects it. An optional standard feature, so off in the strict ANSI
5355 /// baseline (and thus in MySQL/SQLite/ClickHouse/Snowflake, which reuse
5356 /// `PredicateSyntax::ANSI` and reject it); on for PostgreSQL (engine-probed on pg_query)
5357 /// and the Lenient union.
5358 pub is_normalized: bool,
5359 /// Accept an empty parenthesized `IN` list — `<expr> [NOT] IN ()` — with no elements,
5360 /// parsed to an [`Expr::InList`](crate::ast::Expr::InList) whose `list` is empty. SQLite
5361 /// evaluates `x IN ()` to false and `x NOT IN ()` to true (engine-measured via rusqlite
5362 /// 3.53.2, where both `prepare`-accept). A pure grammar gate: when off, the closing `)` in
5363 /// list position is left unconsumed and the required-first-element reject stands — the
5364 /// standard `IN` predicate demands at least one element, so ANSI/PostgreSQL/MySQL/DuckDB
5365 /// syntax-reject the empty list. On for SQLite and the Lenient union, off elsewhere.
5366 pub empty_in_list: bool,
5367 /// Accept the two-word postfix null test `<expr> NOT NULL` (a synonym for `IS NOT NULL`),
5368 /// folded onto [`Expr::IsNull`](crate::ast::Expr::IsNull) with `negated: true` and a
5369 /// [`NullTestSpelling::PostfixNotNull`](crate::ast::NullTestSpelling) tag so it round-trips.
5370 /// A `NOT`-led predicate spelling, parsed at comparison precedence alongside the other
5371 /// `NOT`-led predicates (`NOT IN`/`NOT LIKE`/`NOT BETWEEN`); when off, the `NOT NULL` run is
5372 /// left unconsumed and the `NOT` surfaces as the ordinary prefix operator (or a clean parse
5373 /// error), so the two-word form is rejected.
5374 ///
5375 /// Distinct from the one-word [`OperatorSyntax::null_test_postfix`](OperatorSyntax) gate
5376 /// because the surfaces diverge (engine-measured): SQLite and DuckDB accept both spellings,
5377 /// but PostgreSQL — despite accepting the one-word `ISNULL`/`NOTNULL` — rejects the two-word
5378 /// `NOT NULL` postfix. On for SQLite and the Lenient union; off elsewhere (including
5379 /// PostgreSQL; DuckDB's acceptance is tracked separately).
5380 pub null_test_two_word_postfix: bool,
5381}
5382
5383/// A frozen SQL-standard edition, used to anchor feature availability to a release
5384/// without enumerating features (ZetaSQL's `LanguageVersion` checkpoints).
5385///
5386/// These are the feature-ID-era editions: the `Ennn`/`Fnnn`/`Tnnn` feature taxonomy
5387/// was introduced in SQL:1999, so it is the earliest anchor. Declaration order is
5388/// chronological, so `Ord` reads as "released no later than".
5389#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
5390pub enum StandardVersion {
5391 /// SQL:1999 — first edition with the named-feature taxonomy.
5392 Sql1999,
5393 /// SQL:2003 — adds windowed table functions (OLAP), MERGE, sequences.
5394 Sql2003,
5395 /// SQL:2008.
5396 Sql2008,
5397 /// SQL:2011.
5398 Sql2011,
5399 /// SQL:2016 — the edition "generic"/ANSI is anchored on.
5400 Sql2016,
5401}
5402
5403/// Small, typed dialect data consumed by parser and renderer code.
5404///
5405/// # Preset permissiveness spectrum
5406///
5407/// The shipped presets run from the strict standard to a parse-anything union; a
5408/// caller picks by how much non-standard SQL it must accept. The `squonk` crate's
5409/// `BuiltinDialect::ALL` is the authoritative selectable list — this doc groups those
5410/// presets by how they are held honest:
5411///
5412/// - [`ANSI`](Self::ANSI) — the strict SQL:2016 standard baseline: the
5413/// principled neutral default, accepting only standard surface.
5414/// - `POSTGRES` / `MYSQL` / `SQLITE` / `DUCKDB` — one real dialect's surface, strict in
5415/// its own idiom (each gated behind its cargo feature). These, with `ANSI`, are the
5416/// oracle-compared presets: each is held to its real engine by a differential
5417/// accept/reject oracle, so over-acceptance is a measured, gated zero.
5418/// - `LENIENT` — the permissive "parse anything" catch-all: the documented maximal
5419/// union tooling reaches for on SQL of unknown origin (gated behind the `lenient`
5420/// feature).
5421/// - The conservative, no-oracle presets — `BIGQUERY`, `HIVE`, `CLICKHOUSE`, `DATABRICKS`,
5422/// `MSSQL`, `SNOWFLAKE`, `REDSHIFT` (each behind its cargo feature) — derive from `ANSI`
5423/// and enable only the
5424/// surface that already has a modelled, tested parser gate and documentary evidence
5425/// (their `ON`s are evidence-cited, not oracle-proven). This workspace ships no oracle
5426/// for these engines, so their over-acceptance cannot be measured; conservatism is the
5427/// honesty bar, and they are deliberately excluded from the oracle conformance sets —
5428/// unsupported syntax is a clean reject routed to a follow-up ticket, never a silent
5429/// over-accept. Each preset's module doc spells out exactly what it adds over `ANSI`.
5430///
5431/// There is deliberately **no** permissive "generic" preset (a vibe-union is
5432/// banned): `ANSI` *is* the generic/standard baseline, and `LENIENT` is the
5433/// honest, spelled-out permissive end. This differs from `datafusion-sqlparser-rs`,
5434/// whose `GenericDialect` is a permissive catch-all — the equivalent here is
5435/// `LENIENT`, not the standard baseline. The runtime name `"generic"` aliases `ANSI`;
5436/// see `BuiltinDialect::from_name` in the `squonk` crate for the migration mapping.
5437///
5438/// # Shared byte-trigger ownership: `#`
5439///
5440/// A single-meaning byte folds into one meaning-enum (the `^` axis is
5441/// [`caret_operator`](Self::caret_operator): exactly one of exponent / bitwise-XOR / none
5442/// per dialect). The `#` byte is **not** such a byte, and this is why its claimants stay
5443/// separate flags on their own sub-structs rather than a single `HashMeaning` enum: `#` is
5444/// a shared *lead* byte whose readings **coexist** in one dialect, partitioned by scan
5445/// phase and follow byte, not mutually exclusive. PostgreSQL enables bare-`#` XOR **and**
5446/// the `#>`/`#>>`/`#-` jsonb operators at once; a single-valued enum could not represent
5447/// both. Its five claimants, in resolution order:
5448///
5449/// 1. `#` line comment ([`CommentSyntax::line_comment_hash`], MySQL) — consumed as trivia
5450/// before tokenizing, so when on it shadows every reading below.
5451/// 2. `#`+identifier-start word (a [`byte_classes`](Self::byte_classes) table marking `#`
5452/// [`CLASS_IDENTIFIER_START`](lex_class::CLASS_IDENTIFIER_START), T-SQL `#temp`) — the
5453/// identifier scan precedes the operator/positional arms, so it wins on an identifier
5454/// follow byte.
5455/// 3. `#`+digit positional column ([`ExpressionSyntax::positional_column`], DuckDB `#1`).
5456/// 4. `#`+`>`/`-` jsonb path operators ([`OperatorSyntax::jsonb_operators`], PostgreSQL) —
5457/// maximal-munched ahead of a bare `#`, so disjoint from the readings below by follow
5458/// byte; reaches the operator scanner only when `#` is routed there by claimant 5.
5459/// 5. bare `#` bitwise-XOR operator ([`hash_bitwise_xor`](Self::hash_bitwise_xor),
5460/// PostgreSQL) — the fallthrough when no earlier partition claims the byte.
5461///
5462/// Claimants 3/4/5 partition by follow byte and coexist freely; the *genuine* collisions
5463/// are only where a claimant is silently shadowed — the trivia phase (1 vs 2/3/5) and the
5464/// scan-order overlap (positional 3 vs bare-XOR 5). Those are the tracked pairwise
5465/// [`LexicalConflict`]s ([`HashCommentVersusHashIdentifier`](LexicalConflict::HashCommentVersusHashIdentifier),
5466/// [`HashXorOperatorVersusHashComment`](LexicalConflict::HashXorOperatorVersusHashComment),
5467/// [`HashXorOperatorVersusPositionalColumn`](LexicalConflict::HashXorOperatorVersusPositionalColumn),
5468/// [`HashCommentVersusPositionalColumn`](LexicalConflict::HashCommentVersusPositionalColumn)),
5469/// asserted absent on every shipped preset. That registry — one flag per behaviour, plus a
5470/// conflict entry per silently-shadowing pair — is the correct model for a coexisting-lead
5471/// byte; a `HashMeaning` meaning-enum would falsely impose mutual exclusion.
5472#[derive(Clone, Debug, PartialEq, Eq)]
5473pub struct FeatureSet {
5474 /// Identity fold for unquoted identifiers; exact text remains interned.
5475 pub identifier_casing: Casing,
5476 /// Identifier quote styles the dialect accepts (one or more; symmetric or
5477 /// asymmetric). The tokenizer matches an opening delimiter against this set.
5478 pub identifier_quotes: &'static [IdentifierQuote],
5479 /// Dialect default when `NULLS FIRST`/`NULLS LAST` is omitted.
5480 pub default_null_ordering: NullOrdering,
5481 /// Keywords rejected as an unquoted column/table name or `ColId` alias
5482 /// (PostgreSQL `type_func_name ∪ reserved`). Per-position reservation
5483 /// (prod-keyword-position-reserved-sets): a keyword's admissibility depends on
5484 /// the grammatical position, so the single reserved set is replaced by four.
5485 pub reserved_column_name: KeywordSet,
5486 /// Keywords rejected as a function name (PostgreSQL `reserved`).
5487 pub reserved_function_name: KeywordSet,
5488 /// Keywords rejected as a (user-defined) type name (PostgreSQL
5489 /// `col_name ∪ reserved`).
5490 pub reserved_type_name: KeywordSet,
5491 /// Keywords rejected as a bare column alias — one written without `AS`
5492 /// (PostgreSQL `AS_LABEL`). `AS`-introduced aliases (`ColLabel`) admit every
5493 /// keyword, so they need no reject set.
5494 pub reserved_bare_alias: KeywordSet,
5495 /// Keywords rejected in `ColLabel` position — an `AS`-introduced alias
5496 /// (`SELECT 1 AS <label>`) and a dotted-name continuation part (`schema.<part>`,
5497 /// `x.<part>`). PostgreSQL admits *every* keyword there (`SELECT a AS select` is
5498 /// valid), so its set is [`KeywordSet::EMPTY`]; the same holds for every
5499 /// PostgreSQL-family preset. SQLite draws no `ColId`/`ColLabel` split — a reserved
5500 /// word is rejected in every name position uniformly — so it reuses its single
5501 /// reserved set here, making `SELECT 1 AS delete` / `SELECT x.update` /
5502 /// `FROM schema.case` the parse errors SQLite reports (engine-measured via rusqlite).
5503 pub reserved_as_label: KeywordSet,
5504 /// Whether a relation (table / index / view) name admits a *catalog* qualifier —
5505 /// the third dotted part, `catalog.schema.table`. On for ANSI/PostgreSQL/MySQL/
5506 /// DuckDB/Lenient, which cap a relation at three parts (a fourth is rejected). Off
5507 /// for SQLite, whose relation names are `schema.table` at most (a database *is* the
5508 /// schema namespace), so a three-part `a.b.c` in table/index position is the syntax
5509 /// error SQLite reports (engine-measured via rusqlite). Column references reach one
5510 /// part deeper through composite-field selection — a different grammar position —
5511 /// and are never bounded by this flag.
5512 pub catalog_qualified_names: bool,
5513 /// Byte classes used by tokenizer dispatch.
5514 pub byte_classes: ByteClasses,
5515 /// Binary and prefix operator binding powers used by parser and renderer.
5516 pub binding_powers: BindingPowerTable,
5517 /// Set-operation binding powers used by parser and renderer.
5518 pub set_operation_powers: SetOperationBindingPowerTable,
5519 /// String literal syntax extensions accepted by the tokenizer/parser.
5520 pub string_literals: StringLiteralSyntax,
5521 /// Numeric literal syntax extensions accepted by the tokenizer.
5522 pub numeric_literals: NumericLiteralSyntax,
5523 /// Prepared-statement parameter placeholder forms accepted by the tokenizer.
5524 pub parameters: ParameterSyntax,
5525 /// MySQL session-variable forms (`@name` user variables, `@@[scope.]name` system
5526 /// variables) accepted by the tokenizer.
5527 pub session_variables: SessionVariableSyntax,
5528 /// Unquoted-identifier character policy (the dialect-variable `$`) accepted by the
5529 /// tokenizer.
5530 pub identifier_syntax: IdentifierSyntax,
5531 /// Table-expression syntax forms accepted by the parser.
5532 pub table_expressions: TableExpressionSyntax,
5533 /// Join-operator and recursive-query relation-composition syntax, gathered from
5534 /// [`table_expressions`](Self::table_expressions).
5535 pub join_syntax: JoinSyntax,
5536 /// Table-factor `FROM`-item forms beyond a plain named table, gathered from
5537 /// [`table_expressions`](Self::table_expressions).
5538 pub table_factor_syntax: TableFactorSyntax,
5539 /// Expression postfix and constructor forms (typecast, subscript, COLLATE, AT TIME
5540 /// ZONE, array/row constructors, field selection, typed literals) accepted by the
5541 /// parser. The operator spellings and call-tail forms are separate dimensions —
5542 /// see [`operator_syntax`](Self::operator_syntax) and [`call_syntax`](Self::call_syntax).
5543 pub expression_syntax: ExpressionSyntax,
5544 /// Infix/prefix operator forms (`OPERATOR(…)`, `@>`/`<@`, `->`/`->>`, `==`, general
5545 /// `IS`, `<=>`, the DuckDB lambda, the bitwise family, quantified comparisons)
5546 /// accepted by the parser.
5547 pub operator_syntax: OperatorSyntax,
5548 /// Function-call forms accepted by the parser (named arguments, cast/convert shape,
5549 /// `UTC_*`/`COLUMNS(…)`/`EXTRACT(… FROM …)` special calls, …).
5550 /// Aggregate tails (`SEPARATOR`/`WITHIN GROUP`/`FILTER`/`OVER` eligibility) live in
5551 /// [`aggregate_call_syntax`](Self::aggregate_call_syntax); keyword string specials live
5552 /// in [`string_func_forms`](Self::string_func_forms).
5553 pub call_syntax: CallSyntax,
5554 /// Keyword string/scalar special-form syntax — the flags whose sole AST product
5555 /// is a [`StringFunc`](crate::ast::StringFunc).
5556 pub string_func_forms: StringFuncForms,
5557 /// Aggregate/window function-call syntax — the in-parenthesis and post-`)` tails,
5558 /// the argument-shape/arity restrictions, and the `OVER`-eligibility gate.
5559 pub aggregate_call_syntax: AggregateCallSyntax,
5560 /// Pattern-match predicate forms (`LIKE`/`ILIKE`/`SIMILAR TO`) accepted by the
5561 /// parser. `LIKE` is SQL core (on everywhere); `ILIKE`/`SIMILAR TO` are gated.
5562 pub predicate_syntax: PredicateSyntax,
5563 /// What the `||` operator token means: string concatenation or logical OR.
5564 pub pipe_operator: PipeOperator,
5565 /// What the `&&` operator token means: logical AND, or not an operator.
5566 pub double_ampersand: DoubleAmpersand,
5567 /// Which dialect's keyword infix operators are recognized (MySQL's
5568 /// `DIV`/`MOD`/`XOR`/`RLIKE`/`REGEXP`, SQLite's `GLOB`/`MATCH`/`REGEXP`, or none) —
5569 /// each variant names one dialect's exact set; see [`KeywordOperators`].
5570 pub keyword_operators: KeywordOperators,
5571 /// What the `^` operator token means: arithmetic exponentiation, bitwise XOR, or no
5572 /// infix meaning. The `^` byte always lexes; this is the sole owner of its infix
5573 /// reading — the former split across an `exponent_operator` bool and a `Caret`-XOR
5574 /// spelling is folded here, so the "both power and XOR" state is unrepresentable. See
5575 /// [`CaretOperator`].
5576 pub caret_operator: CaretOperator,
5577 /// Whether a bare `#` lexes and parses as the bitwise-XOR operator
5578 /// ([`BinaryOperator::BitwiseXor`] under the [`Hash`](BitwiseXorSpelling::Hash)
5579 /// spelling, PostgreSQL). A different byte from [`caret_operator`](Self::caret_operator)'s
5580 /// `^`: PostgreSQL spells XOR `#`, MySQL spells it `^`, and neither accepts the other's
5581 /// spelling. Also gates the tokenizer — `#` reaches the operator scanner only under this
5582 /// flag — so it is one of the `#`-trigger claimants tracked in [`LexicalConflict`]
5583 /// (against a `#` line comment and DuckDB's `#n` positional column). Off for every
5584 /// shipped dialect but PostgreSQL.
5585 pub hash_bitwise_xor: bool,
5586 /// Dialect comment syntax extensions accepted by the tokenizer.
5587 pub comment_syntax: CommentSyntax,
5588 /// Mutation-statement (`INSERT`/`UPDATE`/`DELETE`) syntax forms accepted by the
5589 /// parser (`RETURNING`, `ON CONFLICT`).
5590 pub mutation_syntax: MutationSyntax,
5591 /// Whole-statement DDL dispatch gates (non-`TABLE` `CREATE`/`DROP` object dispatch),
5592 /// split from the retired `SchemaChangeSyntax`.
5593 pub statement_ddl_gates: StatementDdlGates,
5594 /// View/sequence clause syntax after a DDL statement head has been chosen —
5595 /// temporary/recursive views, view options, matview targets, sequence `CACHE`.
5596 pub view_sequence_clause_syntax: ViewSequenceClauseSyntax,
5597 /// `CREATE TABLE` table-level clause syntax (storage/CTAS/partitioning/inheritance/
5598 /// persistence clauses), split from the retired `SchemaChangeSyntax`.
5599 pub create_table_clause_syntax: CreateTableClauseSyntax,
5600 /// `CREATE TABLE` column-definition syntax (generated/identity columns, SQLite column
5601 /// attributes, `DEFAULT`/`COLLATE`/`STORAGE` clauses), split from `SchemaChangeSyntax`.
5602 pub column_definition_syntax: ColumnDefinitionSyntax,
5603 /// Table/column-constraint syntax (deferrable/named/bare constraints, `EXCLUDE`, the
5604 /// `NO INHERIT`/`NOT VALID` markers, index parameters), split from `SchemaChangeSyntax`.
5605 pub constraint_syntax: ConstraintSyntax,
5606 /// `CREATE INDEX`/`ALTER TABLE`/`DROP` syntax (index clauses, the extended `ALTER`
5607 /// surface, drop behaviour, routine arg types), split from `SchemaChangeSyntax`.
5608 pub index_alter_syntax: IndexAlterSyntax,
5609 /// `IF [NOT] EXISTS` existence guards on DDL statements (`DROP`/`ALTER IF EXISTS`,
5610 /// `CREATE VIEW`/`CREATE DATABASE IF NOT EXISTS`), gathered from the schema-change
5611 /// surface into one per-site table.
5612 pub existence_guards: ExistenceGuards,
5613 /// SELECT-body syntax forms accepted by the parser (`DISTINCT ON`, `QUALIFY`,
5614 /// projection aliases, set-op quantifiers, …). Row-limiting / locking / pipe tails
5615 /// live in [`query_tail_syntax`](Self::query_tail_syntax); `GROUP BY`/`ORDER BY`
5616 /// modes live in [`grouping_syntax`](Self::grouping_syntax).
5617 pub select_syntax: SelectSyntax,
5618 /// Query-tail syntax — the row-limiting/locking family and the trailing
5619 /// ClickHouse/pipe clauses parsed after the SELECT body.
5620 pub query_tail_syntax: QueryTailSyntax,
5621 /// `GROUP BY`/`ORDER BY` grouping-and-ordering syntax (grouping sets, clause
5622 /// modes, and quantifiers).
5623 pub grouping_syntax: GroupingSyntax,
5624 /// Utility-statement syntax forms the parser dispatches: the
5625 /// PostgreSQL `COPY`/`COMMENT ON` and SQLite `PRAGMA`/`ATTACH`/`DETACH`
5626 /// statement gates.
5627 pub utility_syntax: UtilitySyntax,
5628 /// Transaction-control (TCL) syntax — openers, completers, savepoints, mode lists,
5629 /// and XA forms — split from [`utility_syntax`](Self::utility_syntax).
5630 pub transaction_syntax: TransactionSyntax,
5631 /// SHOW/DESCRIBE introspection-statement syntax (the session `SHOW` reader, the typed
5632 /// `SHOW <object>` listings, and `DESCRIBE`/`SUMMARIZE`), gathered from
5633 /// [`utility_syntax`](Self::utility_syntax).
5634 pub show_syntax: ShowSyntax,
5635 /// Physical-maintenance-statement syntax (`VACUUM`/`REINDEX`/`ANALYZE`/`CHECKPOINT`),
5636 /// gathered from [`utility_syntax`](Self::utility_syntax).
5637 pub maintenance_syntax: MaintenanceSyntax,
5638 /// Access-control-statement syntax (`GRANT`/`REVOKE` and the extended object grammar),
5639 /// gathered from [`utility_syntax`](Self::utility_syntax).
5640 pub access_control_syntax: AccessControlSyntax,
5641 /// Type-name vocabulary the parser recognizes beyond the shared standard set
5642 /// (the MySQL `TINYINT`/`ENUM`/`UNSIGNED`/… surface).
5643 pub type_name_syntax: TypeNameSyntax,
5644 /// Which dialect's canonical surface spelling a [`RenderSpelling::TargetDialect`]
5645 /// render emits for constructs whose spelling diverges across dialects (today the
5646 /// type names). Read by the renderer in place of recognizing a preset by identity,
5647 /// so PostgreSQL-vs-ANSI output spelling is pure dialect data, not a cfg-gated
5648 /// `FeatureSet::POSTGRES` comparison.
5649 ///
5650 /// [`RenderSpelling::TargetDialect`]: crate::render::RenderSpelling::TargetDialect
5651 pub target_spelling: TargetSpelling,
5652}
5653
5654impl FeatureSet {
5655 /// The ANSI/standard config baseline as of SQL edition `version`.
5656 ///
5657 /// The M1 dialect-data surface (identifier quoting, casing, concatenation, set
5658 /// operations, …) is invariant across feature-ID-era editions — every standard
5659 /// feature we implement is SQL:1999 Core — so this returns [`FeatureSet::ANSI`]
5660 /// for every edition today. It is the stable anchor consumers pin against; the
5661 /// exhaustive `match` forces a decision here once edition-varying dialect data
5662 /// is added. For the set of *standard features* available as of an edition
5663 /// (which does vary), query [`standard_features_as_of`].
5664 ///
5665 /// Per-dialect *release* pinning (e.g. PostgreSQL 14 vs 16) is a distinct axis
5666 /// that needs multi-version dialect data; it arrives with the later milestones.
5667 pub const fn as_of(version: StandardVersion) -> Self {
5668 match version {
5669 StandardVersion::Sql1999
5670 | StandardVersion::Sql2003
5671 | StandardVersion::Sql2008
5672 | StandardVersion::Sql2011
5673 | StandardVersion::Sql2016 => Self::ANSI,
5674 }
5675 }
5676
5677 /// Return all lexical classes for `byte` under this dialect.
5678 pub const fn byte_class(&self, byte: u8) -> u8 {
5679 self.byte_classes.byte_class(byte)
5680 }
5681
5682 /// Return true if `byte` has any lexical class in `mask` under this dialect.
5683 pub const fn has_byte_class(&self, byte: u8, mask: u8) -> bool {
5684 self.byte_classes.has_class(byte, mask)
5685 }
5686
5687 /// Return the binary binding power for `op` under this dialect.
5688 pub const fn binding_power(&self, op: &BinaryOperator) -> BindingPower {
5689 self.binding_powers.binary(op)
5690 }
5691
5692 /// Return the prefix binding power for `op` under this dialect.
5693 pub const fn prefix_binding_power(&self, op: &UnaryOperator) -> u8 {
5694 self.binding_powers.prefix(op)
5695 }
5696
5697 /// Return the set-operation binding power for `op` under this dialect.
5698 pub const fn set_operation_binding_power(&self, op: &SetOperator) -> BindingPower {
5699 self.set_operation_powers.set_operation(op)
5700 }
5701
5702 /// Fold an unquoted identifier according to this dialect's identity rules.
5703 pub fn fold_unquoted_identifier<'a>(&self, identifier: &'a str) -> Cow<'a, str> {
5704 self.identifier_casing.fold_identifier(identifier)
5705 }
5706
5707 /// Whether omitted `NULLS FIRST`/`NULLS LAST` sorts nulls first.
5708 pub const fn default_nulls_first(&self) -> bool {
5709 self.default_null_ordering.nulls_first()
5710 }
5711}
5712
5713fn fold_identifier_upper(identifier: &str) -> Cow<'_, str> {
5714 if identifier.bytes().any(|byte| byte.is_ascii_lowercase()) {
5715 Cow::Owned(identifier.to_ascii_uppercase())
5716 } else {
5717 Cow::Borrowed(identifier)
5718 }
5719}
5720
5721fn fold_identifier_lower(identifier: &str) -> Cow<'_, str> {
5722 if identifier.bytes().any(|byte| byte.is_ascii_uppercase()) {
5723 Cow::Owned(identifier.to_ascii_lowercase())
5724 } else {
5725 Cow::Borrowed(identifier)
5726 }
5727}
5728
5729// `FeatureDelta`, `FeatureSet::with`, and the
5730// `Feature` / `FeatureMetadata` registry below are the per-field boilerplate that
5731// drifted each time a `FeatureSet` dimension was added (the delta mirror, the
5732// builder setters, and the enum/`id`/`ALL`/metadata registry). The thin include
5733// below pulls in that generated module: it is derived from the `FeatureSet` struct
5734// plus an annotation table (ADR-0013 drift gate; ADR-0011 self-describing dialect
5735// data) and re-exported here, so the public dialect API is unchanged. To add a
5736// dimension: add the struct field above and its `FEATURE_FIELDS` annotation in
5737// `crates/squonk-sourcegen/src/feature_set.rs`, then run
5738// `cargo run -p squonk-sourcegen`.
5739mod feature_set_generated;
5740pub use feature_set_generated::{FEATURE_METADATA, FEATURES, Feature, FeatureDelta};
5741
5742mod conflict;
5743mod head_contention;
5744mod standard_catalog;
5745mod support_tier;
5746
5747pub use conflict::{FeatureDependencyViolation, GrammarConflict, LexicalConflict};
5748pub use head_contention::{
5749 BASE_VS_FEATURE_STATEMENT_HEADS, BaseVsFeatureStatementHead, FeatureGatePredicate,
5750 HeadResolution, MULTI_CLAIMANT_STATEMENT_HEADS, MultiClaimantHead, NamedFeatureGate,
5751};
5752pub use standard_catalog::{
5753 Conformance, FeatureMetadata, Maturity, STANDARD_FEATURE_CATALOG, StandardFeature,
5754 max_feature_metadata, standard_feature, standard_features_as_of, unsupported_standard_features,
5755};
5756pub use support_tier::{SupportEvidence, SupportTier};
5757
5758#[cfg(test)]
5759mod tests {
5760 use super::*;
5761 use lex_class::{
5762 CLASS_DIGIT, CLASS_IDENTIFIER_CONTINUE, CLASS_IDENTIFIER_START, CLASS_OPERATOR,
5763 CLASS_PUNCTUATION, CLASS_WHITESPACE, CLASS_WHITESPACE_BOUNDARY, CLASS_WHITESPACE_CONTINUE,
5764 byte_class, has_class,
5765 };
5766
5767 #[test]
5768 fn ansi_and_postgres_differ_in_identifier_casing() {
5769 assert_eq!(FeatureSet::ANSI.identifier_casing, Casing::Upper);
5770 assert_eq!(FeatureSet::POSTGRES.identifier_casing, Casing::Lower);
5771 assert_ne!(
5772 FeatureSet::ANSI.identifier_casing,
5773 FeatureSet::POSTGRES.identifier_casing
5774 );
5775 }
5776
5777 #[test]
5778 fn identifier_casing_folds_unquoted_identifier_identity() {
5779 assert_eq!(FeatureSet::ANSI.fold_unquoted_identifier("MiXeD"), "MIXED");
5780 assert_eq!(
5781 FeatureSet::POSTGRES.fold_unquoted_identifier("MiXeD"),
5782 "mixed",
5783 );
5784 assert_eq!(
5785 FeatureSet::ANSI
5786 .with(FeatureDelta::EMPTY.identifier_casing(Casing::Preserve))
5787 .fold_unquoted_identifier("MiXeD"),
5788 "MiXeD",
5789 );
5790 assert_eq!(
5791 FeatureSet::ANSI.fold_unquoted_identifier("CAFE"),
5792 "CAFE",
5793 "already-folded ASCII identifiers stay borrowed-equivalent",
5794 );
5795 }
5796
5797 #[test]
5798 fn default_null_ordering_exposes_sort_behavior() {
5799 assert!(!FeatureSet::ANSI.default_nulls_first());
5800 assert!(
5801 FeatureSet::ANSI
5802 .with(FeatureDelta::EMPTY.default_null_ordering(NullOrdering::NullsFirst))
5803 .default_nulls_first()
5804 );
5805 }
5806
5807 #[test]
5808 fn delta_customizes_from_base_preset() {
5809 let custom = FeatureSet::ANSI.with(
5810 FeatureDelta::EMPTY
5811 .identifier_casing(Casing::Preserve)
5812 .default_null_ordering(NullOrdering::NullsFirst),
5813 );
5814
5815 assert_eq!(custom.identifier_casing, Casing::Preserve);
5816 assert_eq!(custom.default_null_ordering, NullOrdering::NullsFirst);
5817 assert_eq!(custom.identifier_quotes, FeatureSet::ANSI.identifier_quotes);
5818 assert_eq!(
5819 custom.reserved_column_name,
5820 FeatureSet::ANSI.reserved_column_name
5821 );
5822 assert_eq!(
5823 custom.reserved_bare_alias,
5824 FeatureSet::ANSI.reserved_bare_alias
5825 );
5826 assert_eq!(custom.byte_classes, FeatureSet::ANSI.byte_classes);
5827 assert_eq!(custom.binding_powers, FeatureSet::ANSI.binding_powers);
5828 assert_eq!(
5829 custom.set_operation_powers,
5830 FeatureSet::ANSI.set_operation_powers
5831 );
5832 assert_eq!(custom.string_literals, FeatureSet::ANSI.string_literals);
5833 assert_eq!(custom.numeric_literals, FeatureSet::ANSI.numeric_literals);
5834 assert_eq!(custom.parameters, FeatureSet::ANSI.parameters);
5835 assert_eq!(custom.table_expressions, FeatureSet::ANSI.table_expressions);
5836 assert_eq!(custom.double_ampersand, FeatureSet::ANSI.double_ampersand);
5837 assert_eq!(custom.comment_syntax, FeatureSet::ANSI.comment_syntax);
5838 assert_eq!(custom.pipe_operator, FeatureSet::ANSI.pipe_operator);
5839 }
5840
5841 #[test]
5842 fn identifier_quote_styles_expose_open_and_close() {
5843 assert_eq!(
5844 FeatureSet::ANSI.identifier_quotes,
5845 STANDARD_IDENTIFIER_QUOTES
5846 );
5847 assert_eq!(
5848 FeatureSet::POSTGRES.identifier_quotes,
5849 STANDARD_IDENTIFIER_QUOTES
5850 );
5851
5852 let double = IdentifierQuote::Symmetric('"');
5853 assert_eq!((double.open(), double.close()), ('"', '"'));
5854
5855 let bracket = IdentifierQuote::Asymmetric {
5856 open: '[',
5857 close: ']',
5858 };
5859 assert_eq!((bracket.open(), bracket.close()), ('[', ']'));
5860 }
5861
5862 #[test]
5863 fn string_literal_syntax_is_explicit_dialect_data() {
5864 assert_eq!(FeatureSet::ANSI.string_literals, StringLiteralSyntax::ANSI);
5865 assert_eq!(
5866 FeatureSet::POSTGRES.string_literals,
5867 StringLiteralSyntax::POSTGRES,
5868 );
5869 assert_ne!(
5870 FeatureSet::ANSI.string_literals,
5871 FeatureSet::POSTGRES.string_literals,
5872 );
5873 }
5874
5875 #[test]
5876 fn numeric_literal_syntax_is_explicit_dialect_data() {
5877 // The radix/separator forms are off in both baselines (PostgreSQL gates
5878 // them by release), so the dimension is exercised via an explicit delta.
5879 assert_eq!(
5880 FeatureSet::ANSI.numeric_literals,
5881 NumericLiteralSyntax::ANSI
5882 );
5883 let hex =
5884 FeatureSet::ANSI.with(FeatureDelta::EMPTY.numeric_literals(NumericLiteralSyntax {
5885 hex_integers: true,
5886 ..NumericLiteralSyntax::ANSI
5887 }));
5888 assert!(hex.numeric_literals.hex_integers);
5889 assert_ne!(hex.numeric_literals, FeatureSet::ANSI.numeric_literals);
5890 }
5891
5892 #[test]
5893 fn parameter_syntax_is_explicit_dialect_data() {
5894 // ANSI recognises no placeholder forms; PostgreSQL enables positional `$n`
5895 // but not `?`. Bind to a local so the field checks are not const-folded into
5896 // a clippy `assertions_on_constants` lint.
5897 assert_eq!(FeatureSet::ANSI.parameters, ParameterSyntax::ANSI);
5898 assert_eq!(FeatureSet::POSTGRES.parameters, ParameterSyntax::POSTGRES);
5899 let postgres = FeatureSet::POSTGRES.parameters;
5900 assert!(postgres.positional_dollar);
5901 assert!(!postgres.anonymous_question);
5902 assert_ne!(FeatureSet::ANSI.parameters, FeatureSet::POSTGRES.parameters);
5903
5904 let anon = FeatureSet::ANSI.with(FeatureDelta::EMPTY.parameters(ParameterSyntax {
5905 anonymous_question: true,
5906 ..ParameterSyntax::ANSI
5907 }));
5908 assert!(anon.parameters.anonymous_question);
5909 assert_ne!(anon.parameters, FeatureSet::ANSI.parameters);
5910 }
5911
5912 #[test]
5913 fn identifier_syntax_is_explicit_dialect_data() {
5914 // The `$`-in-identifier policy is the dialect-variable part of the identifier
5915 // rule: ANSI forbids `$`, PostgreSQL accepts it mid-identifier. Bind to locals
5916 // so the field checks are not const-folded into a clippy
5917 // `assertions_on_constants` lint.
5918 assert_eq!(FeatureSet::ANSI.identifier_syntax, IdentifierSyntax::ANSI);
5919 assert_eq!(
5920 FeatureSet::POSTGRES.identifier_syntax,
5921 IdentifierSyntax::POSTGRES,
5922 );
5923 let ansi = FeatureSet::ANSI.identifier_syntax;
5924 let postgres = FeatureSet::POSTGRES.identifier_syntax;
5925 assert!(!ansi.dollar_in_identifiers);
5926 assert!(postgres.dollar_in_identifiers);
5927 assert_ne!(ansi, postgres);
5928
5929 // The SQLite string-literal identifier misfeature is SQLite/Lenient-only; every
5930 // other shipped preset syntax-rejects a string in a name position, so it stays off.
5931 // Bound to locals so the check is a runtime `assert!`, not the `assertions_on_constants`
5932 // lint's const-value form.
5933 let sqlite = FeatureSet::SQLITE.identifier_syntax;
5934 let lenient = FeatureSet::LENIENT.identifier_syntax;
5935 let mysql = FeatureSet::MYSQL.identifier_syntax;
5936 assert!(sqlite.string_literal_identifiers);
5937 assert!(lenient.string_literal_identifiers);
5938 assert!(!ansi.string_literal_identifiers);
5939 assert!(!postgres.string_literal_identifiers);
5940 assert!(!mysql.string_literal_identifiers);
5941
5942 // The policy toggles independently of the base preset.
5943 let dollars =
5944 FeatureSet::ANSI.with(FeatureDelta::EMPTY.identifier_syntax(IdentifierSyntax {
5945 dollar_in_identifiers: true,
5946 ..IdentifierSyntax::ANSI
5947 }));
5948 assert!(dollars.identifier_syntax.dollar_in_identifiers);
5949 assert_ne!(
5950 dollars.identifier_syntax,
5951 FeatureSet::ANSI.identifier_syntax
5952 );
5953 }
5954
5955 #[test]
5956 fn table_expression_syntax_is_explicit_dialect_data() {
5957 assert_eq!(
5958 FeatureSet::ANSI.table_expressions,
5959 TableExpressionSyntax::ANSI
5960 );
5961 assert_eq!(
5962 FeatureSet::POSTGRES.table_expressions,
5963 TableExpressionSyntax::POSTGRES
5964 );
5965 assert_ne!(
5966 FeatureSet::ANSI.table_expressions,
5967 FeatureSet::POSTGRES.table_expressions
5968 );
5969 }
5970
5971 #[test]
5972 fn pipe_operator_is_explicit_dialect_data() {
5973 assert_eq!(FeatureSet::ANSI.pipe_operator, PipeOperator::StringConcat);
5974 assert_eq!(
5975 FeatureSet::POSTGRES.pipe_operator,
5976 PipeOperator::StringConcat
5977 );
5978
5979 let mysql_like =
5980 FeatureSet::ANSI.with(FeatureDelta::EMPTY.pipe_operator(PipeOperator::LogicalOr));
5981 assert_eq!(mysql_like.pipe_operator, PipeOperator::LogicalOr);
5982
5983 // `||` maps to one canonical operator; `OR`'s looser binding power follows.
5984 assert_eq!(
5985 PipeOperator::StringConcat.binary_operator(),
5986 BinaryOperator::StringConcat,
5987 );
5988 assert_eq!(
5989 PipeOperator::LogicalOr.binary_operator(),
5990 BinaryOperator::Or
5991 );
5992 }
5993
5994 #[test]
5995 fn double_ampersand_is_explicit_dialect_data() {
5996 // ANSI/PostgreSQL do not treat `&&` as a scalar operator.
5997 assert_eq!(
5998 FeatureSet::ANSI.double_ampersand,
5999 DoubleAmpersand::Unsupported
6000 );
6001 assert_eq!(DoubleAmpersand::Unsupported.binary_operator(), None);
6002
6003 // A MySQL-like dialect maps `&&` to the canonical `AND` operator, so its
6004 // `AND` binding power follows automatically (no precedence override).
6005 let mysql_like = FeatureSet::ANSI
6006 .with(FeatureDelta::EMPTY.double_ampersand(DoubleAmpersand::LogicalAnd));
6007 assert_eq!(mysql_like.double_ampersand, DoubleAmpersand::LogicalAnd);
6008 assert_eq!(
6009 DoubleAmpersand::LogicalAnd.binary_operator(),
6010 Some(BinaryOperator::And),
6011 );
6012 }
6013
6014 #[test]
6015 fn comment_syntax_is_explicit_dialect_data() {
6016 assert_eq!(FeatureSet::ANSI.comment_syntax, CommentSyntax::ANSI);
6017
6018 let mysql_like = FeatureSet::ANSI.with(FeatureDelta::EMPTY.comment_syntax(CommentSyntax {
6019 line_comment_hash: true,
6020 ..CommentSyntax::ANSI
6021 }));
6022 assert!(mysql_like.comment_syntax.line_comment_hash);
6023 assert_ne!(mysql_like.comment_syntax, FeatureSet::ANSI.comment_syntax);
6024
6025 // The MySQL comment shape is data, not tokenizer hard-coding: block comments
6026 // stop nesting and `/*!…*/` becomes conditional inclusion, while the ANSI
6027 // baseline keeps the permissive nesting and no versioned form.
6028 let ansi = FeatureSet::ANSI.comment_syntax;
6029 let mysql = FeatureSet::MYSQL.comment_syntax;
6030 assert!(ansi.nested_block_comments);
6031 assert_eq!(ansi.versioned_comments, None);
6032 assert!(!mysql.nested_block_comments);
6033 assert_eq!(
6034 mysql.versioned_comments,
6035 Some(CommentSyntax::MYSQL_8_VERSION_BOUND)
6036 );
6037 }
6038
6039 #[test]
6040 fn select_syntax_is_explicit_dialect_data() {
6041 // `DISTINCT ON` is PostgreSQL-only; the fetch-first row-limiting spelling is
6042 // standard and on in both presets. Bind to locals so the field checks are not
6043 // const-folded into a clippy `assertions_on_constants` lint.
6044 assert_eq!(FeatureSet::ANSI.select_syntax, SelectSyntax::ANSI);
6045 assert_eq!(FeatureSet::POSTGRES.select_syntax, SelectSyntax::POSTGRES);
6046 let ansi = FeatureSet::ANSI.select_syntax;
6047 let postgres = FeatureSet::POSTGRES.select_syntax;
6048 let ansi_qt = FeatureSet::ANSI.query_tail_syntax;
6049 assert!(!ansi.distinct_on);
6050 assert!(postgres.distinct_on);
6051 assert!(ansi_qt.fetch_first);
6052 assert_ne!(ansi, postgres);
6053
6054 // The fetch-first gate can be turned off independently of the base preset.
6055 let no_fetch =
6056 FeatureSet::ANSI.with(FeatureDelta::EMPTY.query_tail_syntax(QueryTailSyntax {
6057 fetch_first: false,
6058 ..QueryTailSyntax::ANSI
6059 }));
6060 assert!(!no_fetch.query_tail_syntax.fetch_first);
6061 assert_ne!(
6062 no_fetch.query_tail_syntax,
6063 FeatureSet::ANSI.query_tail_syntax
6064 );
6065 }
6066
6067 #[test]
6068 fn utility_syntax_is_explicit_dialect_data() {
6069 // `COPY` and `COMMENT ON` are PostgreSQL-only: the ANSI baseline leaves both off,
6070 // PostgreSQL turns both on, and MySQL reuses the ANSI off value. Bind to locals so
6071 // the field checks are not const-folded into a clippy `assertions_on_constants`
6072 // lint.
6073 assert_eq!(FeatureSet::ANSI.utility_syntax, UtilitySyntax::ANSI);
6074 assert_eq!(FeatureSet::POSTGRES.utility_syntax, UtilitySyntax::POSTGRES);
6075 let ansi = FeatureSet::ANSI.utility_syntax;
6076 let postgres = FeatureSet::POSTGRES.utility_syntax;
6077 assert!(!ansi.copy);
6078 assert!(postgres.copy);
6079 assert!(!ansi.comment_on);
6080 assert!(postgres.comment_on);
6081 assert_ne!(ansi, postgres);
6082
6083 // The gates toggle independently of the base preset.
6084 let copy_on = FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
6085 copy: true,
6086 ..UtilitySyntax::ANSI
6087 }));
6088 assert!(copy_on.utility_syntax.copy);
6089 assert_ne!(copy_on.utility_syntax, FeatureSet::ANSI.utility_syntax);
6090
6091 // The SQLite statement gates ride the same struct: SQLITE dispatches
6092 // `PRAGMA`/`ATTACH` and the `VACUUM`/`REINDEX`/`ANALYZE` maintenance trio while
6093 // keeping the PostgreSQL statements off.
6094 let sqlite = FeatureSet::SQLITE.utility_syntax;
6095 let sqlite_maint = FeatureSet::SQLITE.maintenance_syntax;
6096 let ansi_maint = FeatureSet::ANSI.maintenance_syntax;
6097 let postgres_maint = FeatureSet::POSTGRES.maintenance_syntax;
6098 assert_eq!(sqlite, UtilitySyntax::SQLITE);
6099 assert!(sqlite.pragma);
6100 assert!(sqlite.attach);
6101 assert!(sqlite_maint.vacuum);
6102 assert!(sqlite_maint.reindex);
6103 assert!(sqlite_maint.analyze);
6104 assert!(!sqlite.copy);
6105 assert!(!ansi.pragma);
6106 assert!(!ansi.attach);
6107 assert!(!ansi_maint.vacuum);
6108 assert!(!ansi_maint.reindex);
6109 assert!(!ansi_maint.analyze);
6110 assert!(!postgres.pragma);
6111 assert!(!postgres.attach);
6112 // PostgreSQL has its own `VACUUM`/`ANALYZE`/`REINDEX`, but only SQLite's forms
6113 // are modelled, so the gates stay off under the PostgreSQL preset.
6114 assert!(!postgres_maint.vacuum);
6115 assert!(!postgres_maint.reindex);
6116 assert!(!postgres_maint.analyze);
6117
6118 // `create_trigger` is the whole-statement DDL gate: on for SQLite, off for the
6119 // standard/PostgreSQL baselines whose trigger body form is not modelled. Bound
6120 // to locals like the checks above, for the same const-folding reason.
6121 let sqlite_schema_change = FeatureSet::SQLITE.statement_ddl_gates;
6122 let ansi_schema_change = FeatureSet::ANSI.statement_ddl_gates;
6123 let postgres_schema_change = FeatureSet::POSTGRES.statement_ddl_gates;
6124 assert!(sqlite_schema_change.create_trigger);
6125 assert!(!ansi_schema_change.create_trigger);
6126 assert!(!postgres_schema_change.create_trigger);
6127
6128 // `create_macro` is the parallel whole-statement DDL gate for DuckDB's macro DDL:
6129 // on for DuckDB, off for every non-DuckDB baseline (PostgreSQL's `CREATE FUNCTION`
6130 // is the string-body routine, not a live-body macro). Bound to a local like the
6131 // checks above, both for const-folding and to keep clippy off the constant assert.
6132 let duckdb_schema_change = FeatureSet::DUCKDB.statement_ddl_gates;
6133 assert!(duckdb_schema_change.create_macro);
6134 assert!(!ansi_schema_change.create_macro);
6135 assert!(!postgres_schema_change.create_macro);
6136 assert!(!sqlite_schema_change.create_macro);
6137 }
6138
6139 #[test]
6140 fn mysql_preset_composes_existing_knobs_as_data() {
6141 // The third dialect is a re-composition of foundation knobs, not new fields:
6142 // every distinctive MySQL lexical choice is a value the preset selects.
6143 let mysql = FeatureSet::MYSQL;
6144
6145 // Backtick-only identifier quoting; `"` is a string, not an identifier.
6146 assert_eq!(mysql.identifier_quotes, MYSQL_IDENTIFIER_QUOTES);
6147 assert!(mysql.string_literals.double_quoted_strings);
6148 assert!(mysql.string_literals.backslash_escapes);
6149 // Operator-meaning divergences: `||` is OR and `&&` is AND in MySQL.
6150 assert_eq!(mysql.pipe_operator, PipeOperator::LogicalOr);
6151 assert_eq!(mysql.double_ampersand, DoubleAmpersand::LogicalAnd);
6152 // `#` line comments, `0x`/`0b` numbers, `?` placeholders, `$` in identifiers.
6153 assert!(mysql.comment_syntax.line_comment_hash);
6154 assert!(mysql.numeric_literals.hex_integers && mysql.numeric_literals.binary_integers);
6155 assert!(mysql.parameters.anonymous_question);
6156 assert!(mysql.identifier_syntax.dollar_in_identifiers);
6157 // The one new grammar gate: the `LIMIT a, b` comma form.
6158 assert!(mysql.query_tail_syntax.limit_offset_comma);
6159
6160 // It is genuinely distinct from both shipped presets.
6161 assert_ne!(mysql, FeatureSet::ANSI);
6162 assert_ne!(mysql, FeatureSet::POSTGRES);
6163
6164 // Adding the preset did not perturb the existing ones (no new field default
6165 // leaked a behaviour change): ANSI/PostgreSQL still reject the comma form.
6166 // Bind to locals so the const field reads are not flagged by clippy's
6167 // `assertions_on_constants`.
6168 let ansi_qt = FeatureSet::ANSI.query_tail_syntax;
6169 let postgres_qt = FeatureSet::POSTGRES.query_tail_syntax;
6170 assert!(!ansi_qt.limit_offset_comma);
6171 assert!(!postgres_qt.limit_offset_comma);
6172 }
6173
6174 #[test]
6175 fn limit_by_clause_off_for_oracle_compared_presets() {
6176 // ClickHouse `LIMIT n BY …` has no differential oracle, so the no-oracle
6177 // acceptance addition belongs to the ClickHouse preset and Lenient (the `apply_join`
6178 // precedent): on for Lenient, off for every oracle-compared preset. Bind to a
6179 // local so the const read is not flagged by clippy's `assertions_on_constants`.
6180 let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
6181 assert!(lenient_qt.limit_by_clause);
6182 for preset in [
6183 FeatureSet::ANSI,
6184 FeatureSet::POSTGRES,
6185 FeatureSet::MYSQL,
6186 FeatureSet::SQLITE,
6187 FeatureSet::DUCKDB,
6188 ] {
6189 assert!(!preset.query_tail_syntax.limit_by_clause);
6190 }
6191 }
6192
6193 #[test]
6194 fn format_clause_off_for_oracle_compared_presets() {
6195 // ClickHouse `FORMAT <name>` has no differential oracle, so the no-oracle
6196 // acceptance addition belongs to the ClickHouse preset and Lenient (the `settings_clause`
6197 // precedent): on for Lenient, off for every oracle-compared preset. Bind to a
6198 // local so the const read is not flagged by clippy's `assertions_on_constants`.
6199 let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
6200 assert!(lenient_qt.format_clause);
6201 for preset in [
6202 FeatureSet::ANSI,
6203 FeatureSet::POSTGRES,
6204 FeatureSet::MYSQL,
6205 FeatureSet::SQLITE,
6206 FeatureSet::DUCKDB,
6207 ] {
6208 assert!(!preset.query_tail_syntax.format_clause);
6209 }
6210 }
6211
6212 #[test]
6213 fn for_xml_json_clause_on_for_mssql_and_lenient_off_elsewhere() {
6214 // MSSQL `FOR XML`/`FOR JSON` has no differential oracle, so the no-oracle
6215 // acceptance addition belongs to the MSSQL preset and Lenient (the permissive
6216 // union): on for both, off for every oracle-compared preset. Bind to a local so
6217 // the const read is not flagged by clippy's `assertions_on_constants`.
6218 let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
6219 assert!(lenient_qt.for_xml_json_clause);
6220 #[cfg(feature = "mssql")]
6221 {
6222 let mssql_qt = FeatureSet::MSSQL.query_tail_syntax;
6223 assert!(mssql_qt.for_xml_json_clause);
6224 // MSSQL spells result-shaping with `FOR`, but has no query-tail locking
6225 // clause — the two `FOR`-led clauses never coexist under this preset.
6226 assert!(!mssql_qt.locking_clauses);
6227 }
6228 for preset in [
6229 FeatureSet::ANSI,
6230 FeatureSet::POSTGRES,
6231 FeatureSet::MYSQL,
6232 FeatureSet::SQLITE,
6233 FeatureSet::DUCKDB,
6234 ] {
6235 assert!(!preset.query_tail_syntax.for_xml_json_clause);
6236 }
6237 }
6238
6239 #[test]
6240 fn lateral_view_clause_on_for_hive_databricks_and_lenient_off_elsewhere() {
6241 // Hive/Spark `LATERAL VIEW` has no differential oracle, so the no-oracle
6242 // acceptance addition belongs to the Hive and Databricks presets and Lenient
6243 // (the permissive union): on for all three, off for every oracle-compared
6244 // preset. Bind to a local so the const read is not flagged by clippy's
6245 // `assertions_on_constants`.
6246 let lenient_select = FeatureSet::LENIENT.select_syntax;
6247 assert!(lenient_select.lateral_view_clause);
6248 // Lenient also enables the LATERAL derived-table factor — the one preset with
6249 // both `LATERAL`-led gates on, which the position/follow-token partition keeps
6250 // conflict-free (see the flag doc).
6251 let lenient_factors = FeatureSet::LENIENT.table_factor_syntax;
6252 assert!(lenient_factors.lateral);
6253 #[cfg(feature = "hive")]
6254 {
6255 let hive = FeatureSet::HIVE;
6256 assert!(hive.select_syntax.lateral_view_clause);
6257 // Hive has no LATERAL derived-table factor gate on, so under its preset
6258 // `LATERAL` leads only the view clause.
6259 assert!(!hive.table_factor_syntax.lateral);
6260 }
6261 #[cfg(feature = "databricks")]
6262 {
6263 let dbx = FeatureSet::DATABRICKS;
6264 assert!(dbx.select_syntax.lateral_view_clause);
6265 assert!(!dbx.table_factor_syntax.lateral);
6266 }
6267 for preset in [
6268 FeatureSet::ANSI,
6269 FeatureSet::POSTGRES,
6270 FeatureSet::MYSQL,
6271 FeatureSet::SQLITE,
6272 FeatureSet::DUCKDB,
6273 ] {
6274 assert!(!preset.select_syntax.lateral_view_clause);
6275 }
6276 }
6277
6278 #[test]
6279 fn connect_by_clause_on_for_snowflake_and_lenient_off_elsewhere() {
6280 // The Oracle-style `START WITH`/`CONNECT BY` hierarchical query clause has no
6281 // differential oracle, so the no-oracle acceptance addition belongs to the
6282 // Snowflake preset (whose public docs are the citable grammar) and Lenient (the
6283 // permissive union): on for both, off for every oracle-compared preset. Bind to a
6284 // local so the const read is not flagged by clippy's `assertions_on_constants`.
6285 let lenient_select = FeatureSet::LENIENT.select_syntax;
6286 assert!(lenient_select.connect_by_clause);
6287 #[cfg(feature = "snowflake")]
6288 {
6289 let snowflake = FeatureSet::SNOWFLAKE;
6290 assert!(snowflake.select_syntax.connect_by_clause);
6291 }
6292 // Databricks/Spark documents recursive CTEs instead of `CONNECT BY` and does not
6293 // support the Oracle `CONNECT BY … START WITH` syntax, so it stays off — unlike
6294 // the LATERAL VIEW clause, which Databricks *does* inherit from Spark.
6295 #[cfg(feature = "databricks")]
6296 {
6297 let dbx = FeatureSet::DATABRICKS;
6298 assert!(!dbx.select_syntax.connect_by_clause);
6299 }
6300 for preset in [
6301 FeatureSet::ANSI,
6302 FeatureSet::POSTGRES,
6303 FeatureSet::MYSQL,
6304 FeatureSet::SQLITE,
6305 FeatureSet::DUCKDB,
6306 ] {
6307 assert!(!preset.select_syntax.connect_by_clause);
6308 }
6309 }
6310
6311 #[test]
6312 fn nullable_type_off_for_oracle_compared_presets() {
6313 // ClickHouse `Nullable(T)` has no differential oracle, so the no-oracle
6314 // acceptance addition belongs to the ClickHouse preset and Lenient (the `composite_types`
6315 // precedent): on for Lenient, off for every oracle-compared preset. Bind to a local
6316 // so the const read is not flagged by clippy's `assertions_on_constants`.
6317 let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6318 assert!(lenient_types.nullable_type);
6319 for preset in [
6320 FeatureSet::ANSI,
6321 FeatureSet::POSTGRES,
6322 FeatureSet::MYSQL,
6323 FeatureSet::SQLITE,
6324 FeatureSet::DUCKDB,
6325 ] {
6326 assert!(!preset.type_name_syntax.nullable_type);
6327 }
6328 }
6329
6330 #[test]
6331 fn low_cardinality_type_off_for_oracle_compared_presets() {
6332 // ClickHouse `LowCardinality(T)` has no differential oracle, so the no-oracle
6333 // acceptance addition belongs to the ClickHouse preset and Lenient (the `nullable_type`
6334 // precedent): on for Lenient, off for every oracle-compared preset. Bind to a local
6335 // so the const read is not flagged by clippy's `assertions_on_constants`.
6336 let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6337 assert!(lenient_types.low_cardinality_type);
6338 for preset in [
6339 FeatureSet::ANSI,
6340 FeatureSet::POSTGRES,
6341 FeatureSet::MYSQL,
6342 FeatureSet::SQLITE,
6343 FeatureSet::DUCKDB,
6344 ] {
6345 assert!(!preset.type_name_syntax.low_cardinality_type);
6346 }
6347 }
6348
6349 #[test]
6350 fn fixed_string_type_off_for_oracle_compared_presets() {
6351 // ClickHouse `FixedString(N)` has no differential oracle, so the no-oracle
6352 // acceptance addition belongs to the ClickHouse preset and Lenient (the `nullable_type` /
6353 // `low_cardinality_type` precedent): on for Lenient, off for every oracle-compared
6354 // preset. Bind to a local so the const read is not flagged by clippy's
6355 // `assertions_on_constants`.
6356 let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6357 assert!(lenient_types.fixed_string_type);
6358 for preset in [
6359 FeatureSet::ANSI,
6360 FeatureSet::POSTGRES,
6361 FeatureSet::MYSQL,
6362 FeatureSet::SQLITE,
6363 FeatureSet::DUCKDB,
6364 ] {
6365 assert!(!preset.type_name_syntax.fixed_string_type);
6366 }
6367 }
6368
6369 #[test]
6370 fn datetime64_type_off_for_oracle_compared_presets() {
6371 // ClickHouse `DateTime64(P[, 'tz'])` has no differential oracle, so the
6372 // no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
6373 // `fixed_string_type` precedent): on for Lenient, off for every oracle-compared
6374 // preset. Bind to a local so the const read is not flagged by clippy's
6375 // `assertions_on_constants`.
6376 let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6377 assert!(lenient_types.datetime64_type);
6378 for preset in [
6379 FeatureSet::ANSI,
6380 FeatureSet::POSTGRES,
6381 FeatureSet::MYSQL,
6382 FeatureSet::SQLITE,
6383 FeatureSet::DUCKDB,
6384 ] {
6385 assert!(!preset.type_name_syntax.datetime64_type);
6386 }
6387 }
6388
6389 #[test]
6390 fn nested_type_off_for_oracle_compared_presets() {
6391 // ClickHouse `Nested(name Type, ...)` has no differential oracle, so the
6392 // no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
6393 // `datetime64_type` precedent): on for Lenient, off for every oracle-compared
6394 // preset. Bind to a local so the const read is not flagged by clippy's
6395 // `assertions_on_constants`.
6396 let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6397 assert!(lenient_types.nested_type);
6398 for preset in [
6399 FeatureSet::ANSI,
6400 FeatureSet::POSTGRES,
6401 FeatureSet::MYSQL,
6402 FeatureSet::SQLITE,
6403 FeatureSet::DUCKDB,
6404 ] {
6405 assert!(!preset.type_name_syntax.nested_type);
6406 }
6407 }
6408
6409 #[test]
6410 fn bit_width_integer_names_off_for_oracle_compared_presets() {
6411 // ClickHouse's `Int8`…`Int256`/`UInt*` fixed-bit-width integer names have no
6412 // differential oracle, so the no-oracle acceptance addition belongs to the ClickHouse
6413 // preset and Lenient (the `datetime64_type` precedent): on for Lenient, off for every
6414 // oracle-compared preset. Bind to a local so the const read is not flagged by clippy's
6415 // `assertions_on_constants`.
6416 let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6417 assert!(lenient_types.bit_width_integer_names);
6418 for preset in [
6419 FeatureSet::ANSI,
6420 FeatureSet::POSTGRES,
6421 FeatureSet::MYSQL,
6422 FeatureSet::SQLITE,
6423 FeatureSet::DUCKDB,
6424 ] {
6425 assert!(!preset.type_name_syntax.bit_width_integer_names);
6426 }
6427 }
6428
6429 #[test]
6430 fn settings_clause_off_for_oracle_compared_presets() {
6431 // ClickHouse `SETTINGS name = value, …` has no differential oracle, so the
6432 // no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
6433 // `limit_by_clause` precedent): on for Lenient, off for every oracle-compared
6434 // preset. Bind to a local so the const read is not flagged by clippy's
6435 // `assertions_on_constants`.
6436 let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
6437 assert!(lenient_qt.settings_clause);
6438 for preset in [
6439 FeatureSet::ANSI,
6440 FeatureSet::POSTGRES,
6441 FeatureSet::MYSQL,
6442 FeatureSet::SQLITE,
6443 FeatureSet::DUCKDB,
6444 ] {
6445 assert!(!preset.query_tail_syntax.settings_clause);
6446 }
6447 }
6448
6449 #[test]
6450 fn feature_set_as_of_is_a_deterministic_total_anchor() {
6451 // Invariant across feature-ID-era editions at M1 (every implemented standard
6452 // feature is SQL:1999 Core), but a stable, total pin point.
6453 for version in [
6454 StandardVersion::Sql1999,
6455 StandardVersion::Sql2003,
6456 StandardVersion::Sql2008,
6457 StandardVersion::Sql2011,
6458 StandardVersion::Sql2016,
6459 ] {
6460 assert_eq!(FeatureSet::as_of(version), FeatureSet::ANSI);
6461 }
6462 }
6463
6464 #[test]
6465 fn byte_classes_cover_m1_lexer_needs() {
6466 assert!(has_class(b' ', CLASS_WHITESPACE));
6467 assert!(has_class(b'\n', CLASS_WHITESPACE));
6468
6469 assert!(has_class(b'a', CLASS_IDENTIFIER_START));
6470 assert!(has_class(b'Z', CLASS_IDENTIFIER_START));
6471 assert!(has_class(b'_', CLASS_IDENTIFIER_START));
6472 assert!(has_class(b'a', CLASS_IDENTIFIER_CONTINUE));
6473
6474 assert!(has_class(b'7', CLASS_DIGIT));
6475 assert!(has_class(b'7', CLASS_IDENTIFIER_CONTINUE));
6476 assert!(!has_class(b'7', CLASS_IDENTIFIER_START));
6477
6478 assert!(has_class(b'+', CLASS_OPERATOR));
6479 assert!(has_class(b'|', CLASS_OPERATOR));
6480 assert!(has_class(b'(', CLASS_PUNCTUATION));
6481 assert!(has_class(b'.', CLASS_PUNCTUATION));
6482
6483 assert!(has_class(0x80, CLASS_IDENTIFIER_CONTINUE));
6484 assert_eq!(byte_class(0), 0);
6485 }
6486
6487 #[test]
6488 fn vertical_tab_is_dialect_specific_whitespace() {
6489 // The vertical tab (`0x0b`) is the one member of the flex `space` set `[ \t\n\r\f\v]`
6490 // that Rust's `is_ascii_whitespace` — and hence `STANDARD_BYTE_CLASSES` — omits, and
6491 // every engine treats it differently. Engine-measured:
6492 //
6493 // - PostgreSQL / MySQL fold it as *ordinary* whitespace everywhere (a lone `0x0b`
6494 // parses as an empty statement, `SELECT\x0b1` as `SELECT 1`) — full `CLASS_WHITESPACE`.
6495 assert!(FeatureSet::POSTGRES.has_byte_class(0x0b, CLASS_WHITESPACE));
6496 assert!(FeatureSet::MYSQL.has_byte_class(0x0b, CLASS_WHITESPACE));
6497 // - SQLite folds it only as a whitespace-run *continuation*: it rides an open run
6498 // (`"\x20\x0b"` accepts) but cannot start one (lone `"\x0b"` rejects) — so it carries
6499 // `CLASS_WHITESPACE_CONTINUE`, never `CLASS_WHITESPACE`.
6500 assert!(FeatureSet::SQLITE.has_byte_class(0x0b, CLASS_WHITESPACE_CONTINUE));
6501 assert!(!FeatureSet::SQLITE.has_byte_class(0x0b, CLASS_WHITESPACE));
6502 // - DuckDB folds it as statement-boundary *trim*: whitespace at a `;`-segment's edges
6503 // (lone `"\x0b"`, `"SELECT 1\x0b"` accept) but a hard error interior to a statement
6504 // (`"SELECT\x0b1"` rejects) — so it carries both `CLASS_WHITESPACE` and the
6505 // `CLASS_WHITESPACE_BOUNDARY` marker the tokenizer's interior guard reads.
6506 assert!(FeatureSet::DUCKDB.has_byte_class(0x0b, CLASS_WHITESPACE));
6507 assert!(FeatureSet::DUCKDB.has_byte_class(0x0b, CLASS_WHITESPACE_BOUNDARY));
6508 assert!(FeatureSet::DUCKDB.byte_classes.has_boundary_whitespace());
6509 // - ANSI (and every other preset) keeps it strict: not whitespace in any form.
6510 for preset in [FeatureSet::ANSI, FeatureSet::POSTGRES, FeatureSet::MYSQL] {
6511 assert!(!preset.has_byte_class(0x0b, CLASS_WHITESPACE_BOUNDARY));
6512 }
6513 assert!(!FeatureSet::ANSI.has_byte_class(
6514 0x0b,
6515 CLASS_WHITESPACE | CLASS_WHITESPACE_CONTINUE | CLASS_WHITESPACE_BOUNDARY,
6516 ));
6517 // Only DuckDB flags the boundary class, so only its tokenizer pays the guard.
6518 for preset in [
6519 FeatureSet::ANSI,
6520 FeatureSet::POSTGRES,
6521 FeatureSet::MYSQL,
6522 FeatureSet::SQLITE,
6523 ] {
6524 assert!(!preset.byte_classes.has_boundary_whitespace());
6525 }
6526 // The form feed (`0x0c`) is shared whitespace: every probed engine folds it, and it
6527 // already rides `STANDARD_BYTE_CLASSES` via `is_ascii_whitespace`.
6528 for preset in [
6529 FeatureSet::ANSI,
6530 FeatureSet::POSTGRES,
6531 FeatureSet::MYSQL,
6532 FeatureSet::SQLITE,
6533 FeatureSet::DUCKDB,
6534 ] {
6535 assert!(preset.has_byte_class(0x0c, CLASS_WHITESPACE));
6536 }
6537 }
6538
6539 #[test]
6540 fn dialect_data_owns_byte_classes_and_binding_powers() {
6541 let byte_classes = FeatureSet::ANSI
6542 .byte_classes
6543 .with_class(b'@', CLASS_IDENTIFIER_START | CLASS_IDENTIFIER_CONTINUE);
6544 let binding_powers = FeatureSet::ANSI.binding_powers.with_binary(
6545 &BinaryOperator::StringConcat,
6546 BindingPower {
6547 left: 70,
6548 right: 71,
6549 assoc: crate::precedence::Assoc::Left,
6550 },
6551 );
6552 let custom = FeatureSet::ANSI.with(
6553 FeatureDelta::EMPTY
6554 .byte_classes(byte_classes)
6555 .binding_powers(binding_powers),
6556 );
6557
6558 assert!(custom.has_byte_class(b'@', CLASS_IDENTIFIER_START));
6559 assert_eq!(custom.binding_power(&BinaryOperator::StringConcat).left, 70);
6560 assert_eq!(
6561 custom.binding_power(&BinaryOperator::Plus),
6562 FeatureSet::ANSI.binding_power(&BinaryOperator::Plus),
6563 );
6564 }
6565
6566 #[test]
6567 fn dialect_data_owns_set_operation_binding_powers() {
6568 let custom_set_powers = FeatureSet::ANSI.set_operation_powers.with_set_operator(
6569 &SetOperator::Union,
6570 BindingPower {
6571 left: 30,
6572 right: 31,
6573 assoc: crate::precedence::Assoc::Left,
6574 },
6575 );
6576 let custom =
6577 FeatureSet::ANSI.with(FeatureDelta::EMPTY.set_operation_powers(custom_set_powers));
6578
6579 assert_eq!(
6580 custom.set_operation_binding_power(&SetOperator::Union).left,
6581 30,
6582 );
6583 assert_eq!(
6584 custom
6585 .set_operation_binding_power(&SetOperator::Except)
6586 .left,
6587 30,
6588 );
6589 assert_eq!(
6590 custom.set_operation_binding_power(&SetOperator::Intersect),
6591 FeatureSet::ANSI.set_operation_binding_power(&SetOperator::Intersect),
6592 );
6593 }
6594}