Skip to main content

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}