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//!
35//! # Struct header docs: category + doctrine, never member lists
36//!
37//! A sub-struct's header states the *category* of surface it gates and the *doctrine* its
38//! flags follow (widening/narrowing per the rule above, engine-probed on/off, leading-keyword
39//! dispatch, …), plus pointers to the relevant registries — never an enumeration of the
40//! specific flags/statements/clauses it contains. An enumeration is stale-by-growth: every
41//! field added past the ones the header happened to name turns the list into a misleading
42//! subset. A statement of the struct's MECE boundary against its sibling axes is *not* an
43//! enumeration, and is encouraged.
44
45pub mod keyword;
46pub mod lex_class;
47
48// Each shipped dialect owns its preset, reserved sets, and byte-class choices in
49// its own module; the shared `FeatureSet`/`KeywordSet`/`ByteClasses` machinery in
50// this module stays dialect-agnostic. `ansi` is the always-compiled baseline every
51// dialect derives from; `postgres`/`mysql`/`sqlite`/`duckdb`/`lenient` are each gated
52// behind their cargo feature so an excluded dialect's preset data is genuinely not
53// compiled.
54mod ansi;
55#[cfg(feature = "bigquery")]
56mod bigquery;
57#[cfg(feature = "clickhouse")]
58mod clickhouse;
59#[cfg(feature = "databricks")]
60mod databricks;
61#[cfg(feature = "duckdb")]
62mod duckdb;
63#[cfg(feature = "hive")]
64mod hive;
65#[cfg(feature = "lenient")]
66mod lenient;
67#[cfg(feature = "mssql")]
68mod mssql;
69#[cfg(feature = "mysql")]
70mod mysql;
71#[cfg(feature = "postgres")]
72mod postgres;
73#[cfg(feature = "redshift")]
74mod redshift;
75#[cfg(feature = "snowflake")]
76mod snowflake;
77#[cfg(feature = "sqlite")]
78mod sqlite;
79
80pub use ansi::{
81    ANSI, RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME,
82    STANDARD_IDENTIFIER_QUOTES,
83};
84#[cfg(feature = "bigquery")]
85pub use bigquery::{BIGQUERY, BIGQUERY_IDENTIFIER_QUOTES};
86#[cfg(feature = "clickhouse")]
87pub use clickhouse::{CLICKHOUSE, CLICKHOUSE_IDENTIFIER_QUOTES};
88#[cfg(feature = "databricks")]
89pub use databricks::{
90    DATABRICKS, DATABRICKS_IDENTIFIER_QUOTES, DATABRICKS_QUALIFY_RESERVATION,
91    DATABRICKS_RESERVED_BARE_ALIAS, DATABRICKS_RESERVED_COLUMN_NAME,
92    DATABRICKS_RESERVED_FUNCTION_NAME, DATABRICKS_RESERVED_TYPE_NAME,
93};
94#[cfg(feature = "duckdb")]
95pub use duckdb::{DUCKDB, DUCKDB_BINDING_POWERS};
96#[cfg(feature = "hive")]
97pub use hive::{HIVE, HIVE_IDENTIFIER_QUOTES};
98pub use keyword::{Keyword, KeywordSet, lookup_keyword};
99#[cfg(feature = "lenient")]
100pub use lenient::{LENIENT, LENIENT_IDENTIFIER_QUOTES};
101pub use lex_class::{
102    ByteClassTable, ByteClasses, DUCKDB_BYTE_CLASSES, MYSQL_BYTE_CLASSES, POSTGRES_BYTE_CLASSES,
103    SQLITE_BYTE_CLASSES, STANDARD_BYTE_CLASSES,
104};
105#[cfg(feature = "mssql")]
106pub use mssql::{MSSQL, MSSQL_IDENTIFIER_QUOTES};
107#[cfg(feature = "mysql")]
108pub use mysql::{
109    MYSQL, MYSQL_IDENTIFIER_QUOTES, MYSQL_RESERVED_BARE_ALIAS, MYSQL_RESERVED_COLUMN_NAME,
110    MYSQL_RESERVED_FUNCTION_NAME, MYSQL_RESERVED_TYPE_NAME, MYSQL_WINDOW_FUNCTION_KEYWORDS,
111};
112#[cfg(feature = "postgres")]
113pub use postgres::POSTGRES;
114#[cfg(feature = "redshift")]
115pub use redshift::REDSHIFT;
116#[cfg(feature = "snowflake")]
117pub use snowflake::{
118    SNOWFLAKE, SNOWFLAKE_QUALIFY_RESERVATION, SNOWFLAKE_RESERVED_BARE_ALIAS,
119    SNOWFLAKE_RESERVED_COLUMN_NAME, SNOWFLAKE_RESERVED_FUNCTION_NAME, SNOWFLAKE_RESERVED_TYPE_NAME,
120    SNOWFLAKE_TABLE_OPERATOR_RESERVATION,
121};
122#[cfg(feature = "sqlite")]
123pub use sqlite::{
124    SQLITE, SQLITE_IDENTIFIER_QUOTES, SQLITE_RESERVED_BARE_ALIAS, SQLITE_RESERVED_COLUMN_NAME,
125    SQLITE_RESERVED_FUNCTION_NAME, SQLITE_RESERVED_TYPE_NAME,
126};
127
128use std::borrow::Cow;
129
130use crate::ast::{
131    BinaryOperator, BitwiseXorSpelling, IntegerDivideSpelling, ModuloSpelling, RegexpSpelling,
132    SetOperator, UnaryOperator,
133};
134use crate::precedence::{BindingPower, BindingPowerTable, SetOperationBindingPowerTable};
135
136/// How unquoted identifiers fold for identity in consumers such as planners.
137///
138/// The parser still interns the exact source text; this value describes later
139/// identity behaviour without losing render fidelity.
140///
141/// One tri-state models a single identity fold shared by *every* unquoted
142/// identifier — table, column, and alias alike. That single-fold model is a
143/// deliberate, parity-matching choice, not a gap: `datafusion-sqlparser-rs`, the
144/// drop-in target, does not key case sensitivity by identifier kind either, so one
145/// knob already meets parity and per-kind folding would be strictly more expressive
146/// than parity requires. It also keeps the dialect model minimal.
147/// [`Self::Lower`] approximates the MySQL/T-SQL column rule — "case-preserving
148/// storage, case-insensitive comparison" — closely: fold lower for identity while
149/// the interned text still renders exactly as written (folding is layered onto
150/// the exact form, never baked into the parse).
151///
152/// # Known limitation: per-identifier-kind sensitivity
153///
154/// MySQL and T-SQL are case-insensitive for columns/aliases, but table and database
155/// name sensitivity is a *deployment* setting (MySQL `lower_case_table_names`, the
156/// T-SQL server/database collation). A case-insensitive column beside a
157/// case-sensitive table cannot be expressed by one fold, and that deployment-config
158/// knob is out of model; [`Self::Lower`] is the closest single fit and is what both
159/// dialects use. Per-identifier-kind casing is a deliberate future extension, gated
160/// on committing to a case-sensitive-table dialect (T-SQL): only then does the
161/// table-vs-column split change parse/identity results, so until such a dialect
162/// ships the added expressiveness would be unused surface (an M3 decision).
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub enum Casing {
165    /// Fold unquoted identifiers to upper case (Oracle/DB2 style).
166    Upper,
167    /// Fold unquoted identifiers to lower case (PostgreSQL style).
168    Lower,
169    /// Preserve unquoted identifier case as written (MySQL/SQL Server style).
170    Preserve,
171}
172
173impl Casing {
174    /// Fold an unquoted identifier for dialect identity comparison.
175    ///
176    /// M1 performs ASCII folding only. Non-ASCII bytes are preserved, matching
177    /// the tokenizer's permissive Unicode stance until a later precision pass
178    /// introduces full Unicode identifier semantics.
179    pub fn fold_identifier<'a>(&self, identifier: &'a str) -> Cow<'a, str> {
180        match self {
181            Self::Upper => fold_identifier_upper(identifier),
182            Self::Lower => fold_identifier_lower(identifier),
183            Self::Preserve => Cow::Borrowed(identifier),
184        }
185    }
186}
187
188/// Default sort position for null values when a query omits explicit ordering.
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub enum NullOrdering {
191    /// Sort null values before non-null values by default.
192    NullsFirst,
193    /// Sort null values after non-null values by default.
194    NullsLast,
195}
196
197impl NullOrdering {
198    /// Whether this default places nulls before non-null values.
199    pub const fn nulls_first(&self) -> bool {
200        match self {
201            Self::NullsFirst => true,
202            Self::NullsLast => false,
203        }
204    }
205}
206
207/// Which dialect's canonical surface spelling a target-dialect render emits.
208///
209/// When [`RenderSpelling::TargetDialect`] rendering normalizes a construct whose
210/// spelling diverges across dialects — today the divergent type names (`NUMERIC` vs
211/// `DECIMAL`, `TIMESTAMPTZ` vs `TIMESTAMP WITH TIME ZONE`, `BYTEA` vs `BLOB`, …) — it
212/// emits this family's spelling. It is the dialect *data* the renderer reads instead
213/// of recognizing a preset by identity: each preset declares its family here, so
214/// PostgreSQL-vs-ANSI output spelling is a `FeatureSet` field, not a compile-time
215/// `FeatureSet::POSTGRES` comparison gated on the `postgres` cargo feature.
216/// `PreserveSource` rendering ignores it and keeps the AST's own syntax tag.
217///
218/// [`RenderSpelling::TargetDialect`]: crate::render::RenderSpelling::TargetDialect
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum TargetSpelling {
221    /// The ANSI/SQL-standard canonical spelling — the portable baseline every dialect
222    /// without its own Tier-1 spelling table renders to.
223    Ansi,
224    /// PostgreSQL's canonical spelling (`NUMERIC`, `TIMESTAMPTZ`, `BYTEA`, `VARCHAR`,
225    /// the explicit zone-suffix forms).
226    Postgres,
227}
228
229/// What the `||` operator token *means* in a dialect.
230///
231/// ANSI and PostgreSQL concatenate; MySQL/MariaDB without `PIPES_AS_CONCAT` treat
232/// `||` as a synonym for logical `OR` (sqlglot's `DPIPE_IS_STRING_CONCAT`). This
233/// is dialect *meaning* data, not tree shape: both spellings map to a
234/// single canonical [`BinaryOperator`], and the precedence follows automatically
235/// from that operator — `OR` binds looser than concatenation, so no separate
236/// binding-power override is needed.
237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238pub enum PipeOperator {
239    /// `||` concatenates strings ([`BinaryOperator::StringConcat`]).
240    StringConcat,
241    /// `||` is logical OR ([`BinaryOperator::Or`]).
242    LogicalOr,
243}
244
245impl PipeOperator {
246    /// The canonical binary operator `||` parses to under this dialect.
247    pub const fn binary_operator(self) -> BinaryOperator {
248        match self {
249            Self::StringConcat => BinaryOperator::StringConcat,
250            Self::LogicalOr => BinaryOperator::Or,
251        }
252    }
253}
254
255/// What the `&&` operator token *means* in a dialect — the operator-meaning analog
256/// of [`PipeOperator`].
257///
258/// MySQL/MariaDB treat `&&` as a synonym for logical `AND`; DuckDB (and PostgreSQL)
259/// spell array/range/geometry overlap with `&&`; ANSI has no `&&` scalar operator. This
260/// is dialect *meaning* data: the `&&` lexeme always tokenizes, and this decides whether
261/// it is an infix operator and which canonical [`BinaryOperator`] it maps to. The chosen
262/// operator's binding power follows automatically, so there is no separate precedence
263/// override.
264#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265pub enum DoubleAmpersand {
266    /// `&&` is not a scalar infix operator (ANSI). It still lexes, but the parser rejects
267    /// it in expression position rather than mis-binding.
268    Unsupported,
269    /// `&&` is logical AND ([`BinaryOperator::And`], MySQL/MariaDB).
270    LogicalAnd,
271    /// `&&` is the overlap operator ([`BinaryOperator::Overlap`]) — array/range overlap
272    /// (PostgreSQL) and bounding-box overlap over geometries (DuckDB). Binds at the "any
273    /// other operator" precedence.
274    Overlaps,
275}
276
277impl DoubleAmpersand {
278    /// The canonical binary operator `&&` parses to, or `None` when the dialect
279    /// does not treat `&&` as a scalar infix operator.
280    pub const fn binary_operator(self) -> Option<BinaryOperator> {
281        match self {
282            Self::Unsupported => None,
283            Self::LogicalAnd => Some(BinaryOperator::And),
284            Self::Overlaps => Some(BinaryOperator::Overlap),
285        }
286    }
287}
288
289/// Whether a dialect treats MySQL's reserved-keyword infix operators — `DIV`, `MOD`,
290/// `XOR`, `RLIKE`, `REGEXP` — as operators.
291///
292/// MySQL spells these infix operators as keywords rather than symbols; ANSI and
293/// PostgreSQL have none of them (the words are ordinary identifiers there). Which
294/// keywords act as operators is therefore an explicit dialect-data decision,
295/// the keyword analogue of [`PipeOperator`]/[`DoubleAmpersand`]: this
296/// maps a reserved keyword token to the canonical [`BinaryOperator`] it folds onto.
297/// Precedence is *not* stored here — each operator's binding power is owned by the
298/// [`BindingPowerTable`] (`DIV`/`MOD` multiplicative, `XOR` between `AND` and `OR`,
299/// `RLIKE`/`REGEXP` comparison), so meaning and precedence cannot drift.
300///
301/// The `MySql` variant is named for a dialect, not the capability it grants — the
302/// lone place a `FeatureSet` value is dialect-named — and that is deliberate. A
303/// variant here denotes one dialect's *exact* keyword-operator set (MySQL's
304/// `DIV`/`MOD`/`XOR`/`RLIKE`/`REGEXP` mapping), which is a specification, not a
305/// composable capability: a future dialect with a different set gets its own
306/// variant rather than reusing this one, so the dialect name *is* the spec. A
307/// capability name like `DivModXorRegexp` would only restate that mapping, less
308/// precisely and with no room for the next dialect's variant.
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum KeywordOperators {
311    /// No keyword infix operators (ANSI/PostgreSQL): `DIV`/`MOD`/`XOR`/`RLIKE`/`REGEXP`
312    /// are ordinary identifiers, so in infix position they end the expression.
313    Unsupported,
314    /// The MySQL keyword infix operators: `DIV`, `MOD`, `XOR`, `RLIKE`, `REGEXP`.
315    MySql,
316    /// The SQLite keyword infix operators: `GLOB`, `MATCH`, `REGEXP`. Like
317    /// [`MySql`](Self::MySql), the variant is named for the dialect whose *exact*
318    /// keyword-operator set it denotes (a specification, not a composable capability),
319    /// per the dialect-named-variant rationale documented on this enum. `GLOB` is a
320    /// built-in the differential oracle can verify; `MATCH`/`REGEXP` are grammar hooks
321    /// backed by application-defined functions the bundled engine does not register,
322    /// so they parse but a bare `prepare` rejects them — grammar-only siblings.
323    Sqlite,
324    /// DuckDB's keyword infix set is just `GLOB` (engine-probed on 1.5.4: `MATCH` /
325    /// `REGEXP` are not keyword infix operators there). Named for the dialect's exact
326    /// set, same rationale as [`Sqlite`](Self::Sqlite).
327    DuckDb,
328}
329
330impl KeywordOperators {
331    /// The canonical binary operator `keyword` maps to as an infix operator under
332    /// this dialect, or `None` when `keyword` is not a keyword operator here.
333    ///
334    /// The `MOD` and `RLIKE`/`REGEXP` keywords fold onto the shared modulo/regex
335    /// operators with a surface tag so the exact spelling round-trips;
336    /// `DIV` and `XOR` are their own operator keys.
337    pub const fn binary_operator(self, keyword: Keyword) -> Option<BinaryOperator> {
338        match self {
339            Self::Unsupported => None,
340            Self::MySql => match keyword {
341                Keyword::Div => Some(BinaryOperator::IntegerDivide(IntegerDivideSpelling::Div)),
342                Keyword::Mod => Some(BinaryOperator::Modulo(ModuloSpelling::Mod)),
343                Keyword::Xor => Some(BinaryOperator::Xor),
344                Keyword::Rlike => Some(BinaryOperator::Regexp(RegexpSpelling::Rlike)),
345                Keyword::Regexp => Some(BinaryOperator::Regexp(RegexpSpelling::Regexp)),
346                _ => None,
347            },
348            // SQLite's `GLOB`/`MATCH` get their own operator keys; `REGEXP` folds onto
349            // the shared regex operator with the `Regexp` spelling tag (same round-trip
350            // pattern MySQL uses for `RLIKE`/`REGEXP`).
351            Self::Sqlite => match keyword {
352                Keyword::Glob => Some(BinaryOperator::Glob),
353                Keyword::Match => Some(BinaryOperator::Match),
354                Keyword::Regexp => Some(BinaryOperator::Regexp(RegexpSpelling::Regexp)),
355                _ => None,
356            },
357            Self::DuckDb => match keyword {
358                Keyword::Glob => Some(BinaryOperator::Glob),
359                _ => None,
360            },
361        }
362    }
363}
364
365/// What the always-lexed `^` operator token *means* in a dialect — the operator-meaning
366/// analog of [`PipeOperator`]/[`DoubleAmpersand`].
367///
368/// The `^` byte is a single-character self token that always tokenizes; this decides what
369/// an infix `^` binds to. PostgreSQL/DuckDB read it as arithmetic exponentiation, MySQL as
370/// bitwise XOR, and ANSI/SQLite give it no infix meaning at all. The three readings are
371/// mutually exclusive per dialect — one byte, one meaning — so a single meaning-enum makes
372/// the invalid "both power and XOR" state unrepresentable (conflict-exempt: the both-state is
373/// unrepresentable by construction, so no [`GrammarConflict`] registry variant governs the
374/// exclusion). It folds together what used to
375/// be an `exponent_operator` bool and a `Caret`-XOR spelling that prose alone kept apart.
376/// The precedence follows from the mapped [`BinaryOperator`] via the dialect's
377/// [`BindingPowerTable`] (exponent at its own tier tighter than `*`; `Caret` XOR at the
378/// bitwise-XOR rank), so there is no separate override here.
379///
380/// Bitwise XOR's *other* spelling `#` (PostgreSQL) rides a different byte on its own axis,
381/// [`FeatureSet::hash_bitwise_xor`]; `^` and `#` never share this enum.
382#[derive(Clone, Copy, Debug, PartialEq, Eq)]
383pub enum CaretOperator {
384    /// `^` is not an infix operator (ANSI/SQLite): it still lexes, but the parser ends the
385    /// expression rather than binding it.
386    Unsupported,
387    /// `^` is arithmetic exponentiation ([`BinaryOperator::Exponent`], PostgreSQL/DuckDB) —
388    /// its own precedence tier, tighter than `*`.
389    Exponent,
390    /// `^` is bitwise XOR ([`BinaryOperator::BitwiseXor`] under the
391    /// [`Caret`](BitwiseXorSpelling::Caret) spelling, MySQL).
392    BitwiseXor,
393}
394
395impl CaretOperator {
396    /// The canonical binary operator `^` maps to under this dialect, or `None` when the
397    /// dialect gives `^` no infix meaning.
398    pub const fn binary_operator(self) -> Option<BinaryOperator> {
399        match self {
400            Self::Unsupported => None,
401            Self::Exponent => Some(BinaryOperator::Exponent),
402            Self::BitwiseXor => Some(BinaryOperator::BitwiseXor(BitwiseXorSpelling::Caret)),
403        }
404    }
405}
406
407/// Dialect-owned comment syntax extensions.
408///
409/// Standard `--` line comments and `/* … */` block comments are always part of
410/// the baseline. These flags cover dialect-specific comment forms — and the
411/// dialect-specific *shape* of the baseline forms — whose recognition is an
412/// explicit dialect data decision.
413#[derive(Clone, Copy, Debug, PartialEq, Eq)]
414pub struct CommentSyntax {
415    /// Treat `#` as a line-comment introducer (MySQL). Mutually exclusive with
416    /// using `#` as an identifier byte (T-SQL `#temp`): a dialect that sets this
417    /// must not also mark `#` an identifier-start in its byte classes, or the
418    /// comment branch wins and `#temp` would never lex as a word.
419    pub line_comment_hash: bool,
420    /// Whether a bare carriage return (`\r`, `0x0d`) ends a `--`/`#` line comment, on
421    /// top of the newline (`\n`) that always ends one. PostgreSQL and DuckDB terminate a
422    /// line comment at *either* `\n` or `\r` — their flex scanner's comment body is
423    /// `[^\n\r]*` (engine-verified: `SELECT 1 -- c\rFROM` reads `FROM` as a live token and
424    /// rejects) — while SQLite and MySQL end it at `\n` alone, treating a `\r` as ordinary
425    /// comment content (the same input is one comment to end-of-line, accepted). All four
426    /// engines fold `\r` as whitespace *outside* a comment, so this flag governs only
427    /// whether `\r` *ends* a comment, never how it lexes elsewhere. The terminating byte is
428    /// left for the whitespace scan either way (`\r` is in `CLASS_WHITESPACE` for every
429    /// preset), so it never joins the comment's trivia span — matching PG, whose `[^\n\r]*`
430    /// body excludes the `\r`. `0x0b`/`0x0c` (vertical tab / form feed) sit in the flex
431    /// `space` set but not its newline set, so they are never terminators here.
432    pub line_comment_ends_at_carriage_return: bool,
433    /// Whether `/* … */` block comments nest: an inner `/*` raises a depth an
434    /// inner `*/` must lower before the comment can close. PostgreSQL nests;
435    /// MySQL ends every block comment at the first `*/` (engine-verified against
436    /// mysql:8 — `SELECT /* a /* b */ 1` parses as `SELECT 1` there, while a
437    /// nesting scanner reads it as an unterminated comment). The permissive
438    /// nesting superset predates this flag, so every preset except
439    /// MySQL keeps it on.
440    pub nested_block_comments: bool,
441    /// MySQL versioned comments (`/*! … */`, `/*!NNNNN … */`) as *conditional
442    /// inclusion*: the engine executes the body, so it is not a comment. `None`
443    /// (every non-MySQL dialect) keeps the whole construct an ordinary block
444    /// comment. `Some(bound)` models a server whose `MYSQL_VERSION_ID` is
445    /// `bound`: the body lexes as live tokens when the version is absent or
446    /// `<= bound`, and the region is discarded wholesale when the version
447    /// exceeds it — exactly the engine's include/skip gate.
448    ///
449    /// Engine-verified semantics (probed against mysql:8, 8.4.10): the version
450    /// is the digit run immediately abutting the `!` (a space breaks it) —
451    /// exactly five or exactly six digits form a version; 0–4 digits are not a
452    /// version (the digits stay body tokens and the region is included
453    /// unconditionally); from a run of ≥7 the first five are the version.
454    /// Regions do not nest (a flag, not a depth): a passing inner `/*!NNNNN`
455    /// marker is a no-op, a failing one discards only up to the next `*/`, and
456    /// the first region-level `*/` closes the region. A `*/` inside a string
457    /// literal of an *included* body does not close it (the body is lexed
458    /// normally), while a *discarded* body is raw bytes — not string-aware.
459    pub versioned_comments: Option<u32>,
460    /// Whether an unterminated `/* … ` block comment running to end of input is silently
461    /// closed (valid trailing trivia) rather than the pre-existing hard
462    /// `UnterminatedBlockComment` error. SQLite's tokenizer swallows a `/*` whose body runs
463    /// off the end as a `TK_SPACE` (engine-measured on rusqlite: `SELECT 1/* eof` and a
464    /// whitespace-wrapped `\t\t/*\t\t` both prepare), while every other engine rejects it.
465    ///
466    /// The one exception, replicated here: SQLite treats a *bare* `/*` sitting exactly at
467    /// end of input (no byte after the `*`, `z[2]==0`) as the `/` slash operator, not a
468    /// comment — so `/*` alone and `SELECT 1 /*` still reject on both. The scanner honours
469    /// this by opening a silently-EOF-closed comment only when a byte follows the `/*`. On
470    /// for SQLite / Lenient, off elsewhere.
471    pub unterminated_block_comment_at_eof: bool,
472}
473
474/// One accepted identifier-quoting delimiter style.
475///
476/// SQL identifier quotes escape an embedded close delimiter by *doubling* it
477/// (`"a""b"`, `[a]]b]`, and the backtick analogue), so the escape rule is implied by the kind —
478/// there is no separate escape character. A dialect accepts a *set* of these
479/// ([`FeatureSet::identifier_quotes`]); the tokenizer emits a single `QuotedIdent`
480/// token spanning the delimiters, and stripping/unescaping into an `Ident` is a
481/// later stage that reads the style back from the span's opening byte.
482#[derive(Clone, Copy, Debug, PartialEq, Eq)]
483pub enum IdentifierQuote {
484    /// Open and close are the same delimiter (`"`, `` ` ``).
485    Symmetric(char),
486    /// Distinct open/close (T-SQL `[a]`); only the close needs doubling to escape.
487    Asymmetric {
488        /// The opening delimiter character.
489        open: char,
490        /// The closing delimiter character.
491        close: char,
492    },
493}
494
495impl IdentifierQuote {
496    /// The opening delimiter.
497    pub const fn open(self) -> char {
498        match self {
499            Self::Symmetric(delim) => delim,
500            Self::Asymmetric { open, .. } => open,
501        }
502    }
503
504    /// The closing delimiter (doubled to escape an embedded close).
505    pub const fn close(self) -> char {
506        match self {
507            Self::Symmetric(delim) => delim,
508            Self::Asymmetric { close, .. } => close,
509        }
510    }
511}
512
513/// Dialect-owned string literal syntax extensions.
514///
515/// Standard single-quoted strings are always part of the baseline. These flags
516/// cover dialect-specific forms whose recognition must be an explicit dialect
517/// data decision, not a parser-side type check. Each is a lexical
518/// gate: it changes which token the scanner emits, never how a value is
519/// materialized (deferred).
520#[derive(Clone, Copy, Debug, PartialEq, Eq)]
521pub struct StringLiteralSyntax {
522    /// Accept PostgreSQL `E'...'` escape string constants.
523    pub escape_strings: bool,
524    /// Accept PostgreSQL `$tag$...$tag$` dollar-quoted string constants.
525    pub dollar_quoted_strings: bool,
526    /// Accept `N'...'` national-character string constants (T-SQL; PostgreSQL
527    /// sugar). Lexes as a `String` token spanning the `N` prefix.
528    pub national_strings: bool,
529    /// Lex `"..."` as a string constant rather than a quoted identifier (MySQL
530    /// without `ANSI_QUOTES`). Takes precedence over identifier quoting for `"`,
531    /// so a preset enabling this must not also list `"` in `identifier_quotes`.
532    pub double_quoted_strings: bool,
533    /// Honour C-style backslash escapes inside `'...'` (and double-quoted) strings
534    /// for termination (MySQL default; not T-SQL), so `'a\'b'` is one token. The
535    /// escape is recognised lexically only; value materialization stays deferred.
536    ///
537    /// This bool collapses what PostgreSQL historically made a *tri-state*: legacy
538    /// `standard_conforming_strings = off` made a plain `'…'` backslash-aware too — a
539    /// third mode distinct from both the modern-off default and MySQL's always-on. The
540    /// bool covers every *shipped* preset (modern PostgreSQL off, MySQL on); the
541    /// version-varying third mode belongs to the deferred per-release version presets
542    /// (`prod-dialect-release-version-presets`), which own version-varying knobs.
543    pub backslash_escapes: bool,
544    /// Accept `U&'...'` Unicode-escape string constants (SQL standard, PostgreSQL).
545    pub unicode_strings: bool,
546    /// Accept `B'...'` / `X'...'` bit-string constants (SQL standard, PostgreSQL).
547    /// Lexes as a `String` token spanning the `B`/`X` prefix; the binary-vs-hex
548    /// radix and the digit validation are recovered later, so a
549    /// malformed body like `X'1FG'` still lexes (PostgreSQL defers the check too).
550    pub bit_string_literals: bool,
551    /// Accept SQLite/MySQL `x'53514C'` / `X'53514c'` hexadecimal byte-string literals
552    /// (SQLite's BLOB literal; MySQL's hexadecimal literal). Only the `x`/`X` hex marker
553    /// — the `B'…'` binary form is [`bit_string_literals`](Self::bit_string_literals),
554    /// not this — and the quote must abut the marker (like `E'`/`B'`).
555    ///
556    /// Unlike a PostgreSQL/DuckDB deferred bit-string, the body is validated **eagerly at
557    /// lex time**: it must be an *even* number of ASCII hex digits (each pair is one
558    /// byte), so an odd-length (`x'ABC'`, `x'0'`) or non-hex (`x'XY'`) body is a
559    /// tokenize-time syntax error — the rule both engines enforce (probed: SQLite
560    /// "unrecognized token", MySQL `ER_PARSE_ERROR`). The empty body `x''` is a valid
561    /// zero-byte blob. It lexes as a `String` token spanning the marker and classifies as
562    /// a hex [`BitString`](crate::ast::LiteralKind::BitString) — the same canonical
563    /// hex-digit-string shape as `X'…'`, differing only in this eager lex-time bound; the
564    /// spelling round-trips from the span and a consumer reads the bytes via
565    /// [`as_bit_text`](crate::ast::Literal::as_bit_text).
566    ///
567    /// Disjoint from `bit_string_literals` on the shared `x`/`X` marker by scan
568    /// precedence: where both are on (MySQL), the eager hex arm claims `x`/`X` and the
569    /// deferred bit arm keeps `B`/`b`; where only `bit_string_literals` is on
570    /// (PostgreSQL) `X'…'` stays the deferred bit-string (odd-length allowed).
571    pub blob_literals: bool,
572    /// Accept MySQL `_charset'...'` character-set introducers — an `_`-prefixed
573    /// charset name abutting a string constant (`_utf8mb4'x'`, `_latin1'x'`). Like
574    /// the `N'...'` national prefix this lexes as one `String` token spanning the
575    /// introducer; the charset name is a surface tag that rides the span and is
576    /// recovered on demand, and the value materialises with the
577    /// introducer stripped. The abutting `'` is required — a bare `_name` with no
578    /// quote stays an ordinary identifier — so with this off `_utf8'x'` lexes as the
579    /// identifier `_utf8` then a string (the ANSI/PostgreSQL behaviour).
580    pub charset_introducers: bool,
581    /// Concatenate adjacent string literals separated by whitespace with *no* newline
582    /// (`'a' 'b'` on one line → `'ab'`), MySQL's rule. The SQL standard (and the default
583    /// here) requires a newline in the separator, so `'a' 'b'` same-line is
584    /// otherwise an adjacency error. Not a lexical gate — each literal still tokenizes
585    /// separately; the parser reads this at the continuation-gap classification point and
586    /// the AST materializer walks the folded span the same way it walks the newline form.
587    /// A comment in the gap still blocks concatenation under either rule.
588    pub same_line_adjacent_concat: bool,
589}
590
591/// Dialect-owned numeric literal syntax extensions.
592///
593/// Standard decimal/scientific numbers are always part of the baseline. These
594/// flags cover non-ANSI numeric forms whose recognition is an explicit dialect
595/// data decision. Each is lexical: it widens which numbers the scanner
596/// accepts as one token; value materialization stays deferred. The
597/// radix and separator forms below are PostgreSQL 14+ additions (also in T-SQL /
598/// MySQL for hex), so a release-pinned PostgreSQL preset gates them by version.
599#[derive(Clone, Copy, Debug, PartialEq, Eq)]
600pub struct NumericLiteralSyntax {
601    /// Accept `0x..` hexadecimal integer literals (T-SQL, MySQL, PostgreSQL 14+).
602    pub hex_integers: bool,
603    /// Accept `0o..` octal integer literals (PostgreSQL 14+).
604    pub octal_integers: bool,
605    /// Accept `0b..` binary integer literals (MySQL, PostgreSQL 14+).
606    pub binary_integers: bool,
607    /// Accept `_` digit-group separators between digits (PostgreSQL 14+), e.g.
608    /// `1_500_000`. Recognised lexically only when a digit follows the `_`. When
609    /// [`reject_trailing_junk`](Self::reject_trailing_junk) is off the placement is
610    /// loose (a `_` is a separator wherever a digit follows it); when it is on the
611    /// placement is strict (a decimal `_` must sit *between* two digits, matching PG's
612    /// `{decdigit}(_?{decdigit})*`), so a leading-in-fraction/trailing/doubled `_`
613    /// stops the number and surfaces as trailing junk. Whether a `_` may additionally
614    /// *lead* a radix body (`0x_1F`) is a separate axis
615    /// ([`radix_leading_underscore`](Self::radix_leading_underscore)), because PG and
616    /// SQLite disagree on it.
617    pub underscore_separators: bool,
618    /// Accept a leading `_` immediately after a radix marker, before the first radix
619    /// digit (`0x_1F`, `0b_101`) — PostgreSQL's `0[xX](_?{hexdigit})+` grammar, which
620    /// admits the underscore ahead of the first digit. Requires
621    /// [`underscore_separators`](Self::underscore_separators); an interior radix `_`
622    /// (`0x1_F`) rides that flag alone and is unaffected by this one.
623    ///
624    /// Its own axis rather than a rider on [`reject_trailing_junk`](Self::reject_trailing_junk)
625    /// because the engines that reject trailing junk still split on it: PostgreSQL accepts
626    /// `0x_1F` (probed) but SQLite rejects it — SQLite's radix grammar is
627    /// `0[xX]{hexdigit}(_?{hexdigit})*`, requiring a digit before the first `_`. With this
628    /// off a leading-underscore radix body does not open, so `0x_1F` falls back to the
629    /// bare `0` plus a `x_1F` word/alias (loose dialects) or a trailing-junk reject
630    /// (strict ones), exactly as before this axis existed.
631    pub radix_leading_underscore: bool,
632    /// Reject an identifier-start character abutting a numeric literal — PostgreSQL's
633    /// "trailing junk after numeric literal" scanner error (`123abc`, `1x`, `0.0e`,
634    /// `100_`), plus the bad-radix (`0x`, `0b0x`) and misplaced-separator (`100__000`,
635    /// `1_000._5`) forms that decompose to it. A number is a maximal-munch lexeme, so
636    /// with this on anything identifier-ish immediately following one is a lexer error,
637    /// not a new token; it also switches [`underscore_separators`] to strict placement.
638    ///
639    /// Dialect-gated because the reject is not universal: PostgreSQL and SQLite reject
640    /// these (both probed against their engines), but **DuckDB accepts them all** (it
641    /// re-reads `123abc` as `123` aliased) and MySQL only rejects the integer/radix forms
642    /// — so a dialect that lexes its numerics loosely (DuckDB/MySQL) leaves this off and
643    /// the trailing text falls through to the ordinary word/alias scan, exactly as before.
644    ///
645    /// [`underscore_separators`]: Self::underscore_separators
646    pub reject_trailing_junk: bool,
647    /// Accept `$1234.56` / `$.5` T-SQL money literals (the `$` currency sigil prefixes
648    /// a decimal). Off in ANSI/PostgreSQL: PostgreSQL spells `$` as a positional
649    /// parameter (`$1`) or a dollar-quote (`$tag$`), never money, so the three
650    /// `$`-prefix lexer forms are dialect-disjoint. Lexes as a `Number` token spanning
651    /// the `$`; the money type rides the literal kind and the sigil is stripped at the
652    /// accessor, like the `B`/`X` bit-string prefix.
653    pub money_literals: bool,
654}
655
656/// Dialect-owned prepared-statement parameter placeholder syntax.
657///
658/// SQL has no single standard placeholder spelling, so which forms a dialect
659/// accepts is explicit dialect data, not a parser-side type check. Each
660/// flag is a lexical gate: it decides whether the scanner recognises that sigil as
661/// a parameter [`TokenKind`](crate::ast) rather than a stray byte.
662#[derive(Clone, Copy, Debug, PartialEq, Eq)]
663pub struct ParameterSyntax {
664    /// Accept PostgreSQL positional `$1`, `$2`, … (`$` + ASCII digits). Disjoint
665    /// from dollar-quoting: `$` + digit is a parameter, `$` + tag-start/`$` opens a
666    /// dollar-quoted string, so both forms can be enabled together.
667    pub positional_dollar: bool,
668    /// Accept anonymous positional `?` placeholders (ODBC/JDBC).
669    pub anonymous_question: bool,
670    /// Accept colon-named `:name` placeholders (Oracle, SQLite, JDBC/psycopg). The
671    /// sigil must abut an identifier-start byte, which is what keeps `:name` free of
672    /// the two other `:` meanings: `::` stays the typecast (its second `:` is not an
673    /// identifier byte) and a lone `:` before a non-identifier byte stays the
674    /// array-slice separator. (A dialect that *also* sliced arrays with bare
675    /// identifier bounds — `a[x:y]` — or wrote semi-structured paths as `a:b` would lex
676    /// `:y`/`:b` as a parameter; that pairing is the tracked
677    /// [`LexicalConflict::ColonParameterVersusSliceBound`], since no real dialect combines
678    /// `:name` with those spellings.)
679    pub named_colon: bool,
680    /// Accept at-sign-named `@name` placeholders (T-SQL parameters/local variables).
681    /// The sigil must abut an identifier-start byte, so the system-variable `@@name`
682    /// form is left unclaimed for `prod-token-identifier-prefix-tokens` (the second
683    /// `@` is not an identifier byte, so this never grabs `@@`).
684    ///
685    /// Mutually exclusive with [`SessionVariableSyntax::user_variables`]: both claim
686    /// the `@name` trigger (one as a placeholder, one as a user-variable read), so a
687    /// feature set enabling both is a [`LexicalConflict`]. `@name`-as-parameter
688    /// (T-SQL) and `@name`-as-user-variable (MySQL) are the same surface with
689    /// different meaning, so a dialect picks one.
690    pub named_at: bool,
691    /// Accept dollar-named `$name` placeholders (SQLite). The sigil must abut an
692    /// identifier-start byte — `$` + a *digit* stays the PostgreSQL positional
693    /// `$1` ([`positional_dollar`](Self::positional_dollar)) and `$` + a
694    /// non-identifier byte a stray/money form — so this is follow-set-disjoint from
695    /// both `$`-digit forms and can be enabled alongside them.
696    ///
697    /// Contends only with [`StringLiteralSyntax::dollar_quoted_strings`]: a
698    /// `$tag$…$tag$` opener also leads with `$` + a tag-start byte (the same class as
699    /// identifier-start), so enabling both is a
700    /// [`LexicalConflict::NamedDollarParameterVersusDollarQuotedString`]. SQLite has
701    /// no dollar-quoting, so the two never meet in a shipped preset.
702    pub named_dollar: bool,
703    /// Accept SQLite numbered `?NNN` positional parameters (`?1`, `?123`) — the `?` sigil
704    /// abutting an ASCII-digit run — on top of the bare anonymous `?`
705    /// ([`anonymous_question`](Self::anonymous_question)). Follow-set-disjoint from the
706    /// anonymous form by the digit (a `?` with no digit stays anonymous), exactly as
707    /// PostgreSQL's `$1` splits from `$name`. The number is a maximal digit run, so a
708    /// trailing identifier is a separate token (`?1abc` is `?1` then the alias `abc`;
709    /// engine-measured). SQLite range-restricts the index to `1..=32766`
710    /// (`SQLITE_MAX_VARIABLE_NUMBER`): `?0`, `?32767`, and an overflowing run are parse
711    /// rejects ("variable number must be between ?1 and ?32766"; probed), enforced when the
712    /// numbered token is materialised. On for SQLite / Lenient, off elsewhere.
713    pub numbered_question: bool,
714}
715
716/// Dialect-owned MySQL session-variable syntax.
717///
718/// MySQL exposes user-defined `@name` variables and server `@@[scope.]name` system
719/// variables as *value expressions* — distinct from a prepared-statement placeholder
720/// ([`ParameterSyntax`]), which is a hole bound at execute time. Each flag is a
721/// lexical gate: it decides whether the scanner recognises that sigil as a variable
722/// token ([`Expr::SessionVariable`](crate::ast::Expr::SessionVariable)) rather than a
723/// stray byte. The two forms are independent — a dialect can accept `@@sysvar` without
724/// `@uservar` — and are lookahead-disjoint (`@@` needs a second `@`, `@name` an
725/// identifier-start), so they never contend with each other.
726///
727/// [`Expr::SessionVariable`]: crate::ast
728#[derive(Clone, Copy, Debug, PartialEq, Eq)]
729pub struct SessionVariableSyntax {
730    /// Accept `@name` user-defined session variables (MySQL). The sigil must abut an
731    /// identifier-start byte. Mutually exclusive with
732    /// [`ParameterSyntax::named_at`]: both claim `@name`, so enabling both is a
733    /// [`LexicalConflict::AtNameParameterVersusUserVariable`].
734    pub user_variables: bool,
735    /// Accept `@@name` / `@@global.name` / `@@session.name` system variables (MySQL).
736    /// The `@@` must abut an identifier-start byte; a scoped form folds the optional
737    /// `global.`/`session.` prefix into the same token. Never contends with `named_at`
738    /// or `user_variables` — the second `@` keeps `@@` lookahead-disjoint from the
739    /// single-`@` forms.
740    pub system_variables: bool,
741    /// Accept the MySQL `SET` **variable-assignment** statement grammar and its `:=`
742    /// assignment operator (MySQL).
743    ///
744    /// Two facets of one behaviour that ship together in MySQL and nowhere else:
745    /// * *Parser* — a `SET` becomes a comma-separated list of heterogeneous assignments
746    ///   ([`SessionStatement::SetVariables`](crate::ast::SessionStatement::SetVariables)):
747    ///   `[GLOBAL|SESSION|LOCAL|PERSIST|PERSIST_ONLY] <var> {= | :=} <expr>`, the
748    ///   `@@[scope.]<var>` spellings, user variables `@v {= | :=} <expr>`, and
749    ///   `SET {CHARACTER SET | CHARSET} …`. Each value is a *full expression*, unlike the
750    ///   generic PostgreSQL `SET`'s restricted literal/bareword value list, so the two
751    ///   grammars are distinct statements rather than one widened form.
752    /// * *Lexer* — `:=` lexes to the `ColonEquals` operator token (MySQL's `SET_VAR`), the
753    ///   same token PostgreSQL's deprecated named-argument separator produces; the two never
754    ///   coexist in a shipped preset, so the shared token is unambiguous per dialect.
755    ///
756    /// The two facets sit on independent axes, which is why this flag is a documented
757    /// exemption from the [`feature_dependencies`](FeatureSet::feature_dependencies) registry
758    /// rather than a [`FeatureDependencyViolation`] variant. The *parser* facet is unreachable
759    /// without [`show_syntax.session_statements`](ShowSyntax::session_statements) — the leader
760    /// that dispatches every `SET`/`RESET`/`SHOW`, so with it off no `SET` form parses at all
761    /// (measured: `SET x = 1` is rejected as an unknown statement leader) — a genuine
762    /// cross-axis grammar dependency. The *lexer* facet, however, fires whether or not that
763    /// leader is on: with `session_statements` off, `:=` still munches to one `ColonEquals`
764    /// token (measured), so the flag is **not** inert without its parser-side base. A registry
765    /// whose contract is inertness cannot own a flag that keeps changing tokenization while
766    /// its base is off, so the dependency is recorded here instead. See the exemption note on
767    /// [`feature_dependencies`](FeatureSet::feature_dependencies).
768    ///
769    /// This is a *route* flag on the `SET` head — when on it dispatches the MySQL grammar before
770    /// the standard `SET TIME ZONE` / `SET SESSION AUTHORIZATION` forms are read (see the parser's
771    /// `parse_set`) — but unlike the
772    /// [`access_control_account_grants`](AccessControlSyntax::access_control_account_grants) route
773    /// it carries *no* [`GrammarConflict`] variant: the grammar it displaces is unconditional base
774    /// grammar with no rival feature flag, so there is no independently-expressible both-on state,
775    /// and MySQL (a shipped preset) already exercises the route deterministically. Registering it
776    /// would require promoting the standard `SET` config grammar to its own flag or converting this
777    /// field to an enum axis — see the [`GrammarConflict`] enum doc's route-flag discussion.
778    pub variable_assignment: bool,
779}
780
781/// Dialect-owned policy for which characters form an *unquoted* identifier.
782///
783/// The identifier-start and identifier-continue classes are an explicit,
784/// Unicode-aware policy, not an ad-hoc byte rule. The default mirrors
785/// PostgreSQL — the M1 reference dialect: an identifier starts with a Unicode
786/// *letter* (`char::is_alphabetic`) or `_`, and continues with a letter, a Unicode
787/// *digit* (`char::is_alphanumeric`), `_`, or — where [`dollar_in_identifiers`] is
788/// set — `$`. Quoted identifiers bypass the policy entirely (any character may be
789/// quoted), and no identifier *normalization* (NFC/NFKC) is performed — characters
790/// are compared as written; case folding for identity is the separate
791/// [`identifier_casing`] concern.
792///
793/// Only the dialect-*variable* part lives here. The ASCII letter/digit/`_` classes
794/// are the shared byte-class table (so a dialect can still add ASCII identifier bytes
795/// like T-SQL `#`/`@` through [`byte_classes`]), and the non-ASCII rule is the fixed
796/// Unicode letter/alphanumeric property. The one knob that genuinely differs across
797/// shipped dialects is `$`.
798///
799/// [`dollar_in_identifiers`]: IdentifierSyntax::dollar_in_identifiers
800/// [`identifier_casing`]: FeatureSet::identifier_casing
801/// [`byte_classes`]: FeatureSet::byte_classes
802#[derive(Clone, Copy, Debug, PartialEq, Eq)]
803pub struct IdentifierSyntax {
804    /// Accept `$` as an identifier-*continue* character (`foo$bar`), a PostgreSQL /
805    /// Oracle extension that strict ANSI forbids. `$` never *starts* an identifier (a
806    /// leading `$` is a parameter or dollar-quote), and it is never a dollar-quote
807    /// *tag* character (there `$` is the delimiter).
808    pub dollar_in_identifiers: bool,
809    /// Accept a single-quoted string literal in identifier positions *beyond* aliases —
810    /// SQLite's misfeature where a `'name'` string is read as a name wherever the grammar
811    /// wants a `nm` (identifier). Corpus-admitted for the two positions the SQLite
812    /// test-suite surfaces: a DML/DDL *relation-target* name (`DELETE FROM 'table1'`, and
813    /// so `CREATE TABLE 'name'` / `DROP TABLE 'n'` / `INSERT INTO 'n'` through the shared
814    /// target path) and a `PRIMARY KEY`/`UNIQUE` *table-constraint column-name* list
815    /// (`PRIMARY KEY('a')`, `UNIQUE('b')`). Each admitted position is *position-driven*:
816    /// a bare string there is never a valid literal in standard SQL, so reading it as the
817    /// name is unambiguous — no lexical or grammar conflict (the tokenizer still lexes
818    /// `'x'` to a `String`; a single parser position reads it, shadowing no rival feature).
819    ///
820    /// SQLite's full leniency is broader (a string is also admitted as a column-def name,
821    /// a `CAST` type name, a qualified column-ref qualifier, a `CREATE VIEW`/`TRIGGER`
822    /// target, …); those positions carry no corpus gap and stay out of scope. A string
823    /// *function* name is a SQLite syntax error (`SELECT 'f'(1)`), so the widening is
824    /// deliberately confined to the two name positions rather than folded into the shared
825    /// object-name grammar. The folded name records [`QuoteStyle::Single`] (or `Double`
826    /// under a no-`ANSI_QUOTES` mode) so the quotes round-trip, reusing the projection
827    /// alias's string round-trip. On for SQLite / Lenient, off elsewhere — every other
828    /// dialect syntax-rejects the form, so the over-acceptance risk is zero (flag off).
829    ///
830    /// [`QuoteStyle::Single`]: crate::ast::QuoteStyle::Single
831    pub string_literal_identifiers: bool,
832    /// Accept a *zero-length* delimited (quoted) identifier — the empty backtick
833    /// `` `` ``, the empty bracket `[]`, and the empty double-quote `""` — which SQL's
834    /// `<delimited identifier body>` and PostgreSQL/MySQL both forbid at scan time. SQLite
835    /// alone among the shipped engines admits an empty quoted identifier in every quote
836    /// style (engine-measured on rusqlite: `` SELECT `` ``, `SELECT []`, and `SELECT ""`
837    /// all prepare — the `""` via SQLite's double-quote-to-string fallback, the others as
838    /// zero-length names). When off, the tokenizer rejects a zero-length quoted identifier
839    /// the moment the close abuts the open (the pre-existing, universal
840    /// `ZeroLengthDelimitedIdentifier` scan reject). On for SQLite / Lenient, off elsewhere.
841    pub empty_quoted_identifiers: bool,
842}
843
844/// Dialect-owned table-expression syntax extensions.
845#[derive(Clone, Copy, Debug, PartialEq, Eq)]
846pub struct TableExpressionSyntax {
847    /// Accept PostgreSQL `ONLY table` / `ONLY (table)` inheritance suppression.
848    pub only: bool,
849    /// Accept `TABLESAMPLE method(args...) [REPEATABLE (...)]`.
850    pub table_sample: bool,
851    /// Accept joined tables as parenthesized table factors (`FROM (a JOIN b) JOIN c`).
852    /// On in every shipped preset — the standard join grouping — but stays gateable, so
853    /// a stricter dialect that forbids parenthesizing a join leaves it off and the form
854    /// then surfaces as a clean parse error.
855    pub parenthesized_joins: bool,
856    /// Accept `alias(column, ...)` derived-column lists after table factors. On in every
857    /// shipped preset (the SQL-standard correlation-name column list), but stays gateable
858    /// so a dialect without the form can reject it as trailing input.
859    pub table_alias_column_lists: bool,
860    /// Accept PostgreSQL `JOIN ... USING (...) AS alias`.
861    pub join_using_alias: bool,
862    /// Accept MySQL index hints (`{USE|FORCE|IGNORE} {INDEX|KEY} [FOR …] (…)`) as a
863    /// table-factor tail after the alias. MySQL-only, so it rides
864    /// [`TableFactor::Table::index_hints`](crate::ast::TableFactor); when off the hint
865    /// keywords are left to the identifier grammar and the construct is a clean parse
866    /// divergence. On for MySQL / Lenient, off elsewhere.
867    pub index_hints: bool,
868    /// Accept MSSQL / T-SQL `WITH ( <hint>, … )` table hints (`WITH (NOLOCK)`,
869    /// `WITH (INDEX(ix), FORCESEEK)`) as a table-factor tail after the tablesample
870    /// clause. T-SQL-only, so it rides
871    /// [`TableFactor::Table::table_hints`](crate::ast::TableFactor); when off the
872    /// trailing `WITH` is left unconsumed, so under ANSI/PostgreSQL — where `WITH`
873    /// introduces only a leading CTE clause at statement start — the construct is a
874    /// clean parse divergence. A separate axis from [`index_hints`](TableExpressionSyntax::index_hints):
875    /// a different dialect (T-SQL vs MySQL) and a different grammar position. On for
876    /// MSSQL / Lenient, off elsewhere.
877    pub table_hints: bool,
878    /// Accept MySQL explicit partition selection (`PARTITION (p0, p1)`) as a
879    /// table-factor tail between the table name and the alias. MySQL-only, so it rides
880    /// [`TableFactor::Table::partition`](crate::ast::TableFactor); when off the
881    /// `PARTITION` keyword is left unconsumed and the construct is a clean parse
882    /// divergence. On for MySQL / Lenient, off elsewhere.
883    pub partition_selection: bool,
884    /// Accept a column-list alias (`AS y(a, b)`) on a *base table* factor
885    /// (`FROM t AS y(a, b)`). On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL admits a
886    /// column-list alias only on a *derived* table / subquery / table function
887    /// (`FROM (SELECT …) AS c(x)` parses on mysql:8, only bind-failing) and rejects one on
888    /// a base table (`FROM t AS y(a, b)` is an `ER_PARSE_ERROR` on mysql:8), so it is off
889    /// there; the `(` after the base-table alias name is then a clean parse error. The
890    /// broader [`table_alias_column_lists`](TableExpressionSyntax::table_alias_column_lists) gate governs
891    /// whether the dialect admits column-list aliases *at all* (for the derived/function
892    /// positions); this one further restricts the base-table position — the base-vs-derived
893    /// split MySQL draws.
894    pub base_table_alias_column_lists: bool,
895    /// Accept a single-quoted string literal in table-alias position — both the
896    /// correlation name after an explicit `AS` (`FROM integers AS 't'`) and each entry
897    /// of the alias column list (`FROM integers AS 't'('k')` / `FROM integers t('k')`),
898    /// reusing the projection alias's string-literal round-trip. DuckDB admits this only
899    /// after `AS`: a bare `FROM integers 't'` is an engine reject (probed on 1.5.4),
900    /// preserved by the alias site's leading-string guard.
901    ///
902    /// Deliberately *separate* from [`SelectSyntax::alias_string_literals`], which gates
903    /// the string spelling in *projection* position: the two profiles diverge. MySQL
904    /// accepts a string *column* alias (`SELECT 1 AS 'x'`) but rejects a string *table*
905    /// alias (`FROM t AS 't'` — engine-measured-rejected on mysql:8), so folding both
906    /// onto one flag would make MySQL over-accept the table form. On for DuckDb /
907    /// Lenient, off elsewhere (including MySQL).
908    pub string_literal_aliases: bool,
909    /// Accept a correlation alias on a *parenthesized joined table*
910    /// (`FROM (a CROSS JOIN b) AS x`). On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL
911    /// admits a parenthesized join ([`parenthesized_joins`](TableExpressionSyntax::parenthesized_joins)) but
912    /// rejects an alias on it (`(a CROSS JOIN b) AS x` is an `ER_PARSE_ERROR` on mysql:8,
913    /// while the bare `(a CROSS JOIN b)` and a derived-table `(SELECT …) AS x` both parse),
914    /// so it is off there and the trailing alias surfaces as a clean parse error. Only the
915    /// *joined-table* parenthesization is governed; a derived subquery's alias rides the
916    /// always-accepted derived-table path.
917    pub aliased_parenthesized_join: bool,
918    /// Treat a bare (`AS`-less) *table* correlation alias as a `BareColLabel` — routed to
919    /// [`FeatureSet::reserved_bare_alias`] — instead of the default `ColId`
920    /// ([`FeatureSet::reserved_column_name`]). Off for every dialect except SQLite, where
921    /// the bare alias is the narrow `ids ::= ID|STRING` grammar class (not the `nm` name
922    /// class): the seven `JOIN_KW` keywords are admissible as a *table name* (`FROM cross`)
923    /// yet reserved as a *bare alias*, so `FROM t cross JOIN u` must keep `cross` for the
924    /// join grammar rather than read it as `t`'s alias. Routing the bare-table-alias gate
925    /// to the bare-alias set (which reserves the JOIN keywords) while the table-name gate
926    /// stays on the permissive `ColId` set is what makes the two positions diverge — the
927    /// table-alias twin of [`SelectSyntax::as_alias_rejects_reserved`]. The explicit `AS`
928    /// table alias keeps the `ColId` set (SQLite's `AS nm` admits the JOIN keywords).
929    pub bare_table_alias_is_bare_label: bool,
930    /// Accept a table version / time-travel modifier on a base table, written between the
931    /// table name and the alias: BigQuery/MSSQL `FOR SYSTEM_TIME …`, MSSQL's five temporal
932    /// forms (`AS OF`, `FROM … TO`, `BETWEEN … AND`, `CONTAINED IN`, `ALL`), and
933    /// Databricks/Delta `VERSION`/`TIMESTAMP AS OF`. Rides
934    /// [`TableFactor::Table::version`](crate::ast::TableFactor); when off the clause keyword
935    /// is left unconsumed, so a query-level `FOR` (row locking, MSSQL `FOR XML`) still parses
936    /// — the two `FOR` surfaces are position-partitioned, this one at the table factor and
937    /// the query-level ones after the whole `FROM`/`WHERE`. On for BigQuery / MSSQL /
938    /// Databricks / Lenient, off elsewhere.
939    pub table_version: bool,
940    /// Accept a PartiQL / SUPER JSON path navigating into a semi-structured column at the
941    /// table-source position, attached directly to the table name (`FROM src[0].a`,
942    /// `FROM src[0].a[1].b`). Redshift's SUPER navigation and Snowflake's PartiQL access
943    /// (sqlparser-rs's `supports_partiql`). Rides
944    /// [`TableFactor::Table::json_path`](crate::ast::TableFactor); the path is entered only
945    /// by a `[` immediately after the name (a bracket index root, then `.key` / `[index]`
946    /// suffixes), so a dotted `FROM src.a.b` stays a compound relation name. When off the
947    /// `[` is left unconsumed and the construct is a clean parse divergence. Because the
948    /// entry trigger is the `[` tokenizer trigger, this shares the
949    /// [`BracketIdentifierVersusArraySyntax`](crate::dialect::LexicalConflict) hazard: a
950    /// dialect with a `[` identifier quote cannot also enable it. On for Snowflake /
951    /// Redshift, off elsewhere — including Lenient, whose `[` bracket identifier quote claims
952    /// the trigger (the same reason Lenient keeps `subscript` / `collection_literals` off).
953    pub table_json_path: bool,
954    /// Accept a SQLite `INDEXED BY <index>` / `NOT INDEXED` index directive on a base table,
955    /// written after the table name and its optional alias (`FROM t AS e INDEXED BY ix`,
956    /// `FROM t NOT INDEXED`). Rides
957    /// [`TableFactor::Table::indexed_by`](crate::ast::TableFactor). When on, a bare
958    /// `INDEXED` at the base-table alias position is declined as a correlation alias so the
959    /// directive is reachable (the `CONNECT BY` clause-decline precedent), which is what
960    /// makes SQLite reject a bare `FROM t indexed` while still admitting `indexed` as an
961    /// identifier everywhere else (`SELECT indexed`, `t AS indexed`, `indexed INT`). A
962    /// separate axis from MySQL [`index_hints`](TableExpressionSyntax::index_hints): a
963    /// different dialect, grammar, and cardinality. On for SQLite, off elsewhere — including
964    /// Lenient, whose maximal-accept goal keeps a bare `FROM t indexed` an ordinary alias
965    /// (the directive-versus-bare-alias readings are mutually exclusive given the keyword's
966    /// one-position semantics, and Lenient prefers the more permissive bare-alias reading).
967    pub indexed_by: bool,
968}
969
970/// Dialect-owned join-operator and recursive-query relation-composition syntax
971/// accepted by the parser.
972///
973/// The join-family flags — the join operators and their side/qualifier variants beyond
974/// the always-available inner/left/right/cross joins — together with the recursive-query
975/// structural clauses that compose relations. Split out of [`TableExpressionSyntax`] at
976/// its 16-field line as the relation-composition axis, distinct from the table-factor and
977/// factor-tail/alias cores. Each flag is a grammar gate: when off the keyword is left to
978/// the identifier grammar or surfaces as a clean parse error.
979#[derive(Clone, Copy, Debug, PartialEq, Eq)]
980pub struct JoinSyntax {
981    /// Accept *stacked* join qualifiers — the PostgreSQL right-nesting where two joins
982    /// precede their `ON`/`USING` constraints and the nearest constraint closes the
983    /// innermost join: `a JOIN b JOIN c ON p ON q` reads as `a JOIN (b JOIN c ON p) ON q`
984    /// (PostgreSQL `table_ref: … | joined_table` right-recursion). On for ANSI/PostgreSQL/
985    /// MySQL/DuckDB/Lenient. SQLite's `join-clause` is flat — each `join-operator` takes
986    /// exactly one immediately-following constraint — so it is off; the right operand is
987    /// not extended past the first constraint, and a second stacked `ON`/`USING`
988    /// (`… USING (id) USING (id)`) is then left unconsumed and surfaces as the syntax
989    /// error SQLite reports (engine-measured via rusqlite). The common `a JOIN b ON x
990    /// JOIN c ON y` — each constraint right after its own join — needs no nesting and is
991    /// unaffected either way.
992    pub stacked_join_qualifiers: bool,
993    /// Accept the `FULL [OUTER] JOIN` bilateral outer join. On for ANSI/PostgreSQL/SQLite/
994    /// DuckDB/Lenient. MySQL has no `FULL` join (it offers only `LEFT`/`RIGHT` outer joins),
995    /// so it is off there: the `FULL` join-side keyword is not consumed, and an
996    /// already-aliased factor followed by `FULL [OUTER] JOIN` (`a x FULL OUTER JOIN b`) is
997    /// then a clean parse error — exactly what MySQL reports (engine-measured-rejected on
998    /// mysql:8). Because `FULL` is a non-reserved word in MySQL, a *bare* `a full JOIN b`
999    /// still reads `full` as the factor's alias (grabbed before the join loop), matching the
1000    /// engine — this flag only governs the join-side reading, never the alias.
1001    pub full_outer_join: bool,
1002    /// Accept `NATURAL CROSS JOIN` — `NATURAL` prefixing the `CROSS` join keyword.
1003    /// SQLite's `join-operator` validates its up-to-three keywords post-hoc and admits
1004    /// `NATURAL` before any join type, `CROSS` included; PostgreSQL and DuckDB both
1005    /// parse-reject it (engine-probed). Because `CROSS JOIN` is the optimizer-hint
1006    /// spelling of `INNER JOIN` and `NATURAL` supplies the shared-column constraint,
1007    /// `NATURAL CROSS JOIN` is semantically a natural inner join — engine-probed on
1008    /// rusqlite: it yields the shared-column equijoin's row/column shape, not the cross
1009    /// product — so the parser normalizes it into the canonical
1010    /// [`JoinOperator::Inner`](crate::ast::JoinOperator) + [`JoinConstraint::Natural`](crate::ast::JoinConstraint)
1011    /// shape (the `NATURAL INNER` precedent, where the redundant side keyword is likewise
1012    /// dropped), rendering back as `NATURAL JOIN`. No new AST field is needed: the
1013    /// round-trip oracle compares structure, not spelling, so the elided `CROSS` word
1014    /// re-parses to the same AST. On for SQLite / Lenient, off elsewhere; when off,
1015    /// `NATURAL CROSS` fails the `NATURAL` arm's mandatory `JOIN` and rejects unchanged.
1016    pub natural_cross_join: bool,
1017    /// Accept MySQL's `STRAIGHT_JOIN` join-order hint, in both of its surfaces: the
1018    /// join operator `a STRAIGHT_JOIN b [ON ...]` and the `SELECT STRAIGHT_JOIN ...`
1019    /// modifier. One flag gates both grammar points because they are a single dialect
1020    /// unit — MySQL always admits the modifier wherever it admits the join keyword
1021    /// (mirroring how [`CreateTableClauseSyntax::table_options`] gates the trailing options
1022    /// and the `AUTO_INCREMENT` attribute together). Both fold into the
1023    /// canonical inner-join shape / `Select` flag with a surface tag, never a new node.
1024    /// When off, `STRAIGHT_JOIN` is left to the identifier grammar (a non-reserved word
1025    /// under ANSI/PostgreSQL), so the MySQL construct is a clean parse divergence.
1026    pub straight_join: bool,
1027    /// Accept DuckDB's `ASOF [INNER|LEFT|RIGHT|FULL [OUTER]] JOIN` inexact-match
1028    /// temporal join ([`JoinOperator::AsOf`](crate::ast::JoinOperator)). When off,
1029    /// `ASOF` is left to the identifier grammar (a non-reserved word under
1030    /// ANSI/PostgreSQL) — and because the next word is `JOIN`, the text still parses
1031    /// there, as an aliased *plain* join (a different-tree reading, unlike
1032    /// `STRAIGHT_JOIN`'s leftover-input reject). The flag alone is not enough for the
1033    /// DuckDB meaning on a bare table factor — the word must also be in
1034    /// [`FeatureSet::reserved_column_name`] or the factor's alias swallows it first
1035    /// (the DuckDb preset reserves it; `LENIENT` keeps the ANSI reserved model, so
1036    /// there the join parses only after an explicit alias). On for DuckDb / Lenient,
1037    /// off elsewhere.
1038    pub asof_join: bool,
1039    /// Accept DuckDB's `POSITIONAL JOIN` row-position pairing join
1040    /// ([`JoinOperator::Positional`](crate::ast::JoinOperator)), which takes no
1041    /// `ON`/`USING` constraint and no side keyword. The same reserved-word
1042    /// interaction as [`asof_join`](JoinSyntax::asof_join) applies. On for DuckDb /
1043    /// Lenient, off elsewhere.
1044    pub positional_join: bool,
1045    /// Accept DuckDB's `SEMI JOIN` / `ANTI JOIN` semi-/anti-join operators
1046    /// ([`JoinOperator::Semi`](crate::ast::JoinOperator)/[`Anti`](crate::ast::JoinOperator::Anti)),
1047    /// including their `NATURAL` and `ASOF` compositions (`NATURAL SEMI JOIN`,
1048    /// `ASOF ANTI JOIN`). One flag gates both because `SEMI` and `ANTI` are the same
1049    /// grammar production (a `join_type` keyword taking an `ON`/`USING` constraint) that
1050    /// DuckDB only ever ships together — the paired-flag doctrine (the
1051    /// [`straight_join`](JoinSyntax::straight_join) precedent), unlike the distinct-grammar
1052    /// [`asof_join`](JoinSyntax::asof_join)/[`positional_join`](JoinSyntax::positional_join) pair.
1053    /// The same reserved-word interaction as [`asof_join`](JoinSyntax::asof_join) applies: the
1054    /// DuckDb preset reserves both words as a `ColId`/bare-alias so a bare
1055    /// `l SEMI JOIN r` reads the join rather than aliasing `l`; `LENIENT` keeps the ANSI
1056    /// reserved model, so there the join parses only after an explicit alias. On for
1057    /// DuckDb / Lenient, off elsewhere.
1058    pub semi_anti_join: bool,
1059    /// Accept the Spark/Hive/Databricks *sided* semi-/anti-join spelling
1060    /// (`{LEFT|RIGHT} {SEMI|ANTI} JOIN`), recorded as the
1061    /// [`SemiAntiSide`](crate::ast::SemiAntiSide) axis on the same
1062    /// [`JoinOperator::Semi`](crate::ast::JoinOperator)/[`Anti`](crate::ast::JoinOperator::Anti)
1063    /// operators as DuckDB's side-less form. A *separate* gate from
1064    /// [`semi_anti_join`](JoinSyntax::semi_anti_join) because it is a different engine family:
1065    /// DuckDB parse-rejects `LEFT SEMI JOIN` (engine-probed), so folding the sided
1066    /// spelling into `semi_anti_join` would over-accept it under the DuckDb preset. The
1067    /// leading `LEFT`/`RIGHT` keyword is already a reserved join side, so — unlike the
1068    /// keyword-led [`asof_join`](JoinSyntax::asof_join) pair — no reserved-word interplay is
1069    /// needed: the preceding factor's alias can never swallow it, and a plain
1070    /// `LEFT [OUTER] JOIN` is disambiguated by the following `SEMI`/`ANTI` keyword. One
1071    /// flag gates the whole sided family (both sides × `SEMI`/`ANTI`) as one grammar
1072    /// production (the [`straight_join`](JoinSyntax::straight_join)/[`semi_anti_join`](JoinSyntax::semi_anti_join)
1073    /// paired-flag doctrine). The sided form always takes an `ON`/`USING` constraint and
1074    /// never composes with `NATURAL`/`ASOF`. On for the Databricks and Hive presets (whose
1075    /// engine family documents the spelling — Hive originated `LEFT SEMI JOIN`) and for
1076    /// Lenient (the permissive superset); off elsewhere, where the other engines
1077    /// parse-reject the sided spelling. The atomic flag admits the `RIGHT`-sided and `ANTI`
1078    /// spellings those presets do not all document — a known conservative-direction
1079    /// over-acceptance a future side-refinement would tighten.
1080    pub sided_semi_anti_join: bool,
1081    /// Accept MSSQL's `CROSS APPLY` / `OUTER APPLY` join operators
1082    /// ([`JoinOperator::Apply`](crate::ast::JoinOperator)) — a lateral-correlated join
1083    /// over a right table factor (derived table or table-valued function), taking no
1084    /// `ON`/`USING` constraint. One flag gates the whole `APPLY` family (both the
1085    /// `CROSS` and `OUTER` flavours) because they are a single grammar production
1086    /// differing only by that keyword (the [`straight_join`](JoinSyntax::straight_join)
1087    /// paired-flag doctrine; the [`ApplyKind`](crate::ast::ApplyKind) axis carries the
1088    /// flavour). No reserved-word interplay is needed — the leading `CROSS`/`OUTER`
1089    /// keyword already anchors the operator, so the preceding factor's alias can never
1090    /// swallow it (unlike the keyword-led [`asof_join`](JoinSyntax::asof_join) pair). On for
1091    /// the MSSQL preset and Lenient (the permissive superset), off elsewhere: the other
1092    /// engines parse-reject `APPLY` in join position.
1093    pub apply_join: bool,
1094    /// Accept the SQL:2023 recursive-query `SEARCH { DEPTH | BREADTH } FIRST BY … SET …`
1095    /// and `CYCLE … SET … [TO … DEFAULT …] USING …` clauses on a CTE
1096    /// ([`Cte::search`](crate::ast::Cte)/[`cycle`](crate::ast::Cte)), written after the
1097    /// CTE body's `)`. One flag gates both clauses because PostgreSQL ships them as a
1098    /// single recursive-query unit (the [`straight_join`](JoinSyntax::straight_join)
1099    /// paired-flag doctrine) — no dialect admits one without the other. When off, the
1100    /// `SEARCH`/`CYCLE` keyword after a CTE body is left unconsumed and the statement is a
1101    /// clean parse error. On for PostgreSQL / Lenient; off for ANSI (the conservative
1102    /// baseline, like [`unnest`](TableFactorSyntax::unnest)), MySQL/SQLite (no such clauses), and
1103    /// DuckDB — which parse-rejects both (`syntax error at or near "SEARCH"`, probed on
1104    /// 1.5.4), so it overrides the PostgreSQL surface it otherwise inherits (the
1105    /// [`MutationSyntax::data_modifying_ctes`] split precedent).
1106    pub recursive_search_cycle: bool,
1107    /// Parse-reject a top-level `ORDER BY` / `LIMIT` / `OFFSET` on a recursive CTE whose
1108    /// body is a `UNION [ALL]` set operation (`WITH RECURSIVE t AS (SELECT … UNION ALL
1109    /// SELECT … FROM t ORDER BY …)`). DuckDB special-cases the recursive term's grammar:
1110    /// once `WITH RECURSIVE` fronts a `UNION`-bodied CTE the query is a *recursive query*,
1111    /// and a result-shaping modifier on it is a parse error (`Parser Error: ORDER BY in a
1112    /// recursive query is not allowed` / `LIMIT or OFFSET in a recursive query is not
1113    /// allowed`; probed on 1.5.4). "order_limit" names the whole modifier set — `ORDER BY`,
1114    /// `LIMIT`, and `OFFSET` — because DuckDB forbids them under one rule (no dialect admits
1115    /// one but not the others), so it is one behaviour, not three.
1116    ///
1117    /// Three boundaries are load-bearing and engine-probed (1.5.4), so the check mirrors
1118    /// them exactly rather than firing on the bare `RECURSIVE` keyword: the body must be a
1119    /// `UNION` set operation (a non-set-op recursive CTE, or an `INTERSECT`/`EXCEPT` body,
1120    /// keeps its modifiers — those are not recursive-eligible); the modifier must sit on the
1121    /// set operation itself, not a parenthesized arm or a nested subquery (`((SELECT …
1122    /// LIMIT 1) UNION ALL …)` and `… WHERE x < (SELECT … ORDER BY 1)` both accept); and
1123    /// self-reference is *not* required (DuckDB rejects even a `UNION` whose right arm never
1124    /// names the CTE — the check is syntactic). On for DuckDB only; every other preset
1125    /// parse-accepts the modifier (PostgreSQL admits it outright, the rest defer the
1126    /// restriction to binding), so it stays off there and the modifier rides the ordinary
1127    /// query tail.
1128    pub recursive_union_rejects_order_limit: bool,
1129    /// Accept DuckDB's `USING KEY (col, ...)` recursive-CTE key clause
1130    /// ([`Cte::using_key`](crate::ast::Cte)), written between the CTE column list and `AS`
1131    /// (`WITH RECURSIVE t(a, b) USING KEY (a) AS (…)`). DuckDB's keyed-recursion variant
1132    /// (stable since 1.3; probed accepting on 1.5.4): the recurring table becomes a
1133    /// key-indexed dictionary whose rows the recursive term overwrites in place. A distinct
1134    /// gate from [`recursive_search_cycle`](Self::recursive_search_cycle) — a different
1135    /// engine (DuckDB, not PostgreSQL) and a different grammar slot (before `AS`, not after
1136    /// the body's `)`), so neither implies the other. Positioned ahead of `AS`, the leading
1137    /// `USING` cannot be swallowed by any prior production (a CTE otherwise expects `AS`
1138    /// next), so enabling it shadows no existing spelling. On for DuckDB / Lenient (the
1139    /// permissive superset); off elsewhere, where the clause is a parse error.
1140    pub recursive_using_key: bool,
1141}
1142
1143/// Dialect-owned table-factor syntax accepted by the parser.
1144///
1145/// The `FROM`-item factor forms beyond a plain named table. Split out of
1146/// [`TableExpressionSyntax`] at its 16-field line as the table-factor axis, distinct from
1147/// the join and factor-tail/alias cores. Each flag is a grammar gate: when off the factor
1148/// keyword falls through to the named-table path or surfaces as a clean parse error.
1149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1150pub struct TableFactorSyntax {
1151    /// Accept `LATERAL` before derived tables and table functions.
1152    pub lateral: bool,
1153    /// Accept function calls as `FROM` items.
1154    pub table_functions: bool,
1155    /// Accept PostgreSQL `ROWS FROM (...)` table functions.
1156    pub rows_from: bool,
1157    /// Accept the first-class `UNNEST(<expr>[, <expr>…])` table factor
1158    /// ([`TableFactor::Unnest`](crate::ast::TableFactor)) — an array/collection
1159    /// expression expanded into a relation, with optional `WITH ORDINALITY`, a
1160    /// correlation alias, and (under [`unnest_with_offset`](TableFactorSyntax::unnest_with_offset))
1161    /// a BigQuery `WITH OFFSET` tail. On for PostgreSQL/DuckDB/Lenient (each admits
1162    /// `FROM unnest(…)`); off for ANSI/MySQL/SQLite, where `UNNEST(` is left to the
1163    /// named-table path and — with those presets' [`table_functions`](TableFactorSyntax::table_functions)
1164    /// also off — surfaces as the same clean parse error as any other function-in-FROM.
1165    /// Distinct from [`table_functions`](TableFactorSyntax::table_functions): a dialect can admit
1166    /// generic table functions yet route `UNNEST` to this dedicated node.
1167    pub unnest: bool,
1168    /// Accept the BigQuery/ZetaSQL `WITH OFFSET [AS <alias>]` tail on an
1169    /// [`unnest`](TableFactorSyntax::unnest) table factor — a 0-based ordinal column, the BigQuery
1170    /// counterpart of PostgreSQL's `WITH ORDINALITY`; the dependency is
1171    /// [`FeatureDependencyViolation::UnnestWithOffsetWithoutUnnest`]. On for the BigQuery
1172    /// preset alone — the first shipped dialect to enable it; off for every oracle-compared
1173    /// preset (PostgreSQL and DuckDB both parse-*reject* `WITH OFFSET`, engine-probed) and
1174    /// off for Lenient, and BigQuery carries no differential oracle. When off, a
1175    /// trailing `WITH OFFSET` is left unconsumed and the statement rejects.
1176    pub unnest_with_offset: bool,
1177    /// Accept a `WITH ORDINALITY` tail on a table-valued `FROM` source — the generic
1178    /// function factor, an [`unnest`](TableFactorSyntax::unnest) factor, and a
1179    /// [`rows_from`](TableFactorSyntax::rows_from) factor — adding a trailing ordinal column. On for
1180    /// PostgreSQL/DuckDB/Lenient; off for SQLite, whose grammar admits generic
1181    /// [`table_functions`](TableFactorSyntax::table_functions) but syntax-rejects `WITH ORDINALITY`
1182    /// (engine-probed: `FROM pragma_table_info('t') WITH ORDINALITY` errors at
1183    /// `ORDINALITY`). When off, the trailing `WITH ORDINALITY` is left unconsumed and the
1184    /// statement rejects. Distinct from [`unnest_with_offset`](TableFactorSyntax::unnest_with_offset),
1185    /// the BigQuery `WITH OFFSET` counterpart.
1186    pub table_function_ordinality: bool,
1187    /// Accept a bare SQL special value function (`current_date`, `current_timestamp`,
1188    /// `current_user`, …) as a `FROM` table source — PostgreSQL's `func_table`
1189    /// promotion of a `SQLValueFunction` ([`TableFactor::SpecialFunction`](crate::ast::TableFactor)),
1190    /// e.g. `SELECT * FROM current_date`. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient.
1191    /// MySQL has no such promotion — `current_date`/`current_timestamp` are reserved and a
1192    /// bare one in table position is an `ER_PARSE_ERROR` on mysql:8 — so it is off there,
1193    /// and the special-function keyword then falls through to the named-table path where the
1194    /// reserved-word gate rejects it (exactly as the alias position already does).
1195    pub special_function_table_source: bool,
1196    /// Accept DuckDB's `PIVOT` operator ([`Pivot`](crate::ast::Pivot)) in both of its
1197    /// surfaces: the `<source> PIVOT (<aggs> FOR <col> IN (<vals>))` table factor and
1198    /// the leading-keyword `PIVOT <source> ON … USING …` statement. One flag gates both
1199    /// grammar points because they are a single dialect unit — DuckDB always admits the
1200    /// statement wherever it admits the table factor (the [`straight_join`](JoinSyntax::straight_join)
1201    /// precedent). The flag alone is not enough on a bare table factor: `PIVOT` must
1202    /// also be in [`FeatureSet::reserved_bare_alias`] or the source's alias swallows it
1203    /// first (the DuckDb preset reserves it, class `reserved` like `QUALIFY`). On for
1204    /// DuckDb / Lenient, off elsewhere.
1205    pub pivot: bool,
1206    /// Accept DuckDB's `UNPIVOT` operator ([`Unpivot`](crate::ast::Unpivot)) in both its
1207    /// table-factor and leading-keyword-statement surfaces — the [`pivot`](TableFactorSyntax::pivot)
1208    /// counterpart, kept a separate flag because `PIVOT` and `UNPIVOT` are distinct
1209    /// operators (the [`asof_join`](JoinSyntax::asof_join)/[`positional_join`](JoinSyntax::positional_join)
1210    /// precedent). The same reserved-word interaction applies. On for DuckDb / Lenient,
1211    /// off elsewhere.
1212    pub unpivot: bool,
1213    /// Accept DuckDB's `DESCRIBE`/`SHOW`/`SUMMARIZE` utility as a parenthesized `FROM`
1214    /// table source — DuckDB's `SHOW_REF` table reference
1215    /// ([`TableFactor::ShowRef`](crate::ast::TableFactor)), e.g.
1216    /// `FROM (DESCRIBE SELECT …)`, `FROM (SHOW databases)`. One flag gates all three
1217    /// keywords because DuckDB models them as a single `SHOW_REF` production (the
1218    /// paired-flag doctrine). When off, the leading keyword is left to the query/join
1219    /// grammar inside the parentheses and the construct is a clean parse divergence
1220    /// (`FROM (DESCRIBE …)` reads `DESCRIBE` as neither a query start nor a joined
1221    /// table). Only the table-source position is admitted — DuckDB parse-rejects these
1222    /// at CTE-body position — so, unlike [`pivot`](TableFactorSyntax::pivot), there is no query-body
1223    /// or leading-keyword-statement surface. On for DuckDb / Lenient, off elsewhere.
1224    pub show_ref: bool,
1225    /// Accept DuckDB's bare `FROM VALUES (<row>, …) AS <alias>` row-list table factor —
1226    /// a `VALUES` constructor standing directly as a table factor *without* the wrapping
1227    /// parentheses the standard derived table requires
1228    /// ([`TableFactor::Derived`](crate::ast::TableFactor) tagged
1229    /// [`DerivedSpelling::BareValues`](crate::ast::DerivedSpelling)). DuckDB
1230    /// parse-requires a table alias here (a bare `FROM VALUES (1)` is a syntax error;
1231    /// `FROM VALUES (1) t` accepts — probed on 1.5.4), so the parser rejects a missing
1232    /// one. When off, `VALUES` in table-factor position is not a table name and the
1233    /// construct is a clean parse divergence (the reject the other dialects give). The
1234    /// parenthesized `FROM (VALUES …)` derived table is separate and always accepted. On
1235    /// for DuckDb / Lenient, off elsewhere.
1236    pub from_values: bool,
1237    /// Accept the SQL/JSON `JSON_TABLE(context, path [AS name] [PASSING …] COLUMNS (…)
1238    /// [… ON ERROR])` table factor ([`TableFactor::JsonTable`](crate::ast::TableFactor)). On
1239    /// for PostgreSQL/Lenient. Off elsewhere: DuckDB parse-rejects it, and MySQL's `JSON_TABLE`
1240    /// has a *different* grammar (kept off so this PG-shaped node never fires for it). When
1241    /// off, `JSON_TABLE(` falls to the ordinary function/name path; reached only when the
1242    /// keyword is immediately followed by `(`.
1243    pub json_table: bool,
1244    /// Accept the SQL/XML `XMLTABLE([XMLNAMESPACES(…),] row PASSING doc COLUMNS …)` table
1245    /// factor ([`TableFactor::XmlTable`](crate::ast::TableFactor)). On for PostgreSQL/Lenient,
1246    /// off elsewhere (DuckDB parse-rejects it; MySQL/SQLite/ANSI have no such form). When off,
1247    /// `XMLTABLE(` falls to the ordinary function/name path; reached only when the keyword is
1248    /// immediately followed by `(`.
1249    pub xml_table: bool,
1250    /// Accept `TABLE(<expr>)` as a first-class `FROM` table factor
1251    /// ([`TableFactor::TableExpr`](crate::ast::TableFactor)) — sqlparser-rs's
1252    /// `TableFunction`. Distinct from [`table_functions`](TableFactorSyntax::table_functions) (a
1253    /// *named* table function, `FROM f(1)`) and from the standalone `TABLE t` query
1254    /// form, which is not a `FROM`-position factor at all. Snowflake and Oracle are the
1255    /// engines that document this shape, but neither ships a differential oracle here,
1256    /// so over-acceptance is unmeasurable — the conservative-preset family rule keeps
1257    /// this off everywhere but Lenient (the permissive superset), with no oracle-backed
1258    /// preset enabling it. When off, `TABLE(` in table-factor position falls through to
1259    /// the named-table path, where the reserved `TABLE` keyword is not an admissible
1260    /// relation name and the construct is a clean parse error.
1261    pub table_expr_factor: bool,
1262    /// Accept the SQL-standard PIVOT table factor's extended value sources and default —
1263    /// the Snowflake/BigQuery/Oracle grammar layered on the shared
1264    /// `<source> PIVOT (<aggs> FOR <col> IN (…))` shape: the `IN (ANY [ORDER BY …])`
1265    /// wildcard and `IN (<subquery>)` value sources
1266    /// ([`PivotValueSource`](crate::ast::PivotValueSource)) and the Snowflake
1267    /// `DEFAULT ON NULL (<expr>)` tail ([`Pivot::default_on_null`](crate::ast::Pivot)).
1268    /// Also the reachability gate for the standard single-`FOR`-column table-factor
1269    /// PIVOT itself where the DuckDB [`pivot`](Self::pivot) flag is off — so with `pivot`
1270    /// off and this on the parser reads the table-factor PIVOT but *not* the DuckDB
1271    /// leading-keyword statement / query-body / `IN <enum>` forms, and stops the
1272    /// bare-chained multi-`FOR`-column list at one head (the standard admits exactly one).
1273    /// On for BigQuery / Snowflake / Lenient — none oracle-compared here, so
1274    /// over-acceptance is unmeasurable and the conservative-preset family rule keeps it
1275    /// off elsewhere. Like [`pivot`](Self::pivot), a *bare*-factor standard PIVOT is
1276    /// reachable only where `PIVOT` is in [`FeatureSet::reserved_bare_alias`]; where it is
1277    /// not (BigQuery/Snowflake today), the suffix fires after an explicit alias — the
1278    /// `pivot`/`ASOF` reservation-dependency precedent.
1279    ///
1280    /// It doubles as the reachability gate for the standard `UNPIVOT` table factor where
1281    /// the DuckDB [`unpivot`](Self::unpivot) flag is off: PIVOT and UNPIVOT co-travel in
1282    /// these engines' grammars (every dialect with the standard PIVOT table factor also
1283    /// has UNPIVOT), so one flag reaches both rather than a redundant sibling that no
1284    /// dialect would ever toggle apart. The `UNPIVOT` table factor grammar is fully
1285    /// shared — the `INCLUDE`/`EXCLUDE NULLS` marker, per-column aliases, and multi-column
1286    /// value/name lists are all DuckDB fields BigQuery/Snowflake reuse — so this gate adds
1287    /// no new UNPIVOT syntax, only reachability; the same explicit-alias reachability note
1288    /// applies to a bare-factor standard UNPIVOT.
1289    pub pivot_value_sources: bool,
1290    /// Accept the SQL:2016 `<source> MATCH_RECOGNIZE (…)` row-pattern-recognition table
1291    /// factor ([`MatchRecognize`](crate::ast::MatchRecognize)) — the Snowflake / Oracle
1292    /// row-pattern-matching clause, with its `PARTITION BY` / `ORDER BY` / `MEASURES` /
1293    /// `ONE|ALL ROWS PER MATCH` / `AFTER MATCH SKIP` / `PATTERN (…)` / `SUBSET` / `DEFINE`
1294    /// subclauses. On for Snowflake (documented; no differential oracle, so
1295    /// over-acceptance is unmeasurable and the conservative-preset family rule keeps it
1296    /// off elsewhere) and Lenient (the permissive superset). Oracle is not a shipped
1297    /// preset, so it does not enable this. Like [`pivot`](Self::pivot), a *bare*-factor
1298    /// `MATCH_RECOGNIZE` is reachable only where the keyword is in
1299    /// [`FeatureSet::reserved_bare_alias`]; where it is not, the suffix fires after an
1300    /// explicit alias — the `pivot`/`ASOF` reservation-dependency precedent. When off,
1301    /// `MATCH_RECOGNIZE` falls through to the named-table/alias path and the construct is
1302    /// a clean parse divergence.
1303    pub match_recognize: bool,
1304    /// Accept SQL Server's `OPENJSON(<json> [, <path>]) [WITH (<col> <type> [<path>] [AS JSON],
1305    /// …)]` rowset-function table factor ([`OpenJson`](crate::ast::OpenJson)) — a JSON document
1306    /// parsed into a relation, either with the default `key`/`value`/`type` schema (no `WITH`)
1307    /// or an explicit column schema. On for MSSQL (documented — SQL Server is the sole engine
1308    /// with this exact form; no differential oracle ships, so over-acceptance is unmeasurable
1309    /// and the conservative-preset family rule keeps it off elsewhere) and Lenient (the
1310    /// permissive superset). `OPENJSON` is unreserved (a rowset function name), so this fires
1311    /// only when the keyword is immediately followed by `(`; a bare `OPENJSON` stays an ordinary
1312    /// relation name. When off, `OPENJSON(` falls to the ordinary function/name path, which
1313    /// rejects at the `WITH (…)` clause tail — the [`json_table`](Self::json_table) precedent.
1314    ///
1315    /// Docs: <https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql>.
1316    pub open_json: bool,
1317}
1318
1319/// Dialect-owned mutation-statement (`INSERT`/`UPDATE`/`DELETE`) syntax extensions.
1320///
1321/// These cover the non-ANSI tails the standard expresses differently (the standard
1322/// upsert is `MERGE`, and it has no `RETURNING`): they are explicit dialect data
1323/// so a parser gate never type-checks the dialect. Each flag is purely a
1324/// grammar gate — when off, the keyword is left unconsumed and the trailing clause
1325/// surfaces as a parse error, which is how ANSI rejects PostgreSQL upserts.
1326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1327pub struct MutationSyntax {
1328    /// Accept a `RETURNING <output> [, ...]` clause on `INSERT`/`UPDATE`/`DELETE`
1329    /// (PostgreSQL; also Oracle/MariaDB/SQLite).
1330    pub returning: bool,
1331    /// Accept an `INSERT ... ON CONFLICT [<target>] DO {NOTHING | UPDATE ...}`
1332    /// upsert clause (PostgreSQL; also SQLite).
1333    pub on_conflict: bool,
1334    /// Accept an `INSERT ... ON DUPLICATE KEY UPDATE <col> = <expr> [, ...]` upsert
1335    /// clause (MySQL/MariaDB). MySQL infers the conflicting unique key, so there is
1336    /// no arbiter and no `DO NOTHING`; this also admits the `VALUES(<col>)` reference
1337    /// to a column's proposed insert value inside those update expressions.
1338    pub on_duplicate_key_update: bool,
1339    /// Accept `UPDATE ... SET ( col, ... ) = <source>` multiple-column assignment
1340    /// (PostgreSQL; SQL feature T641). Also reached through `ON CONFLICT DO UPDATE`.
1341    pub multi_column_assignment: bool,
1342    /// Reject explicit value-row RHS tuple assignments whose element count differs from
1343    /// the target column count (`UPDATE t SET (a, b) = (1)`). This is a parse-time
1344    /// DuckDB arity check; PostgreSQL parses the same surface and leaves arity to later
1345    /// analysis, so the behavior is intentionally split from [`multi_column_assignment`](Self::multi_column_assignment).
1346    pub update_tuple_value_row_arity: bool,
1347    /// Accept `WHERE CURRENT OF <cursor>` positioned `UPDATE`/`DELETE`
1348    /// (PostgreSQL; SQL feature F831).
1349    pub where_current_of: bool,
1350    /// Accept the `MERGE INTO <target> USING <source> ON <cond> WHEN [NOT] MATCHED ...`
1351    /// statement (SQL:2003 feature F312, the standard upsert; PostgreSQL 15+). Unlike
1352    /// the other flags here this gates a *leading* statement keyword, not a trailing
1353    /// clause: when off, `MERGE` is not dispatched and surfaces as an unknown
1354    /// statement, which is how MySQL (no `MERGE`) rejects it.
1355    pub merge: bool,
1356    /// Accept the MySQL `REPLACE [INTO] <table> ...` delete-then-insert statement.
1357    /// Like `merge` this gates a *leading* statement keyword: when off (ANSI /
1358    /// PostgreSQL), `REPLACE` is not dispatched and surfaces as an unknown statement.
1359    /// `REPLACE` reuses the `INSERT` tail grammar tagged
1360    /// [`InsertVerb::Replace`](crate::ast::InsertVerb), so it needs no node of its own.
1361    pub replace_into: bool,
1362    /// Accept the MySQL `INSERT`/`REPLACE ... SET <col> = <value> [, ...]`
1363    /// assignment-list source ([`InsertSource::Set`](crate::ast::InsertSource)). When
1364    /// off (ANSI / PostgreSQL), `SET` after the target is left unconsumed and surfaces
1365    /// as a parse error, the same reject mechanism the other trailing-clause gates use.
1366    pub insert_set: bool,
1367    /// Accept the MySQL single-table `UPDATE`/`DELETE ... [ORDER BY <keys>] [LIMIT
1368    /// <count>]` row-limiting tails. One flag gates both clauses on both statements
1369    /// because they are a single dialect unit — MySQL admits the `ORDER BY` only
1370    /// alongside the `LIMIT` it orders, and on `UPDATE` exactly as on `DELETE`
1371    /// (mirroring how `table_options` gates the trailing options and column attribute
1372    /// together). The multi-table `UPDATE`/`DELETE` forms take no such tail, so this is
1373    /// the single-table grammar only. Off in ANSI/PostgreSQL, which have neither tail:
1374    /// there the trailing `ORDER BY`/`LIMIT` is left unconsumed and surfaces as a clean
1375    /// parse error.
1376    pub update_delete_tails: bool,
1377    /// Accept the SQLite `INSERT OR <action>` / `UPDATE OR <action>` conflict-resolution
1378    /// prefix on the mutation verb, where `<action>` is `REPLACE`/`IGNORE`/`ABORT`/`FAIL`/
1379    /// `ROLLBACK` ([`ConflictResolution`](crate::ast::ConflictResolution), the [`Insert::or_action`](crate::ast::Insert)
1380    /// / [`Update::or_action`](crate::ast::Update) slot). SQLite only. Distinct from the
1381    /// MySQL `INSERT IGNORE` surface (a bare post-verb `IGNORE`, no `OR`) — that is not
1382    /// absorbed here. Off in ANSI/PostgreSQL/MySQL, where the `OR` after the verb is left
1383    /// unconsumed and surfaces as a clean parse error.
1384    pub or_conflict_action: bool,
1385    /// Accept DuckDB `INSERT INTO t BY NAME|BY POSITION …` column-matching mode
1386    /// between the target and the source. When off, `BY` is left unconsumed.
1387    pub insert_column_matching: bool,
1388    /// Accept the `DELETE FROM <target> USING <from-list> …` multi-relation delete
1389    /// (PostgreSQL; also MySQL's multi-table delete). On for
1390    /// ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no `USING` on `DELETE`
1391    /// (engine-measured-rejected via rusqlite), so it is off; the `USING` keyword is then
1392    /// left unconsumed and the trailing relation list surfaces as a clean parse error.
1393    pub delete_using: bool,
1394    /// Accept a PostgreSQL/SQLite `UPDATE … SET … FROM <table refs>` additional-relations
1395    /// clause. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL has no `UPDATE … FROM` —
1396    /// its multi-table update lists every table in the target position
1397    /// (`UPDATE t1, t2 SET …`) — so `UPDATE t SET … FROM u` is an `ER_PARSE_ERROR` on
1398    /// mysql:8; with the flag off the `FROM` keyword is left unconsumed and surfaces as a
1399    /// clean parse error.
1400    pub update_from: bool,
1401    /// Accept an alias on the *target* of a `DELETE FROM <target> USING <table_refs>`
1402    /// multi-table delete (`DELETE FROM t AS e USING u …`). On for ANSI/PostgreSQL/
1403    /// DuckDB/Lenient. MySQL's `DELETE FROM tbl … USING …` names the delete target(s) as
1404    /// bare table names that must match the `USING` relations, so an alias there is an
1405    /// `ER_PARSE_ERROR` on mysql:8 (`DELETE FROM event AS e USING sales …` rejects, while a
1406    /// single-table `DELETE FROM event AS e WHERE …` — no `USING` — parses); it is off for
1407    /// MySQL. Only the `USING`-form target is governed — the plain single-table delete's
1408    /// alias is unaffected. Independent of [`delete_using`](Self::delete_using), which gates
1409    /// whether the `USING` clause is admitted at all.
1410    pub delete_using_target_alias: bool,
1411    /// Accept a leading `WITH` CTE clause before an `INSERT` statement
1412    /// (`WITH a AS (…) INSERT INTO t …`). On for ANSI/PostgreSQL/DuckDB/Lenient. MySQL
1413    /// admits a statement-leading `WITH` before `SELECT`/`UPDATE`/`DELETE` but *not* before
1414    /// `INSERT` (its CTE for an insert rides the `INSERT … SELECT` source instead:
1415    /// `INSERT INTO t WITH … SELECT …`), so `WITH … INSERT …` is an `ER_PARSE_ERROR` on
1416    /// mysql:8; it is off for MySQL and the `INSERT` after the CTE list then surfaces as a
1417    /// clean parse error. The bare `INSERT` with no leading `WITH` is unaffected.
1418    pub cte_before_insert: bool,
1419    /// Accept a leading `WITH` CTE clause before a `MERGE` statement
1420    /// (`WITH a AS (…) MERGE INTO t USING a …`; PostgreSQL 15+, DuckDB — probed on
1421    /// 1.5.4). Off for ANSI: SQL:2016's `<merge statement>` takes no `<with clause>`
1422    /// (unlike an `INSERT`, whose source query carries one), so a leading `WITH`
1423    /// before `MERGE` is a dialect extension, not standard surface. The `MERGE`
1424    /// counterpart of [`cte_before_insert`](Self::cte_before_insert): when off, the
1425    /// `MERGE` after the CTE list surfaces as a clean parse error, and the bare
1426    /// `MERGE` (no leading `WITH`) is unaffected. Only reachable where
1427    /// [`merge`](Self::merge) dispatches `MERGE` at all (the dependency is
1428    /// [`FeatureDependencyViolation::CteBeforeMergeWithoutMerge`]).
1429    pub cte_before_merge: bool,
1430    /// Accept a data-modifying statement — `INSERT`/`UPDATE`/`DELETE`/`MERGE`, the
1431    /// `MERGE` arm PG 17+ — as a CTE body
1432    /// (`WITH t AS (DELETE FROM x RETURNING *) SELECT * FROM t`;
1433    /// [`CteBody`](crate::ast::CteBody)). PostgreSQL admits the DML body at *every*
1434    /// `WITH` site during raw parsing — nested subquery/scalar-subquery `WITH`s,
1435    /// `DECLARE CURSOR`/`CREATE TABLE AS`/`CREATE VIEW`/`EXPLAIN`/`COPY (…) TO`
1436    /// bodies (all probed on pg_query 17; misuse is rejected at analysis, past the
1437    /// parse boundary this crate models) — so the gate governs the one shared CTE
1438    /// grammar uniformly rather than per placement. Off everywhere else: DuckDB
1439    /// parse-rejects a DML CTE body (`A CTE needs a SELECT`, probed on 1.5.4), MySQL
1440    /// is an `ER_PARSE_ERROR` (probed on mysql:8), SQLite rejects, and the SQL
1441    /// standard has no data-modifying `WITH`; there the DML keyword after `AS (` is
1442    /// not dispatched and surfaces as the ordinary query-body parse error.
1443    pub data_modifying_ctes: bool,
1444    /// Accept the `WHEN NOT MATCHED BY {SOURCE | TARGET}` `MERGE` arms (PostgreSQL 17+,
1445    /// DuckDB — both probed). `NOT MATCHED BY TARGET` is the bare `NOT MATCHED` (an
1446    /// unpaired source row → insert); `NOT MATCHED BY SOURCE` is the new arm firing on an
1447    /// unpaired *target* row (→ update/delete, like `MATCHED`). Off in ANSI: SQL:2016's
1448    /// `<merge when clause>` is only `MATCHED`/`NOT MATCHED`, so the `BY` after `NOT
1449    /// MATCHED` is not standard surface — with the flag off it is left unconsumed and the
1450    /// clause surfaces as a clean parse error. Only reachable where [`merge`](Self::merge)
1451    /// dispatches `MERGE` at all (the dependency is
1452    /// [`FeatureDependencyViolation::MergeWhenNotMatchedByWithoutMerge`]); the bare
1453    /// `WHEN NOT MATCHED` is unaffected.
1454    pub merge_when_not_matched_by: bool,
1455    /// Accept the `MERGE ... WHEN NOT MATCHED THEN INSERT DEFAULT VALUES` action — a
1456    /// column-default row taking neither a column list nor an `OVERRIDING` clause
1457    /// (PostgreSQL, DuckDB — both probed). Off in ANSI: SQL:2016's
1458    /// `<merge insert specification>` is `INSERT [cols] [override] VALUES (...)` with no
1459    /// `DEFAULT VALUES` alternative, so with the flag off the `DEFAULT` after `INSERT`
1460    /// surfaces as a clean parse error. Distinct from the top-level `INSERT ... DEFAULT
1461    /// VALUES` source, which every dialect admits. Only reachable where
1462    /// [`merge`](Self::merge) dispatches `MERGE` at all (the dependency is
1463    /// [`FeatureDependencyViolation::MergeInsertDefaultValuesWithoutMerge`]).
1464    pub merge_insert_default_values: bool,
1465    /// Accept the `MERGE ... WHEN NOT MATCHED THEN INSERT ... OVERRIDING {SYSTEM | USER}
1466    /// VALUE` identity override on the merge insert action (SQL:2016
1467    /// `<merge insert specification>`'s `<override clause>`; PostgreSQL). Unlike the
1468    /// other two merge extensions here this is *standard* surface, so it is on for ANSI —
1469    /// but DuckDB rejects `OVERRIDING` inside `MERGE` (`syntax error at or near
1470    /// "OVERRIDING"`, probed on 1.5.4) while accepting it on a top-level `INSERT`, so its
1471    /// preset splits from the shared top-level [`InsertOverriding`](crate::ast::InsertOverriding)
1472    /// grammar in exactly this knob. Off for DuckDB/MySQL; with the flag off the
1473    /// `OVERRIDING` between the column list and `VALUES` is left unconsumed and surfaces
1474    /// as a clean parse error. Only reachable where [`merge`](Self::merge) dispatches
1475    /// `MERGE` at all (the dependency is
1476    /// [`FeatureDependencyViolation::MergeInsertOverridingWithoutMerge`]).
1477    pub merge_insert_overriding: bool,
1478    /// Accept DuckDB `UPDATE SET *` in a MERGE WHEN MATCHED arm (column-wise copy
1479    /// from the source). Off in ANSI/PostgreSQL.
1480    pub merge_update_set_star: bool,
1481    /// Accept DuckDB `INSERT *` / `INSERT BY NAME [*]` merge insert spellings.
1482    /// Off in ANSI/PostgreSQL (only the standard column-list / VALUES form).
1483    pub merge_insert_star_by_name: bool,
1484    /// Accept DuckDB `THEN ERROR` as a MERGE action (runtime raises when the arm
1485    /// fires). Off in ANSI/PostgreSQL.
1486    pub merge_error_action: bool,
1487    /// Accept a multi-part (qualified) column name as an `UPDATE … SET` assignment target
1488    /// (`UPDATE t SET t.i = 1` / `schema.t.col = …`). On for ANSI/PostgreSQL/MySQL/SQLite/Lenient.
1489    /// DuckDB parse-rejects qualified SET targets ("Qualified column names in UPDATE .. SET
1490    /// not supported", probed on 1.5.4), so it is off there; with the flag off a target whose
1491    /// [`ObjectName`](crate::ast::ObjectName) has more than one part surfaces as a clean parse
1492    /// error. A bare single-part column remains free either way.
1493    pub update_set_qualified_column: bool,
1494}
1495
1496/// Dialect-owned whole-statement DDL dispatch gates accepted by the parser.
1497///
1498/// The leading-keyword gates for non-`TABLE` object DDL (the `CREATE`/`DROP` of the
1499/// various object kinds, plus the `OR REPLACE` view/function modifier). Split out of the
1500/// retired `SchemaChangeSyntax` at its 16-field line as the whole-statement-dispatch axis,
1501/// distinct from the table-clause, column-definition, and constraint axes. When off, the
1502/// leading keyword is not dispatched and surfaces as an unknown statement.
1503#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1504pub struct StatementDdlGates {
1505    /// Accept the SQLite `CREATE [TEMP] TRIGGER [IF NOT EXISTS] <name> <timing>
1506    /// <event> ON <table> [FOR EACH ROW] [WHEN <expr>] BEGIN <stmt>; … END` statement.
1507    /// Like the other flags on this axis it gates the *whole* statement (the leading
1508    /// `TRIGGER` after `CREATE`), not a decoration of an already-accepted family: only
1509    /// SQLite's SQL-statement body form is modelled, and PostgreSQL/MySQL spell an
1510    /// incompatible `EXECUTE FUNCTION`/external-routine body they reject for this form,
1511    /// so gating to SQLite (and Lenient) is behaviour-accurate. When off, `TRIGGER`
1512    /// falls through to the `CREATE TABLE` expectation and surfaces as an unknown
1513    /// statement.
1514    pub create_trigger: bool,
1515    /// Accept the DuckDB `CREATE [OR REPLACE] [TEMP] {MACRO | FUNCTION} <name>(<params>)
1516    /// AS <expr> | AS TABLE <query>` macro DDL. Like [`create_trigger`](StatementDdlGates::create_trigger)
1517    /// this gates the *whole* statement, not a decoration of an always-accepted family:
1518    /// DuckDB's `MACRO` keyword and its live-body `FUNCTION` are dispatched to the
1519    /// [`CreateMacro`](crate::ast::CreateMacro) node only under this flag. On for
1520    /// DuckDB/Lenient. Off elsewhere: `MACRO` then falls through to the `CREATE TABLE`
1521    /// expectation (an unknown statement), and `CREATE FUNCTION` keeps routing to the
1522    /// string-body routine parser gated by [`routines`](StatementDdlGates::routines) — the two
1523    /// grammars are disjoint (a live expr/query body vs an opaque source string), so a
1524    /// macro is never silently reinterpreted as a routine where the flag is off.
1525    ///
1526    /// This same flag also gates the matching `DROP MACRO [TABLE] <name>` object kinds
1527    /// ([`DropObjectKind::Macro`](crate::ast::DropObjectKind::Macro) /
1528    /// [`MacroTable`](crate::ast::DropObjectKind::MacroTable)) — a dialect with `CREATE MACRO`
1529    /// has `DROP MACRO`. Unlike the sibling one-flag-gates-both forms (`TYPE`/`SEQUENCE`),
1530    /// only the `MACRO` drop spelling is gated here: the `FUNCTION` synonym DuckDB accepts on
1531    /// `DROP` routes to the signature routine drop gated by [`routines`](StatementDdlGates::routines),
1532    /// not to a macro object kind.
1533    pub create_macro: bool,
1534    /// Accept DuckDB's secrets-management statements: `CREATE [PERSISTENT] SECRET <name>
1535    /// (<option> <value>, …)`, dispatched to the [`CreateSecret`](crate::ast::CreateSecret)
1536    /// node, and its drop counterpart `DROP [PERSISTENT | TEMPORARY] SECRET [IF EXISTS] <name>
1537    /// [FROM <storage>]`, dispatched to [`DropSecretStmt`](crate::ast::DropSecretStmt). One
1538    /// flag covers both because they are the same secrets behaviour surface (the drop is
1539    /// meaningless without the create). Like [`create_macro`](StatementDdlGates::create_macro)
1540    /// this gates the *whole* statement: on for DuckDB/Lenient, off elsewhere, where the
1541    /// `PERSISTENT`/`SECRET` keyword falls through to the `CREATE TABLE`/`DROP` object-kind
1542    /// expectation and surfaces as an unknown statement.
1543    pub create_secret: bool,
1544    /// Accept DuckDB's `CREATE [OR REPLACE] [TEMP] TYPE <name> AS ENUM(…)/STRUCT(…)/<alias>`
1545    /// user-defined-type DDL, dispatched to the [`CreateType`](crate::ast::CreateType) node,
1546    /// and the matching `DROP TYPE` object kind ([`DropObjectKind::Type`](crate::ast::DropObjectKind)).
1547    /// Like [`create_macro`](StatementDdlGates::create_macro) this gates the *whole* statement: on for
1548    /// DuckDB/Lenient, off elsewhere, where the `TYPE` keyword falls through to the
1549    /// `CREATE TABLE` expectation (an unknown statement) and `DROP TYPE` is an unexpected
1550    /// object kind. One flag gates both leading forms — a dialect with `CREATE TYPE` has
1551    /// `DROP TYPE`.
1552    pub create_type: bool,
1553    /// Accept the SQLite `CREATE VIRTUAL TABLE [IF NOT EXISTS] <name> USING <module>
1554    /// [(<args>)]` statement, dispatched to the
1555    /// [`CreateVirtualTable`](crate::ast::CreateVirtualTable) node. Like
1556    /// [`create_trigger`](StatementDdlGates::create_trigger) this gates the *whole* statement (the
1557    /// leading `VIRTUAL` after `CREATE`): only SQLite has virtual tables, and the
1558    /// module-owned argument list is meaningless elsewhere, so on for SQLite/Lenient and
1559    /// off everywhere else — where `VIRTUAL` falls through to the `CREATE TABLE`
1560    /// expectation and surfaces as an unknown statement.
1561    pub create_virtual_table: bool,
1562    /// Accept the `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] <name> [<option> ...]`
1563    /// sequence-generator statement (SQL:2003 T176; PostgreSQL/DuckDB), dispatched to the
1564    /// [`CreateSequence`](crate::ast::CreateSequence) node, and the matching `DROP SEQUENCE`
1565    /// object kind ([`DropObjectKind::Sequence`](crate::ast::DropObjectKind)). Like
1566    /// [`create_type`](StatementDdlGates::create_type) this gates the *whole* statement: on for
1567    /// PostgreSQL/DuckDB/Lenient, off elsewhere, where the `SEQUENCE` keyword falls through
1568    /// to the `CREATE TABLE` expectation (an unknown statement) and `DROP SEQUENCE` is an
1569    /// unexpected object kind. One flag gates both leading forms — a dialect with
1570    /// `CREATE SEQUENCE` has `DROP SEQUENCE`. The modelled tail is the shared standard option
1571    /// core both engines' parsers accept (`START [WITH]`, `INCREMENT [BY]`, `MIN`/`MAXVALUE`,
1572    /// `NO MIN`/`MAXVALUE`, `CYCLE`/`NO CYCLE`); PostgreSQL's `CACHE`/`AS`/`OWNED BY` extensions
1573    /// stay unmodelled so DuckDB (which rejects them) does not over-accept.
1574    pub create_sequence: bool,
1575    /// Accept the PostgreSQL extension-DDL statements `CREATE EXTENSION [IF NOT EXISTS]
1576    /// <name> [WITH] [SCHEMA s] [VERSION v] [CASCADE]` and `ALTER EXTENSION <name>
1577    /// {UPDATE [TO v] | ADD <member> | DROP <member>}`, dispatched to the
1578    /// [`CreateExtension`](crate::ast::CreateExtension) and
1579    /// [`AlterExtension`](crate::ast::AlterExtension) nodes. Like
1580    /// [`create_sequence`](StatementDdlGates::create_sequence) this gates the *whole*
1581    /// statement (the leading `EXTENSION` keyword after `CREATE`, and the `EXTENSION`
1582    /// dispatch after `ALTER`): PostgreSQL is the only shipped dialect with an extension
1583    /// catalogue, so on for PostgreSQL/Lenient and off everywhere else — where `EXTENSION`
1584    /// falls through to the `CREATE TABLE` expectation (an unknown statement) or the
1585    /// `ALTER TABLE` expectation. One flag gates both the `CREATE` and `ALTER` forms — a
1586    /// dialect with `CREATE EXTENSION` has `ALTER EXTENSION`.
1587    pub extension_ddl: bool,
1588    /// Accept the PostgreSQL `DROP TRANSFORM [IF EXISTS] FOR <type> LANGUAGE <lang>
1589    /// [CASCADE | RESTRICT]` statement, dispatched to the
1590    /// [`DropTransform`](crate::ast::DropTransform) node. Like
1591    /// [`extension_ddl`](StatementDdlGates::extension_ddl) this gates the *whole* statement
1592    /// (the `TRANSFORM` keyword after `DROP`): only PostgreSQL has the transform catalogue
1593    /// (`pg_transform` — a `(type, language)` conversion registered by `CREATE TRANSFORM`),
1594    /// so on for PostgreSQL/Lenient and off everywhere else — where `TRANSFORM` falls through
1595    /// to the `DROP` object-kind expectation and surfaces as a clean parse error.
1596    ///
1597    /// A *separate* behaviour from [`extension_ddl`](StatementDdlGates::extension_ddl),
1598    /// carrying its own gate rather than riding that one — the same split
1599    /// [`alter_system`](StatementDdlGates::alter_system) makes. A transform is procedural-
1600    /// language infrastructure (a type↔language conversion), not an extension-catalogue
1601    /// operation: unlike `ALTER … DEPENDS ON EXTENSION` (which names `EXTENSION` in its
1602    /// syntax and so rides `extension_ddl`), `DROP TRANSFORM` mutates a standalone
1603    /// `pg_transform` object with no extension in the grammar. It reuses the shared
1604    /// [`ObjectReference::Transform`](crate::ast::ObjectReference) axis for its `FOR type
1605    /// LANGUAGE lang` shape — the same axis `ALTER EXTENSION … ADD|DROP TRANSFORM` names a
1606    /// member with — but that shared *node* is not a shared *behaviour gate*.
1607    pub transform_ddl: bool,
1608    /// Accept the PostgreSQL `ALTER SYSTEM { SET <name> {= | TO} <value> | RESET <name> |
1609    /// RESET ALL }` server-configuration statement, dispatched to the
1610    /// [`AlterSystem`](crate::ast::AlterSystem) node. Like
1611    /// [`extension_ddl`](StatementDdlGates::extension_ddl) this gates the *whole* statement
1612    /// (the `SYSTEM` dispatch after `ALTER`): only PostgreSQL persists a server-wide
1613    /// configuration through SQL (`postgresql.auto.conf`), so on for PostgreSQL/Lenient and
1614    /// off everywhere else — where `SYSTEM` falls through to the `ALTER TABLE` expectation
1615    /// and surfaces as a clean parse error. A *separate* behaviour from
1616    /// [`extension_ddl`](StatementDdlGates::extension_ddl): `ALTER SYSTEM` is server
1617    /// configuration, unrelated to the extension catalogue, so it carries its own gate rather
1618    /// than riding that one. It reuses the session-`SET` value axis
1619    /// ([`SetValue`](crate::ast::SetValue) / [`ConfigParameter`](crate::ast::ConfigParameter))
1620    /// for the setting name/value grammar, but admits no `SESSION`/`LOCAL` scope and no
1621    /// `FROM CURRENT` — the wrapper is exactly PostgreSQL's `generic_set`/`generic_reset`.
1622    pub alter_system: bool,
1623    /// Accept MySQL's tablespace storage-DDL statements — `CREATE [UNDO] TABLESPACE <name> …`,
1624    /// `ALTER [UNDO] TABLESPACE <name> <action>`, and `DROP [UNDO] TABLESPACE <name> [<option>...]`
1625    /// — dispatched to the [`CreateTablespace`](crate::ast::CreateTablespace),
1626    /// [`AlterTablespace`](crate::ast::AlterTablespace), and
1627    /// [`DropTablespace`](crate::ast::DropTablespace) nodes. Like
1628    /// [`extension_ddl`](StatementDdlGates::extension_ddl) this gates the *whole* statement (the
1629    /// `TABLESPACE`/`UNDO` dispatch after `CREATE`/`ALTER`/`DROP`): only MySQL models an
1630    /// InnoDB/NDB tablespace catalogue, so on for MySQL/Lenient and off everywhere else — where
1631    /// the keyword falls through to the `CREATE`/`ALTER TABLE` expectation (an unknown statement)
1632    /// or the `DROP` object-kind expectation. One flag gates all three verbs and both the plain
1633    /// and `UNDO` variants — a dialect with `CREATE TABLESPACE` has the whole family. A separate
1634    /// behaviour from [`logfile_group_ddl`](StatementDdlGates::logfile_group_ddl): a tablespace and
1635    /// a logfile group are distinct storage objects with distinct leading keywords, so each
1636    /// carries its own gate rather than one bundling both.
1637    pub tablespace_ddl: bool,
1638    /// Accept MySQL's NDB logfile-group storage-DDL statements — `CREATE LOGFILE GROUP <name> ADD
1639    /// UNDOFILE '<f>' [<option>...]`, `ALTER LOGFILE GROUP <name> ADD UNDOFILE '<f>' [<option>...]`,
1640    /// and `DROP LOGFILE GROUP <name> [<option>...]` — dispatched to the
1641    /// [`CreateLogfileGroup`](crate::ast::CreateLogfileGroup),
1642    /// [`AlterLogfileGroup`](crate::ast::AlterLogfileGroup), and
1643    /// [`DropLogfileGroup`](crate::ast::DropLogfileGroup) nodes. Like
1644    /// [`tablespace_ddl`](StatementDdlGates::tablespace_ddl) this gates the *whole* statement (the
1645    /// `LOGFILE GROUP` dispatch after `CREATE`/`ALTER`/`DROP`): only MySQL (NDB) has a logfile-group
1646    /// catalogue, so on for MySQL/Lenient and off everywhere else — where `LOGFILE` falls through
1647    /// to the `CREATE`/`ALTER TABLE` expectation (an unknown statement) or the `DROP` object-kind
1648    /// expectation. One flag gates all three verbs — a dialect with `CREATE LOGFILE GROUP` has the
1649    /// whole family. A separate behaviour from
1650    /// [`tablespace_ddl`](StatementDdlGates::tablespace_ddl), for the reason documented there.
1651    pub logfile_group_ddl: bool,
1652    /// Accept the `CREATE SCHEMA …` and `DROP SCHEMA …` schema-object statements
1653    /// (SQL:2016 F771; PostgreSQL/MySQL). One flag gates both leading forms — a dialect
1654    /// with `CREATE SCHEMA` has `DROP SCHEMA`. On for ANSI/PostgreSQL/MySQL/DuckDB/
1655    /// Lenient. SQLite has no schema objects (a database *is* the schema namespace), so
1656    /// it is off; the `SCHEMA` keyword is then not dispatched and surfaces as an unknown
1657    /// statement.
1658    pub schemas: bool,
1659    /// Accept the SQL-standard embedded schema-element list on `CREATE SCHEMA`
1660    /// (`CREATE SCHEMA s CREATE TABLE t (...) CREATE VIEW ...`): the component objects
1661    /// created *inside* the new schema, parsed as children of the [`CreateSchema`](crate::ast::CreateSchema)
1662    /// node so the whole construct stays ONE statement. The admissible element set is
1663    /// closed and measured against PostgreSQL — `CREATE TABLE`/`VIEW`/`INDEX`/`SEQUENCE`/
1664    /// `TRIGGER` and `GRANT` — with `CREATE MATERIALIZED VIEW`/`FUNCTION`, a nested
1665    /// `CREATE SCHEMA`, and `DROP`/`ALTER`/`INSERT`/… all rejected as elements.
1666    ///
1667    /// On for PostgreSQL and Lenient (its superset). Off elsewhere: ANSI/MySQL/DuckDB
1668    /// accept the schema head but not the embedded form (MySQL/DuckDB reject the
1669    /// standard embedding; ANSI keeps a bare head), so a following `CREATE`/`GRANT`
1670    /// there is left to the top-level statement loop rather than consumed as an element.
1671    /// Narrower than [`schemas`](StatementDdlGates::schemas) on purpose — the head is
1672    /// widely accepted, the embedding is not.
1673    pub schema_elements: bool,
1674    /// Accept the `CREATE DATABASE …` statement (PostgreSQL/MySQL). On for
1675    /// ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no `CREATE DATABASE` (databases
1676    /// are files, reached via `ATTACH`), so it is off; the `DATABASE` keyword is then not
1677    /// dispatched and surfaces as an unknown statement.
1678    pub databases: bool,
1679    /// Accept MySQL's `DROP {DATABASE | SCHEMA} [IF EXISTS] <name>` — a single-database drop
1680    /// where `DATABASE` and `SCHEMA` are exact synonyms (the lexer folds them onto one
1681    /// grammar), naming exactly one unqualified database with no `CASCADE`/`RESTRICT` and no
1682    /// comma list (server-measured on mysql:8: `DROP DATABASE a, b`, `DROP DATABASE db.x`, and
1683    /// `DROP DATABASE a CASCADE` are each `ER_PARSE_ERROR`). Distinct from the shared
1684    /// name-list `DROP SCHEMA <name> [, …] [CASCADE | RESTRICT]` gated by
1685    /// [`schemas`](StatementDdlGates::schemas): where this flag is on, both the `DATABASE` and
1686    /// `SCHEMA` keywords are intercepted for the single-name form *before* the shared
1687    /// name-list path, so the two cannot both fire. On for MySQL only. Off elsewhere,
1688    /// including Lenient: because enabling it would recast `DROP SCHEMA` as the single-name
1689    /// form and forfeit the more permissive PostgreSQL/DuckDB name-list-plus-`CASCADE`
1690    /// `DROP SCHEMA` — a documented conflict resolution — Lenient keeps the name-list path and
1691    /// forgoes the MySQL `DROP DATABASE` spelling. With the flag off the `DATABASE` keyword is
1692    /// not dispatched as a drop and surfaces as an unknown drop object kind.
1693    pub drop_database: bool,
1694    /// Accept the `CREATE MATERIALIZED VIEW …` and `DROP MATERIALIZED VIEW …` statements
1695    /// (PostgreSQL; DuckDB). One flag gates both — a dialect with the create form has the
1696    /// drop form. On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no materialized
1697    /// views, so it is off; `MATERIALIZED` is then not dispatched and surfaces as an
1698    /// unknown statement (a plain `CREATE VIEW` is unaffected — a separate always-accepted
1699    /// family).
1700    pub materialized_views: bool,
1701    /// Accept the `TEMP`/`TEMPORARY` modifier on a plain `CREATE [OR REPLACE] VIEW`
1702    /// (PostgreSQL/SQLite/DuckDB spell session-local views). On for ANSI/PostgreSQL/SQLite/
1703    /// DuckDB/Lenient. MySQL has no temporary views (only temporary *tables*), so it is off;
1704    /// a consumed `TEMP`/`TEMPORARY` prefix leading into `VIEW` is then the syntax error
1705    /// MySQL reports (engine-measured-rejected on mysql:8). Gates only the view surface — the
1706    /// `CREATE TEMPORARY TABLE` form is a separate, unaffected family.
1707    pub temporary_views: bool,
1708    /// Accept the stored-routine DDL — `CREATE FUNCTION …`, `DROP FUNCTION …`, and
1709    /// `DROP PROCEDURE …` (PostgreSQL/MySQL SQL/PSM). One flag gates the routine family as
1710    /// a unit. On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no stored routines
1711    /// (its functions are C-registered, not SQL-declared), so it is off; the `FUNCTION`/
1712    /// `PROCEDURE` keyword is then not dispatched and surfaces as an unknown statement.
1713    pub routines: bool,
1714    /// Accept the `CREATE OR REPLACE …` object-replacement modifier on `VIEW`/`FUNCTION`
1715    /// (PostgreSQL; MySQL `OR REPLACE VIEW`). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient.
1716    /// SQLite has no `OR REPLACE` (it spells the intent `DROP` then `CREATE`), so it is
1717    /// off; the `OR` after `CREATE` is then left unconsumed and surfaces as a clean parse
1718    /// error.
1719    pub or_replace: bool,
1720    /// Accept the `RECURSIVE` keyword before `VIEW` in `CREATE [OR REPLACE]
1721    /// [TEMP|TEMPORARY] RECURSIVE VIEW <name> (<columns>) AS <query>` (DuckDB,
1722    /// engine-measured on duckdb 1.5.4). On for DuckDB/Lenient only — although
1723    /// PostgreSQL spells the same form, it is gated to the measured dialect per the
1724    /// no-shadowing doctrine rather than widened to the PostgreSQL reference without a
1725    /// differential. Off elsewhere, where the `RECURSIVE` keyword is left unconsumed
1726    /// before the expected `VIEW` and surfaces as a clean parse error. The keyword sits
1727    /// between the `TEMP`/`TEMPORARY` prefix and `VIEW`, never composes with
1728    /// `MATERIALIZED`, and requires the explicit column list (the engine desugars a
1729    /// recursive view to `WITH RECURSIVE`, which names its output columns).
1730    pub recursive_views: bool,
1731    /// Parse a routine/trigger/event body as a MySQL SQL/PSM *compound statement* — the
1732    /// `[<label>:] BEGIN [<declarations>] … END` block with its `DECLARE` prefix, the
1733    /// flow-control statements (`IF`/`CASE`/`LOOP`/`WHILE`/`REPEAT`/`LEAVE`/`ITERATE`/
1734    /// `RETURN`) and the cursor operations (`OPEN`/`FETCH`/`CLOSE`). This is a
1735    /// *body-context* behaviour, not a top-level statement gate: it governs the separate
1736    /// `parse_body_statement` dispatcher the routine/trigger wrappers invoke, never the
1737    /// top-level one (a bare top-level `BEGIN … END` stays transaction-start regardless).
1738    /// On for MySQL (and Lenient). Off elsewhere: PostgreSQL/ANSI spell a routine body as
1739    /// an opaque `$$…$$`/string routine definition (a different, unaffected grammar), and
1740    /// SQLite has no stored programs — so where it is off the body dispatcher rejects the
1741    /// compound grammar and the existing string/opaque body paths are untouched.
1742    pub compound_statements: bool,
1743    /// Accept DuckDB's `ALTER DATABASE [IF EXISTS] <name> SET ALIAS TO <alias>` statement
1744    /// (`AlterDatabaseStmt`), dispatched to the [`AlterDatabase`](crate::ast::AlterDatabase)
1745    /// node. Like [`alter_system`](StatementDdlGates::alter_system) this gates the *whole*
1746    /// statement (the `DATABASE` dispatch after `ALTER`): DuckDB's sole `ALTER DATABASE` form
1747    /// re-aliases an attached database. On for DuckDB/Lenient, off elsewhere — where
1748    /// `DATABASE` falls through to the `ALTER TABLE` expectation and surfaces as a clean parse
1749    /// error. Named for the object it alters, not the DuckDB-specific `SET ALIAS TO` spelling.
1750    /// MySQL's `ALTER DATABASE` (the charset/collation/encryption/read-only option list) is a
1751    /// *disjoint* behaviour on its own
1752    /// [`alter_database_options`](StatementDdlGates::alter_database_options) gate: the two
1753    /// grammars share only the `ALTER DATABASE` head and cannot be unioned under one gate (DuckDB
1754    /// rejects MySQL's options and vice versa), so each is its own flag and node per the MECE
1755    /// doctrine. Although PostgreSQL also has an `ALTER DATABASE` grammar, this stays gated to the
1756    /// measured dialect (DuckDB) per the no-shadowing doctrine rather than widened to the
1757    /// PostgreSQL reference without a differential.
1758    pub alter_database: bool,
1759    /// Accept MySQL's `ALTER {DATABASE | SCHEMA} [<name>] <option> …` schema-option change
1760    /// (`alter_database_stmt`), dispatched to the
1761    /// [`AlterDatabaseOptions`](crate::ast::AlterDatabaseOptions) node. A *disjoint* behaviour
1762    /// from DuckDB's [`alter_database`](StatementDdlGates::alter_database) `SET ALIAS`
1763    /// relocation: the two share only the `ALTER DATABASE` head, but MySQL adds the `SCHEMA`
1764    /// synonym and an optional name and takes a non-empty, repeatable list of charset/collation/
1765    /// encryption/read-only options — so it is its own gate and its own node rather than a union
1766    /// (which would make each dialect over-accept the other's grammar). On for MySQL/Lenient, off
1767    /// elsewhere — where `DATABASE`/`SCHEMA` falls through to the `ALTER TABLE` expectation and
1768    /// surfaces as a clean parse error (the DuckDB `SET ALIAS` head is intercepted first where
1769    /// its gate is on, and the two are disambiguated by lookahead under Lenient, which enables
1770    /// both).
1771    pub alter_database_options: bool,
1772    /// Accept MySQL's federated-server DDL — `CREATE SERVER <name> FOREIGN DATA WRAPPER
1773    /// <wrapper> OPTIONS ( … )`, `ALTER SERVER <name> OPTIONS ( … )`, and `DROP SERVER
1774    /// [IF EXISTS] <name>` — dispatched to the [`CreateServer`](crate::ast::CreateServer),
1775    /// [`AlterServer`](crate::ast::AlterServer), and [`DropServer`](crate::ast::DropServer)
1776    /// nodes. One flag gates all three leading dispatches because they are one cohesive
1777    /// server-object behaviour: `CREATE`/`ALTER` share the
1778    /// [`ServerOption`](crate::ast::ServerOption) axis (the `server_options_list` grammar) and
1779    /// `DROP` disposes of the same object — the `extension_ddl` (CREATE + ALTER extension) and
1780    /// `view_definition_options` (CREATE VIEW prefix + ALTER VIEW) one-object-one-gate precedent.
1781    /// On for MySQL/Lenient, off elsewhere, where the `SERVER` head falls through and surfaces as
1782    /// a clean parse error.
1783    pub server_definition: bool,
1784    /// Accept MySQL's `ALTER INSTANCE <action>` server-instance administration statement
1785    /// (`alter_instance_stmt`), dispatched to the [`AlterInstance`](crate::ast::AlterInstance)
1786    /// node. A whole-statement gate like [`alter_system`](StatementDdlGates::alter_system) — the
1787    /// `INSTANCE` dispatch after `ALTER`: a single instance-wide maintenance action (rotate a
1788    /// master key, reload TLS/the keyring, toggle the InnoDB redo log). A *separate* behaviour
1789    /// from the server and database DDL: it names no object and touches the running instance, not
1790    /// a catalogue object. On for MySQL/Lenient, off elsewhere, where `INSTANCE` surfaces as the
1791    /// "expected TABLE" parse error.
1792    pub alter_instance: bool,
1793    /// Accept MySQL's spatial-reference-system DDL — `CREATE [OR REPLACE] SPATIAL REFERENCE
1794    /// SYSTEM [IF NOT EXISTS] <srid> <attributes>` and `DROP SPATIAL REFERENCE SYSTEM
1795    /// [IF EXISTS] <srid>` — dispatched to the
1796    /// [`CreateSpatialReferenceSystem`](crate::ast::CreateSpatialReferenceSystem) and
1797    /// [`DropSpatialReferenceSystem`](crate::ast::DropSpatialReferenceSystem) nodes. One flag
1798    /// gates both dispatches because they are one catalogue-object behaviour (the
1799    /// [`server_definition`](StatementDdlGates::server_definition) one-object-one-gate
1800    /// precedent). On for MySQL/Lenient, off elsewhere — where the `SPATIAL` head falls through
1801    /// to the `TABLE` expectation and surfaces as a clean parse error.
1802    pub spatial_reference_system: bool,
1803    /// Accept MySQL's resource-group DDL — `CREATE RESOURCE GROUP <name> TYPE [=] {SYSTEM |
1804    /// USER} [VCPU …] [THREAD_PRIORITY …] [ENABLE | DISABLE]`, `ALTER RESOURCE GROUP <name>
1805    /// [VCPU …] [THREAD_PRIORITY …] [ENABLE | DISABLE] [FORCE]`, `DROP RESOURCE GROUP <name>
1806    /// [FORCE]`, and the session-statement `SET RESOURCE GROUP <name> [FOR <thread_ids>]` —
1807    /// dispatched to the [`CreateResourceGroup`](crate::ast::CreateResourceGroup) /
1808    /// [`AlterResourceGroup`](crate::ast::AlterResourceGroup) /
1809    /// [`DropResourceGroup`](crate::ast::DropResourceGroup) statement nodes and the
1810    /// [`SetResourceGroup`](crate::ast::SessionStatement::SetResourceGroup) session variant. One
1811    /// flag gates all four because they are one resource-group behaviour sharing the
1812    /// `VCPU`/`THREAD_PRIORITY`/`ENABLE|DISABLE` axes (the
1813    /// [`server_definition`](StatementDdlGates::server_definition) precedent); the `SET` member
1814    /// rides the same flag from inside the `SET`-statement dispatch, claimed off the
1815    /// `RESOURCE GROUP` two-word lookahead before the variable-assignment fallback. On for
1816    /// MySQL/Lenient, off elsewhere — where `RESOURCE` falls through and surfaces as a clean
1817    /// parse error (and `SET RESOURCE GROUP g` stays a variable-assignment parse error).
1818    pub resource_group: bool,
1819    /// Accept DuckDB's `ALTER SEQUENCE [IF EXISTS] <name> <option>...` statement
1820    /// (`AlterSeqStmt`), dispatched to the [`AlterSequence`](crate::ast::AlterSequence) node.
1821    /// Like [`alter_database`](StatementDdlGates::alter_database) this gates the *whole*
1822    /// statement (the `SEQUENCE` dispatch after `ALTER`): the option-list form changes a
1823    /// sequence generator's options, reusing the shared
1824    /// [`IdentityOption`](crate::ast::IdentityOption) axis for the core `CREATE SEQUENCE` also
1825    /// accepts. On for DuckDB/Lenient, off elsewhere — where `SEQUENCE` falls through to the
1826    /// `ALTER TABLE` expectation and surfaces as a clean parse error. A *separate* behaviour
1827    /// from [`alter_object_set_schema`](StatementDdlGates::alter_object_set_schema): `ALTER
1828    /// SEQUENCE … SET SCHEMA` is a schema relocation (that gate), not a sequence-option change.
1829    /// Although PostgreSQL also has `ALTER SEQUENCE`, this stays gated to DuckDB per the
1830    /// no-shadowing doctrine.
1831    pub alter_sequence: bool,
1832    /// Accept DuckDB's `ALTER {TABLE | VIEW | SEQUENCE} [IF EXISTS] <name> SET SCHEMA
1833    /// <schema>` statement (`AlterObjectSchemaStmt`), dispatched to the
1834    /// [`AlterObjectSchema`](crate::ast::AlterObjectSchema) node. Like
1835    /// [`alter_database`](StatementDdlGates::alter_database) this gates the *whole* statement
1836    /// (the `SET SCHEMA` tail after the object head): it relocates a relocatable object to
1837    /// another schema. DuckDB 1.5.4's binder rejects this as `Not implemented`, but the
1838    /// production is parse-reachable and PARSE-level parity is the modelled bar (analogous to
1839    /// PostgreSQL's grammar-present, engine-unimplemented `CREATE ASSERTION`). On for
1840    /// DuckDB/Lenient, off elsewhere — where a `SET SCHEMA` tail is left to the `ALTER TABLE`
1841    /// command parser (a clean parse error for the `VIEW`/`SEQUENCE`/`DATABASE` heads). Gated
1842    /// to DuckDB per the no-shadowing doctrine, though PostgreSQL shares the grammar.
1843    pub alter_object_set_schema: bool,
1844    /// Accept MySQL's view definition-option surface: the `[ALGORITHM = {UNDEFINED | MERGE |
1845    /// TEMPTABLE}] [DEFINER = <user>] [SQL SECURITY {DEFINER | INVOKER}]` prefix (before the
1846    /// `VIEW` keyword) on `CREATE VIEW`, and the whole `ALTER VIEW` redefinition statement,
1847    /// dispatched to the [`AlterView`](crate::ast::AlterView) node. One flag gates both because
1848    /// they are the one MySQL view-definition behaviour — the identical [`ViewOptions`](crate::ast::ViewOptions)
1849    /// prefix decorates `CREATE VIEW` and heads `ALTER VIEW`, and no dialect has one without the
1850    /// other. On for MySQL/Lenient. Off elsewhere: the option keywords before `VIEW` are left
1851    /// unconsumed (a clean parse error), and `ALTER VIEW` routes only to the DuckDB
1852    /// [`alter_object_set_schema`](StatementDdlGates::alter_object_set_schema) `SET SCHEMA` head
1853    /// where that flag is on. A *separate* behaviour from that schema-relocation gate: this is
1854    /// the redefinition/option surface, that is the cross-schema move.
1855    pub view_definition_options: bool,
1856}
1857
1858/// Dialect-owned `CREATE TABLE` table-level clause syntax accepted by the parser.
1859///
1860/// The table-level decorations on a `CREATE TABLE` — the clauses that attach to the table
1861/// as a whole rather than to a single column or constraint. Split out of the retired
1862/// `SchemaChangeSyntax` at its 16-field line as the table-clause axis, distinct from the
1863/// column-definition and constraint axes. Each flag is a grammar gate: when off the clause
1864/// keyword is left unconsumed and surfaces as a clean parse error.
1865#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1866pub struct CreateTableClauseSyntax {
1867    /// Accept the MySQL `CREATE TABLE` storage decorations: the trailing table-option
1868    /// list (`ENGINE = InnoDB`, `AUTO_INCREMENT = 100`, `DEFAULT CHARSET = utf8mb4`,
1869    /// `COMMENT = '...'`, `ROW_FORMAT = ...`, `COLLATE = ...`) and the column-level
1870    /// `AUTO_INCREMENT` attribute. One flag gates both grammar points because they
1871    /// are a single dialect unit — MySQL always admits the column attribute wherever
1872    /// it admits the table option (mirroring how `existence_guards.if_exists` gates
1873    /// `IF EXISTS` on `DROP` and `IF NOT EXISTS` on `ADD COLUMN` together). Not
1874    /// ANSI/PostgreSQL,
1875    /// which reject the trailing options and the column attribute as leftover input.
1876    pub table_options: bool,
1877    /// Accept the SQLite trailing `WITHOUT ROWID` table option on `CREATE TABLE`
1878    /// (`CREATE TABLE t (a INTEGER PRIMARY KEY) WITHOUT ROWID`), recorded as
1879    /// [`CreateTableOptionKind::WithoutRowid`](crate::ast::CreateTableOptionKind::WithoutRowid).
1880    /// SQLite-only; off in every other preset, where the trailing `WITHOUT ROWID` is left
1881    /// unconsumed and surfaces as a clean parse error. Split out of
1882    /// the retired `sqlite_table_decorations` bundle because the rowid-storage
1883    /// table option is an independent grammar point from the trailing `STRICT` option, the
1884    /// typeless column, `AUTOINCREMENT`, the column `COLLATE`, and the inline-`PRIMARY KEY`
1885    /// ordering.
1886    pub without_rowid_table_option: bool,
1887    /// Accept the SQLite trailing `STRICT` table option on `CREATE TABLE`
1888    /// (`CREATE TABLE t (a INTEGER) STRICT`), recorded as
1889    /// [`CreateTableOptionKind::Strict`](crate::ast::CreateTableOptionKind::Strict); the table
1890    /// then enforces its declared column types instead of SQLite's default flexible typing.
1891    /// SQLite-only; off in every other preset, where the trailing `STRICT` is left unconsumed
1892    /// and surfaces as a clean parse error. Split out of
1893    /// the retired `sqlite_table_decorations` bundle because the strict-typing
1894    /// table option is an independent grammar point from the trailing `WITHOUT ROWID` option,
1895    /// the typeless column, `AUTOINCREMENT`, the column `COLLATE`, and the inline-`PRIMARY KEY`
1896    /// ordering.
1897    pub strict_table_option: bool,
1898    /// Accept DuckDB's `CREATE OR REPLACE TABLE` — the `OR REPLACE` object-replacement
1899    /// modifier on `TABLE` (a flag threaded onto the [`CreateTable`](crate::ast::CreateTable)
1900    /// node). On for DuckDB/Lenient. Off elsewhere: the other dialects take `OR REPLACE` only
1901    /// on `VIEW`/`FUNCTION` (gated by [`or_replace`](StatementDdlGates::or_replace)), so with this flag off
1902    /// a `TABLE` after `OR REPLACE` is left for the `VIEW` expectation and surfaces as a clean
1903    /// parse error. Distinct from [`or_replace`](StatementDdlGates::or_replace), which gates the modifier
1904    /// on the view/function surfaces every dialect but SQLite admits.
1905    pub create_or_replace_table: bool,
1906    /// Accept the `CREATE TABLE … WITH ( <name> = <value>, … )` storage-parameter list
1907    /// (PostgreSQL `WITH (fillfactor=…)`; Trino/Spark `WITH (format=…)`). On for
1908    /// ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no `WITH (…)` table clause, so it
1909    /// is off; the `WITH` keyword is then not read as a table option and surfaces as a
1910    /// clean parse error.
1911    pub storage_parameters: bool,
1912    /// Accept the temporary-table `ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP }`
1913    /// action (SQL:1999; PostgreSQL). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite
1914    /// has no `ON COMMIT` table clause, so it is off; the `ON` keyword is then left
1915    /// unconsumed and surfaces as a clean parse error.
1916    pub on_commit: bool,
1917    /// Accept the trailing `WITH [NO] DATA` populate clause on `CREATE TABLE … AS <query>`
1918    /// (and `CREATE MATERIALIZED VIEW`). On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL's
1919    /// `CREATE TABLE … AS SELECT` has no `WITH [NO] DATA` clause (`ER_PARSE_ERROR` on
1920    /// mysql:8), so it is off there; the `WITH` keyword after the query is then left as
1921    /// leftover input and surfaces as a clean parse error.
1922    pub create_table_as_with_data: bool,
1923    /// Accept the PostgreSQL `CREATE TABLE t [(cols)] AS EXECUTE <prepared> [(args)] [WITH [NO]
1924    /// DATA]` form — a CTAS whose rows come from running a prepared statement. On for
1925    /// PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB (DuckDB rejects `AS EXECUTE`), where
1926    /// the `EXECUTE` keyword after `AS` is left unconsumed and the inline-query CTAS path rejects
1927    /// it as a clean parse error.
1928    pub create_table_as_execute: bool,
1929    /// Accept PostgreSQL declarative partitioning: the parent `CREATE TABLE … PARTITION BY
1930    /// {LIST | RANGE | HASH} (<key>, …)` clause, the child `CREATE TABLE … PARTITION OF <parent>
1931    /// [(<augmentation>, …)] {FOR VALUES … | DEFAULT}` body, and the `ALTER TABLE … {ATTACH |
1932    /// DETACH} PARTITION` actions. On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB:
1933    /// none spell this grammar (MySQL's `PARTITION BY HASH(c) PARTITIONS n` and DuckDB's
1934    /// COPY-level `PARTITION_BY` are unrelated surfaces), so the `PARTITION` / `ATTACH` /
1935    /// `DETACH` keyword is left unconsumed and surfaces as a clean parse error. One flag gates
1936    /// the whole family — the parent spec, the child body, and the two alter actions travel
1937    /// together as a single dialect unit.
1938    pub declarative_partitioning: bool,
1939    /// Accept the PostgreSQL `INHERITS (<parent>, ...)` legacy table-inheritance clause
1940    /// (`CREATE TABLE t (…) INHERITS (parent)`). On for PostgreSQL/Lenient. Off for
1941    /// ANSI/MySQL/SQLite/DuckDB — none have table inheritance (DuckDB, which otherwise inherits
1942    /// the PostgreSQL schema surface, rejects it), so the `INHERITS` keyword is left unconsumed
1943    /// and surfaces as a clean parse error.
1944    pub table_inheritance: bool,
1945    /// Accept the PostgreSQL `LIKE <source> [{INCLUDING | EXCLUDING} <feature> …]` source-table
1946    /// copy *element* inside the parenthesized `CREATE TABLE` definition list (`CREATE TABLE t
1947    /// (LIKE src INCLUDING ALL)`). On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB:
1948    /// DuckDB rejects the element form, and MySQL's `CREATE TABLE t LIKE src` is a distinct
1949    /// *statement-level* production (no parentheses), not this element — so when off, a `LIKE` at
1950    /// an element position surfaces as a clean parse error.
1951    pub like_source_table: bool,
1952    /// Accept MySQL's statement-level `CREATE TABLE t LIKE <source>` table-clone body and its
1953    /// parenthesized twin `CREATE TABLE t (LIKE <source>)` — a whole-statement production that
1954    /// copies an existing table's definition, distinct from the PostgreSQL copy *element* gated
1955    /// by [`like_source_table`](CreateTableClauseSyntax::like_source_table). The source is a single bare (qualified)
1956    /// name: no `{INCLUDING | EXCLUDING} <feature>` options, no co-element, no trailing table
1957    /// options (`LIKE src ENGINE=…`, `(LIKE src, x INT)`, `(LIKE src INCLUDING ALL)` are all
1958    /// `ER_PARSE_ERROR` on mysql:8.4). On for MySQL/Lenient. Off for ANSI/PostgreSQL/SQLite/
1959    /// DuckDB, where a `LIKE` after the table name (or as the first token inside `(`) is left
1960    /// unconsumed and surfaces as a clean parse error — PostgreSQL rejects the bare form at raw
1961    /// parse, and only reads `(LIKE src …)` as the element form above. When both this and
1962    /// [`like_source_table`](CreateTableClauseSyntax::like_source_table) are on (Lenient), the parenthesized `(LIKE
1963    /// …)` reads as the more general PostgreSQL element (a superset that also admits the feature
1964    /// options), so this flag governs only the bare form there; MySQL, with the element flag off,
1965    /// takes both spellings onto this body.
1966    pub statement_level_table_like: bool,
1967    /// Accept `CREATE UNLOGGED TABLE` — the non-WAL-logged persistence keyword. On for
1968    /// PostgreSQL/DuckDB/Lenient (DuckDB parses it as a no-op). Off for ANSI/MySQL/SQLite, where
1969    /// `UNLOGGED` after `CREATE` is left unconsumed and surfaces as a clean parse error. In
1970    /// PostgreSQL's grammar `UNLOGGED` is a peer of `TEMP`/`TEMPORARY`, so the two are mutually
1971    /// exclusive; the parser rejects `CREATE TEMP UNLOGGED TABLE` accordingly.
1972    pub unlogged_tables: bool,
1973    /// Accept the trailing `USING <access_method>` table access-method clause (PostgreSQL
1974    /// `CREATE TABLE … USING heap`). On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB
1975    /// (DuckDB has no pluggable table access methods), where the `USING` keyword after the table
1976    /// body is left unconsumed and surfaces as a clean parse error. Distinct from the CREATE
1977    /// INDEX `USING <method>` clause gated by [`index_using_method`](IndexAlterSyntax::index_using_method).
1978    pub table_access_method: bool,
1979    /// Accept the legacy `WITHOUT OIDS` trailing option (PostgreSQL) — kept as an accepted
1980    /// no-op. On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB, where the `WITHOUT`
1981    /// keyword (absent SQLite's `WITHOUT ROWID`, a separate
1982    /// [`without_rowid_table_option`](CreateTableClauseSyntax::without_rowid_table_option) option) is left unconsumed
1983    /// and surfaces as a clean parse error.
1984    pub without_oids: bool,
1985    /// Accept the `CREATE TABLE t OF <type> [(…)]` typed-table form (PostgreSQL): the table's
1986    /// column shape is drawn from a composite type. On for PostgreSQL/Lenient. Off for
1987    /// ANSI/MySQL/SQLite/DuckDB, where `OF` after the table name is left unconsumed and surfaces
1988    /// as a clean parse error.
1989    pub typed_tables: bool,
1990}
1991
1992/// Dialect-owned `CREATE TABLE` column-definition syntax accepted by the parser.
1993///
1994/// The column-level decorations inside a `CREATE TABLE` definition — the attributes and
1995/// restrictions that attach to a single column. Split out of the retired
1996/// `SchemaChangeSyntax` at its 16-field line as the column-definition axis, distinct from
1997/// the table-clause and constraint axes. Each flag is a grammar gate: when off the
1998/// decoration is left unconsumed and rejects.
1999#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2000pub struct ColumnDefinitionSyntax {
2001    /// Accept the keywordless generated-column shorthand `<col> <type> AS (<expr>)
2002    /// [STORED|VIRTUAL]`, written without the leading `GENERATED ALWAYS` (MySQL,
2003    /// SQLite). It folds onto the one [`GeneratedColumn`](crate::ast::GeneratedColumn)
2004    /// shape tagged
2005    /// [`GeneratedColumnSpelling::Shorthand`](crate::ast::GeneratedColumnSpelling),
2006    /// never a new node. PostgreSQL requires the `GENERATED ALWAYS`
2007    /// keywords, so this is off there and the bare `AS (…)` after a column type is left
2008    /// unconsumed and surfaces as a clean parse error.
2009    pub generated_column_shorthand: bool,
2010    /// Accept the SQLite column-level `ON CONFLICT <resolution>` clause on an inline
2011    /// `NOT NULL` / `UNIQUE` / `PRIMARY KEY` / `CHECK` constraint
2012    /// (`a INTEGER UNIQUE ON CONFLICT REPLACE`), recording the resolution algorithm in
2013    /// [`ColumnConstraint::conflict`](crate::ast::ColumnConstraint). SQLite-only; off in
2014    /// every other preset, where the trailing `ON` after the constraint is left unconsumed
2015    /// and surfaces as a clean parse error. Split out of
2016    /// the retired `sqlite_table_decorations` bundle because column-level
2017    /// conflict resolution is an independent grammar point from the trailing table options,
2018    /// the typeless column, `AUTOINCREMENT`, and the inline-`PRIMARY KEY` ordering.
2019    pub column_conflict_resolution_clause: bool,
2020    /// Accept a SQLite *typeless* column definition — a column named with no data type
2021    /// (`CREATE TABLE t (a, b)`), leaving [`ColumnDef::data_type`](crate::ast::ColumnDef)
2022    /// unset. SQLite-only; off in every other preset, where a column with no type falls
2023    /// through to `parse_data_type` and surfaces as a clean parse error. Split out of
2024    /// the retired `sqlite_table_decorations` bundle because the typeless
2025    /// column is an independent grammar point from the trailing table options,
2026    /// `AUTOINCREMENT`, the column `COLLATE`, and the inline-`PRIMARY KEY` ordering.
2027    pub typeless_column_definitions: bool,
2028    /// Accept a column that omits its data type *only* when the column is a generated column —
2029    /// DuckDB's narrowing of the SQLite typeless rule. DuckDB requires a data type on every
2030    /// column except a generated one: both the `GENERATED { ALWAYS | BY DEFAULT } AS …` form
2031    /// and the keywordless `AS (<expr>)` shorthand may drop the type
2032    /// (`CREATE TABLE t (x INT, gen_x AS (x + 5))`, engine-measured on libduckdb 1.5.4), while
2033    /// a plain typeless column (`x`) or a typeless non-generated constraint (`y DEFAULT 5`) is
2034    /// a parse error. On for DuckDB/Lenient. Off for ANSI/PostgreSQL/MySQL — a generated column
2035    /// with no type falls through to `parse_data_type` and surfaces as a clean parse error — and
2036    /// off for SQLite, which instead accepts *any* typeless column via the strictly wider
2037    /// [`typeless_column_definitions`](ColumnDefinitionSyntax::typeless_column_definitions), so
2038    /// this narrow gate stays off there.
2039    pub typeless_generated_columns: bool,
2040    /// Accept the SQLite joined `AUTOINCREMENT` column attribute — the bare one-word keyword
2041    /// on an inline `PRIMARY KEY` column (`a INTEGER PRIMARY KEY AUTOINCREMENT`), recorded as
2042    /// [`AutoIncrementSpelling::Joined`](crate::ast::AutoIncrementSpelling::Joined) so it
2043    /// round-trips as one word. SQLite-only; off in every other preset, where the trailing
2044    /// `AUTOINCREMENT` is left unconsumed and surfaces as a clean parse error. This gates
2045    /// *only* the joined spelling: the underscored MySQL `AUTO_INCREMENT` attribute is a
2046    /// separate surface gated by [`table_options`](CreateTableClauseSyntax::table_options), so the two spellings
2047    /// toggle independently and neither preset admits the other's word. Split out of
2048    /// the retired `sqlite_table_decorations` bundle because the auto-increment
2049    /// attribute is an independent grammar point from the typeless column, the column
2050    /// `COLLATE`, and the inline-`PRIMARY KEY` ordering.
2051    pub joined_autoincrement_attribute: bool,
2052    /// Accept an `ASC`/`DESC` sort-order qualifier on an inline `PRIMARY KEY` column
2053    /// constraint (`CREATE TABLE t (a INTEGER PRIMARY KEY DESC)`), recorded in the
2054    /// [`ColumnOption::PrimaryKey`](crate::ast::ColumnOption) `ascending` field (`ASC` →
2055    /// `Some(true)`, `DESC` → `Some(false)`, absent → `None`). SQLite-only; off in every other
2056    /// preset, where the trailing `ASC`/`DESC` is left unconsumed and surfaces as a clean parse
2057    /// error. Split out of the retired `sqlite_table_decorations` bundle because
2058    /// the inline primary-key ordering is an independent grammar point from the trailing table
2059    /// options, the typeless column, `AUTOINCREMENT`, and the column `COLLATE`.
2060    pub inline_primary_key_ordering: bool,
2061    /// Accept a `CONSTRAINT <name>` prefix on a column `COLLATE` clause
2062    /// (`CREATE TABLE t (a TEXT CONSTRAINT c COLLATE nocase)`): SQLite's grammar makes `COLLATE`
2063    /// an ordinary nameable column constraint, so a `CONSTRAINT <name>` symbol binds to it.
2064    /// SQLite-only; off in every other preset (PostgreSQL and DuckDB engine-measured reject the
2065    /// named form, where `COLLATE any_name` is a constraint alternative parallel to — not under —
2066    /// the nameable constraint element), leaving the `CONSTRAINT <name>` prefix on a `COLLATE`
2067    /// clause as a clean parse error. This gates *only* the named wrapper: the bare column
2068    /// `COLLATE` surface ([`ColumnOption::Collate`](crate::ast::ColumnOption)) is a separate
2069    /// cross-dialect shape gated by [`column_collation`](ColumnDefinitionSyntax::column_collation), so the accepting
2070    /// case needs both flags on. Split out of
2071    /// the retired `sqlite_table_decorations` bundle because the named-`COLLATE`
2072    /// wrapper is an independent grammar point from the inline-`PRIMARY KEY` ordering.
2073    pub named_column_collate_constraint: bool,
2074    /// Accept the `<col> <type> GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [(…)]`
2075    /// identity column (SQL:2003; PostgreSQL). On for ANSI/PostgreSQL/MySQL/DuckDB/
2076    /// Lenient. SQLite has no `IDENTITY` (its auto-key is `INTEGER PRIMARY KEY
2077    /// [AUTOINCREMENT]`), so it is off; the `IDENTITY` keyword is then left unconsumed and
2078    /// surfaces as a clean parse error. Gates only the `AS IDENTITY` reading — the
2079    /// `GENERATED ALWAYS AS (<expr>)` computed column (which SQLite has) is unaffected.
2080    pub identity_columns: bool,
2081    /// Require a *parenthesized* `DEFAULT (expr)` for a functional / general-expression
2082    /// column default. On for MySQL: a column `DEFAULT` there admits only a literal, a
2083    /// signed literal, the `CURRENT_TIMESTAMP`/`NOW()`/`LOCALTIME`/`LOCALTIMESTAMP` temporal
2084    /// family, or a parenthesized expression — a bare function call or operator expression
2085    /// (`DEFAULT UUID()`, `DEFAULT 1 + 2`) is an `ER_PARSE_ERROR` on mysql:8, while
2086    /// `DEFAULT (UUID())` parses. Off for ANSI/PostgreSQL/SQLite/DuckDB/Lenient, which accept
2087    /// a bare expression default. When off, the default expression is read whole with no
2088    /// wrapping requirement.
2089    pub default_expression_requires_parens: bool,
2090    /// Restrict a *column-constraint* `DEFAULT` to PostgreSQL's `b_expr` grammar class
2091    /// (`ColConstraintElem: DEFAULT b_expr`), rather than the full `a_expr` used everywhere
2092    /// else. `b_expr` is the "boolean-and-predicate-free" expression production: it keeps
2093    /// arithmetic, comparison, `||`, `OPERATOR(...)`, `::`, subscripts, `COLLATE`, and the
2094    /// `IS [NOT] DISTINCT FROM` / `IS [NOT] DOCUMENT` tests, but excludes the `a_expr`-only
2095    /// forms — `AND`/`OR`/`NOT`, `IN`, `BETWEEN`, `LIKE`/`ILIKE`/`SIMILAR TO`, `IS [NOT]
2096    /// NULL`/`TRUE`/`FALSE`/`UNKNOWN`, a quantified `= ANY(...)`, and `AT TIME ZONE`. So
2097    /// `... DEFAULT 1 IN (1, 2)` is a syntax error on PostgreSQL (the `IN` predicate is
2098    /// `a_expr`-level), while the parenthesized `DEFAULT (1 IN (1, 2))` — a `c_expr` reset to
2099    /// `a_expr` — parses. On for PostgreSQL only; every other dialect (including DuckDB, which
2100    /// otherwise inherits the PostgreSQL schema surface) reads the default as a full `a_expr`.
2101    /// Note the asymmetry: `ALTER COLUMN ... SET DEFAULT` is `a_expr` even on PostgreSQL, so
2102    /// this gates only the inline column-constraint site.
2103    pub column_default_requires_b_expr: bool,
2104    /// Accept the column-definition `COLLATE <collation>` clause (`a text COLLATE "C"`). On for
2105    /// PostgreSQL/SQLite/DuckDB/Lenient — all three spell a per-column collation inside the
2106    /// column definition (PostgreSQL takes a qualified `any_name`, SQLite/DuckDB a single bare
2107    /// identifier; the parser narrows the name grammar per dialect). Off for ANSI/MySQL, where a
2108    /// column-level `COLLATE` is left unconsumed and surfaces as a clean parse error. Distinct
2109    /// from the expression-level `COLLATE` gated by
2110    /// [`ExpressionSyntax::collate`] on a different sub-struct — this one
2111    /// qualifies a column, not an expression. (MySQL *does* spell a column `COLLATE` in its own
2112    /// `CHARACTER SET … COLLATE …` attribute grammar, a distinct surface left to a follow-up.)
2113    pub column_collation: bool,
2114    /// Accept the per-column `STORAGE {PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT}` and
2115    /// `COMPRESSION <method>` physical-storage clauses (PostgreSQL). The two travel together as a
2116    /// single dialect unit — both are fixed-position clauses between a column's type and its
2117    /// constraint list. On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB (DuckDB,
2118    /// which otherwise inherits the PostgreSQL schema surface, rejects both), where the
2119    /// `STORAGE`/`COMPRESSION` keyword is left unconsumed and surfaces as a clean parse error.
2120    pub column_storage: bool,
2121}
2122
2123/// Dialect-owned table/column-constraint syntax accepted by the parser.
2124///
2125/// The constraint forms and their decorations. Split out of the retired
2126/// `SchemaChangeSyntax` at its 16-field line as the constraint axis, distinct from the
2127/// table-clause and column-definition axes. Each flag is a grammar gate: when off the
2128/// constraint keyword is left unconsumed and rejects.
2129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2130pub struct ConstraintSyntax {
2131    /// Accept a trailing `<constraint characteristics>` clause (`[NOT] DEFERRABLE`,
2132    /// `INITIALLY {DEFERRED | IMMEDIATE}`) on a table/column constraint, in both `CREATE
2133    /// TABLE` and `ALTER TABLE ADD CONSTRAINT`. On for PostgreSQL/SQLite/DuckDB/Lenient.
2134    /// MySQL has no deferrable constraints — `… REFERENCES t (c) DEFERRABLE` / `INITIALLY
2135    /// DEFERRED` are `ER_PARSE_ERROR` on mysql:8 — so it is off there and the
2136    /// `DEFERRABLE`/`INITIALLY` keyword surfaces as a clean parse error.
2137    pub deferrable_constraints: bool,
2138    /// Accept a `CONSTRAINT <symbol>` name prefix on a *non-CHECK* inline column constraint
2139    /// (`a INT CONSTRAINT c REFERENCES b`, `… CONSTRAINT c UNIQUE`). On for ANSI/PostgreSQL/
2140    /// SQLite/DuckDB/Lenient. MySQL admits a named inline constraint only for `CHECK`
2141    /// (`a INT CONSTRAINT c CHECK (…)` parses on mysql:8), so a `CONSTRAINT <symbol>` before
2142    /// any other inline column constraint — `REFERENCES`, `UNIQUE`, `PRIMARY KEY`,
2143    /// `NOT NULL` — is an `ER_PARSE_ERROR`; it is off there. The bare, unnamed inline
2144    /// constraints and the named `CHECK` are unaffected either way.
2145    pub named_inline_non_check_constraints: bool,
2146    /// Accept a trailing bodyless `CONSTRAINT <name>` — a nameable constraint marker with no
2147    /// constraint element after it — in a column definition (`a INT CONSTRAINT cn`) or as a
2148    /// standalone table constraint (`CREATE TABLE t (a INT, CONSTRAINT cn)`), recorded as
2149    /// [`ColumnOption::Bare`](crate::ast::ColumnOption)/[`TableConstraint::Bare`](crate::ast::TableConstraint).
2150    /// SQLite's grammar makes the constraint element optional after `CONSTRAINT <name>`, and (in
2151    /// the table-constraint list only) lets the separating comma before such a bare marker be
2152    /// omitted too — `UNIQUE(a) CONSTRAINT c` and `CONSTRAINT a UNIQUE(x) CONSTRAINT b` both
2153    /// engine-measured accept, any number of bare/named-with-body constraints chaining freely.
2154    /// Parsed only when nothing but the element terminator (`,`/`)`) follows the name — a
2155    /// `CONSTRAINT <name>` immediately followed by a constraint element (`CHECK`, `UNIQUE`, …)
2156    /// still takes that element as its body, unaffected by this flag. On for SQLite/Lenient. Off
2157    /// elsewhere (PostgreSQL engine-measured rejects a bodyless `CONSTRAINT <name>`, requiring a
2158    /// constraint element), where a `CONSTRAINT <name>` with nothing following is a clean parse
2159    /// error.
2160    pub bare_constraint_name: bool,
2161    /// Accept the PostgreSQL `EXCLUDE [USING <method>] (<element> WITH <operator> [, ...]) [tail]`
2162    /// exclusion constraint as a table element (`CREATE TABLE t (c circle, EXCLUDE USING gist (c
2163    /// WITH &&))`). On for PostgreSQL/Lenient. Off for ANSI/MySQL/SQLite/DuckDB (DuckDB, which
2164    /// otherwise inherits the PostgreSQL schema surface, rejects it), where `EXCLUDE` at a
2165    /// constraint position is left unconsumed and surfaces as a clean parse error.
2166    pub exclusion_constraints: bool,
2167    /// Accept the `NO INHERIT` / `NOT VALID` constraint markers on `CHECK` (and `NOT VALID` on
2168    /// `FOREIGN KEY`) constraints — PostgreSQL's shared `ConstraintAttributeSpec` slot, also
2169    /// accepted by DuckDB. On for PostgreSQL/DuckDB/Lenient. Off for ANSI/MySQL/SQLite, where the
2170    /// `NO INHERIT` / `NOT VALID` keywords after a constraint are left unconsumed and surface as a
2171    /// clean parse error. The parser enforces which constraint kinds admit which marker
2172    /// (PostgreSQL rejects `NOT VALID` on `PRIMARY KEY`/`UNIQUE`/`EXCLUDE` and `NO INHERIT` on
2173    /// everything but `CHECK`, in the grammar action — reproduced at parse), so the flag only
2174    /// opens the keywords, not every combination.
2175    pub constraint_no_inherit_not_valid: bool,
2176    /// Accept the PostgreSQL index-backed-constraint parameters on `UNIQUE`/`PRIMARY KEY`: the
2177    /// covering `INCLUDE (<col>, ...)` list, the `NULLS [NOT] DISTINCT` null-treatment (PG 15+),
2178    /// and the `USING INDEX TABLESPACE <name>` index tablespace. The three travel together as one
2179    /// PostgreSQL `ConstraintElem` index-parameter unit (the same "single dialect unit" rationale
2180    /// as [`column_storage`](ColumnDefinitionSyntax::column_storage)). On for PostgreSQL/Lenient. Off for
2181    /// ANSI/MySQL/SQLite/DuckDB (DuckDB rejects all three), where the `INCLUDE`/`NULLS`/`USING`
2182    /// keyword is left unconsumed and surfaces as a clean parse error.
2183    pub index_constraint_parameters: bool,
2184    /// Accept a per-column `COLLATE <collation>` and `ASC`/`DESC` sort order inside a
2185    /// `PRIMARY KEY (...)` / `UNIQUE (...)` table-constraint column list — SQLite's
2186    /// "indexed-column" spelling in constraint position (`PRIMARY KEY (a COLLATE nocase)`,
2187    /// `UNIQUE ('b' COLLATE nocase DESC)`). On for SQLite/Lenient.
2188    ///
2189    /// Engine-measured scope (constraint position, *not* `CREATE INDEX`): SQLite admits a bare
2190    /// column name optionally decorated with `COLLATE`/`ASC`/`DESC`, but prohibits general
2191    /// expressions (`UNIQUE (a+b)` / `(lower(a))` → "expressions prohibited in PRIMARY KEY and
2192    /// UNIQUE constraints") and `NULLS FIRST`/`LAST` ("unsupported use of NULLS FIRST"), so the
2193    /// widened parser stays column-name + `COLLATE` + `ASC`/`DESC` and never fills
2194    /// [`IndexColumn::nulls_first`](crate::ast::IndexColumn). Off for ANSI/PostgreSQL/DuckDB
2195    /// (all engine-measured reject `COLLATE`/`ASC`/`DESC` here — the decoration belongs to their
2196    /// `CREATE INDEX` grammar, not the table constraint), where the keyword is left unconsumed
2197    /// and surfaces as a clean parse error. Also off for MySQL: its `key_part` admits `ASC`/`DESC`
2198    /// and length prefixes / functional `(expr)` parts but not `COLLATE`, a differently-shaped
2199    /// surface with no corpus demand — left off and scoped out rather than modelled as this
2200    /// SQLite-shaped gate.
2201    pub constraint_column_collate_order: bool,
2202    /// Accept the cascading referential actions `ON {DELETE|UPDATE} {CASCADE | SET NULL |
2203    /// SET DEFAULT}` on a foreign-key constraint. On for ANSI/PostgreSQL/MySQL/SQLite/Lenient.
2204    /// DuckDB parse-rejects those three actions ("FOREIGN KEY constraints cannot use CASCADE,
2205    /// SET NULL or SET DEFAULT", probed on 1.5.4) while still admitting `RESTRICT` and
2206    /// `NO ACTION`, so it is off there; with the flag off those three keywords after
2207    /// `ON DELETE`/`ON UPDATE` surface as a clean parse error. Distinct from admitting the
2208    /// `ON DELETE`/`ON UPDATE` clause itself — `RESTRICT`/`NO ACTION` remain free either way.
2209    pub referential_action_cascade_set: bool,
2210    /// Accept a subquery (or `EXISTS` / `IN (SELECT …)`) inside a `CHECK` constraint
2211    /// expression. On for ANSI/PostgreSQL/MySQL/Lenient. Off for SQLite and DuckDB, both of
2212    /// which parse-reject subqueries in `CHECK` ("subqueries prohibited in CHECK constraints",
2213    /// engine-measured); with the flag off a subquery in the CHECK body is a clean parse error.
2214    pub check_constraint_subqueries: bool,
2215}
2216
2217/// Dialect-owned `CREATE INDEX` / `ALTER TABLE` / `DROP` syntax accepted by the parser.
2218///
2219/// The clause-level gates on the index, alter-table, and drop statements — the object-DDL
2220/// surface that decorates an already-dispatched statement rather than deciding its leading
2221/// keyword. Split out of the retired `SchemaChangeSyntax` at its 16-field line as the
2222/// index/alter/drop axis, distinct from the whole-statement-dispatch axis. Each flag is a
2223/// grammar gate: when off the keyword is left unconsumed and rejects.
2224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2225pub struct IndexAlterSyntax {
2226    /// Accept a trailing `CASCADE` / `RESTRICT` drop behaviour on `DROP` statements
2227    /// and `ALTER TABLE ... DROP` actions (the SQL standard `<drop behavior>`;
2228    /// PostgreSQL). A dialect that does not model dependency behaviour leaves it off.
2229    pub drop_behavior: bool,
2230    /// Accept MySQL's `DROP INDEX <name> ON <table> [ALGORITHM [=] {DEFAULT | INPLACE |
2231    /// INSTANT | COPY}] [LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}]` — the index drop
2232    /// that names its owning table with a mandatory `ON` and carries the online-DDL
2233    /// `ALGORITHM`/`LOCK` execution hints (`drop_index_stmt`/`opt_index_lock_and_algorithm`,
2234    /// `sql_yacc.yy`). Server-measured on mysql:8: the `ON <table>` is mandatory (`DROP INDEX
2235    /// i` with no `ON` is `ER_PARSE_ERROR`), the tail admits at most one `ALGORITHM` and one
2236    /// `LOCK` in either order, and no trailing `CASCADE`/`RESTRICT`. On for MySQL only. Off
2237    /// elsewhere, where `DROP INDEX <name> [, …]` stays the shared name-list drop; enabling it
2238    /// would make the mandatory-`ON` form displace that name-list drop, so Lenient forgoes it
2239    /// to keep the more permissive bare-name form (a documented conflict resolution). With the
2240    /// flag off a `DROP INDEX i ON t` leaves `ON` unconsumed and surfaces as a clean parse
2241    /// error.
2242    pub index_drop_on_table: bool,
2243    /// Accept `CREATE INDEX CONCURRENTLY` (PostgreSQL builds the index without an
2244    /// exclusive table lock). Not ANSI.
2245    pub index_concurrently: bool,
2246    /// Accept the `CREATE INDEX … USING <method>` access-method clause (PostgreSQL
2247    /// `btree`/`hash`/`gin`/`gist`/…; MySQL `USING BTREE`). Not ANSI.
2248    pub index_using_method: bool,
2249    /// Accept a trailing `WHERE <predicate>` partial-index clause on `CREATE INDEX`
2250    /// (PostgreSQL, SQLite). Not ANSI.
2251    pub partial_index: bool,
2252    /// Accept the `IF NOT EXISTS` guard on `CREATE INDEX` (PostgreSQL/SQLite/DuckDB). On for
2253    /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL has no `CREATE INDEX IF NOT EXISTS`
2254    /// (engine-measured `ER_PARSE_ERROR` on mysql:8), so it is off; the `IF NOT EXISTS` is
2255    /// then left unconsumed and the following index name surfaces as a clean parse error.
2256    /// Distinct from the `CREATE TABLE`/`CREATE DATABASE` guards, which MySQL *does* admit.
2257    pub index_if_not_exists: bool,
2258    /// Accept the per-key `NULLS FIRST` / `NULLS LAST` null-ordering modifier on a
2259    /// `CREATE INDEX` column (PostgreSQL/SQLite 3.30+). On for ANSI/PostgreSQL/SQLite/DuckDB/
2260    /// Lenient. MySQL has no index-key `NULLS` ordering (engine-measured `ER_PARSE_ERROR` on
2261    /// mysql:8; it orders NULLs implicitly), so it is off; the `NULLS` keyword is then left
2262    /// unconsumed and surfaces as a clean parse error. Independent of the `ORDER BY` null
2263    /// ordering, which is a separate grammar position.
2264    pub index_nulls_order: bool,
2265    /// Accept the extended `ALTER TABLE` surface beyond SQLite's lenient action set: the
2266    /// table-level `IF EXISTS`, comma-separated multiple actions, `ALTER COLUMN …`, the
2267    /// `ADD PRIMARY KEY`/`UNIQUE`/`FOREIGN KEY` table constraints, and the `IF [NOT]
2268    /// EXISTS` guard on `ADD`/`DROP COLUMN` (PostgreSQL/MySQL). On for ANSI/PostgreSQL/
2269    /// MySQL/DuckDB/Lenient. Off for SQLite, whose `ALTER TABLE` (engine-measured via
2270    /// rusqlite) admits `RENAME TO`/`RENAME COLUMN`, a single `ADD [COLUMN] <def>` / `ADD
2271    /// [CONSTRAINT …] CHECK (…)`, and a single `DROP [COLUMN]` / `DROP CONSTRAINT` — those
2272    /// stay accepted with the flag off; every other form is left unconsumed and surfaces
2273    /// as a clean parse error. Distinct from [`ExistenceGuards::if_exists`], which stays
2274    /// on for SQLite (its `DROP TABLE IF EXISTS` is valid) — only the *`ALTER`* existence
2275    /// guard is gated here.
2276    pub alter_table_extended: bool,
2277    /// Accept a comma-separated multi-action `ALTER TABLE` list
2278    /// (`ALTER TABLE t ADD COLUMN a INT, DROP COLUMN b`). On for ANSI/PostgreSQL/MySQL/Lenient.
2279    /// Off for DuckDB (parse-rejects with "Only one ALTER command per statement is supported",
2280    /// probed on 1.5.4) and moot for SQLite (its
2281    /// [`alter_table_extended`](IndexAlterSyntax::alter_table_extended) is already off, so the
2282    /// multi-action loop is never entered). Only reachable where `alter_table_extended` is on;
2283    /// with the flag off the first action parses and a trailing comma surfaces as a clean parse
2284    /// error.
2285    pub alter_table_multiple_actions: bool,
2286    /// Accept an `IF EXISTS` / `IF NOT EXISTS` existence guard *inside* `ALTER TABLE` — the
2287    /// table-level `ALTER TABLE IF EXISTS t …` and the per-action `ADD COLUMN IF NOT EXISTS`
2288    /// / `DROP [COLUMN|CONSTRAINT] IF EXISTS` guards. On for PostgreSQL/DuckDB/Lenient (and
2289    /// unused by SQLite, whose non-[`alter_table_extended`](IndexAlterSyntax::alter_table_extended) path
2290    /// parses no guard, so it rides that gate —
2291    /// [`FeatureDependencyViolation::AlterExistenceGuardsWithoutAlterTableExtended`]). MySQL
2292    /// supports the extended `ALTER TABLE` surface (multi-action
2293    /// lists, `ADD`/`DROP CONSTRAINT`, `ALTER COLUMN`) but *not* these guards
2294    /// (`ALTER TABLE IF EXISTS`, `ADD COLUMN IF NOT EXISTS`, `DROP COLUMN IF EXISTS` are each
2295    /// `ER_PARSE_ERROR` on mysql:8), so it is off there; the `IF` keyword is then read as a
2296    /// name and surfaces as a clean parse error. Distinct from
2297    /// [`ExistenceGuards::if_exists`](ExistenceGuards::if_exists), which MySQL keeps on for
2298    /// `DROP TABLE IF EXISTS`.
2299    pub alter_existence_guards: bool,
2300    /// Accept dotted nested-column paths in DuckDB `ALTER TABLE` column targets for the
2301    /// actions the engine parses (`ADD COLUMN s.k`, `DROP COLUMN s.k`, and old-side
2302    /// `RENAME COLUMN s.k TO k2`). The gate does not affect `ALTER COLUMN` or the rename
2303    /// destination: DuckDB 1.5.4 parse-rejects dotted paths in those positions.
2304    pub alter_nested_column_paths: bool,
2305    /// Accept the PostgreSQL `ALTER TABLE … ALTER COLUMN` actions beyond `SET`/`DROP
2306    /// DEFAULT` — `SET DATA TYPE <type>` (and its bare `TYPE <type>` synonym, with an
2307    /// optional `USING <expr>`) plus `SET`/`DROP NOT NULL`. On for PostgreSQL/DuckDB/Lenient.
2308    /// MySQL's `ALTER COLUMN` admits only `SET`/`DROP DEFAULT` — it changes a column's type
2309    /// with `MODIFY`/`CHANGE` — so `ALTER COLUMN i SET DATA TYPE …`/`SET NOT NULL`/`DROP NOT
2310    /// NULL` are `ER_PARSE_ERROR` on mysql:8; with the flag off those actions surface as a
2311    /// clean parse error. SQLite never reaches the action (its
2312    /// [`alter_table_extended`](IndexAlterSyntax::alter_table_extended) is off), so this is moot there —
2313    /// it rides that gate
2314    /// ([`FeatureDependencyViolation::AlterColumnSetDataTypeWithoutAlterTableExtended`]).
2315    pub alter_column_set_data_type: bool,
2316    /// Accept a routine reference's parenthesized argument-type list — `DROP FUNCTION f(INT)`,
2317    /// `GRANT EXECUTE ON FUNCTION f(INT) …` (PostgreSQL overload-disambiguation). On for
2318    /// ANSI/PostgreSQL/DuckDB/Lenient. MySQL identifies a routine by name alone, so the arg
2319    /// list is a syntax error there (`DROP FUNCTION f(INT)` → engine-measured `ER_PARSE_ERROR`
2320    /// on mysql:8); with the flag off the `(` is left unconsumed and surfaces as a clean parse
2321    /// error. Off for SQLite too (it has no stored routines — [`routines`](StatementDdlGates::routines) is
2322    /// already off, so this is moot there).
2323    pub routine_arg_types: bool,
2324    /// Accept a `CREATE FUNCTION` parameter default — `func_arg DEFAULT <expr>` / `func_arg =
2325    /// <expr>` (PostgreSQL `func_arg_with_default`). On for ANSI/PostgreSQL/DuckDB/Lenient.
2326    /// MySQL's routine parameters are `[IN|OUT|INOUT] name type` with no default, so a
2327    /// `DEFAULT`/`=` after the type is a syntax error there (`ER_PARSE_ERROR` on mysql:8); with
2328    /// the flag off the `DEFAULT`/`=` is left unconsumed and the parameter-list close surfaces
2329    /// as a clean parse error. Distinct from [`routine_arg_types`](Self::routine_arg_types)
2330    /// (which gates the *reference*-site arg-type list on `DROP`/`GRANT`): this gates the
2331    /// *definition*-site default on `CREATE FUNCTION`. Off for SQLite too (no stored routines —
2332    /// [`routines`](StatementDdlGates::routines) is already off, so this is moot there).
2333    pub routine_arg_defaults: bool,
2334    /// Accept a `CREATE FUNCTION` parameter argument mode — the `arg_class` prefix
2335    /// `IN`/`OUT`/`INOUT`/`VARIADIC` before the parameter (PostgreSQL `func_arg`). On for
2336    /// ANSI/PostgreSQL/DuckDB/Lenient. MySQL's `CREATE FUNCTION` parameters are `name type`
2337    /// with no mode (the `IN`/`OUT`/`INOUT` modes are a stored-*procedure* form, a distinct
2338    /// statement), so a mode keyword before a `CREATE FUNCTION` parameter is a syntax error
2339    /// there; with the flag off the keyword is left for the name/type parse (a reserved mode
2340    /// keyword like `IN`/`VARIADIC` then surfaces as a clean parse error). A sibling of
2341    /// [`routine_arg_defaults`](Self::routine_arg_defaults): both gate independent
2342    /// *definition*-site `CREATE FUNCTION` parameter facets that MySQL's grammar omits. Off
2343    /// for SQLite too (no stored routines — [`routines`](StatementDdlGates::routines) is
2344    /// already off, so this is moot there).
2345    pub routine_arg_modes: bool,
2346    /// Accept a string-constant (`Sconst`) spelling of the routine `LANGUAGE` name —
2347    /// `LANGUAGE 'sql'`/`E'sql'`/`$$sql$$` — alongside the bare word, PostgreSQL's
2348    /// `NonReservedWord_or_Sconst` operand (the same shape the `DO … LANGUAGE` argument
2349    /// spells). On for PostgreSQL/Lenient. MySQL's routine `LANGUAGE` admits only the bare
2350    /// word `SQL` — `LANGUAGE 'SQL'` is engine-measured `ER_PARSE_ERROR` (1064) on mysql:8 for
2351    /// both `CREATE FUNCTION` and `CREATE PROCEDURE` — so off there; with the flag off a string
2352    /// in the `LANGUAGE` position falls through to the bare-word parse, which rejects the string
2353    /// as MySQL does. Off for ANSI too: the SQL-standard `<language name>` is a bare identifier
2354    /// (`SQL`/`C`/`ADA`/…), not a string. A bit-string (`b'…'`/`x'…'`) or national (`N'…'`)
2355    /// constant is never an `Sconst`, so it stays a reject even where the flag is on (matching
2356    /// PostgreSQL). Off for SQLite too (no stored routines — [`routines`](StatementDdlGates::routines)
2357    /// is already off, so this is moot there).
2358    pub routine_language_string: bool,
2359}
2360
2361/// Dialect-owned `IF [NOT] EXISTS` existence guards on DDL statements.
2362///
2363/// The SQL existence guards — `IF EXISTS` on `DROP`/`ALTER`, `IF NOT EXISTS` on
2364/// `CREATE` — are dialect data: each flag decides whether the parser admits
2365/// the guard at one statement site, and when off the `IF [NOT] EXISTS` is left
2366/// unconsumed and surfaces as a clean parse error (the same reject mechanism the other
2367/// grammar gates use). They live in their own sub-struct rather than scattered through
2368/// the retired `SchemaChangeSyntax` because their bundle membership differs per dialect: leaving a
2369/// coarse `if_exists` bundle in `SchemaChangeSyntax` made every new dialect bolt on a
2370/// separate exception flag (`view_if_not_exists`, `create_database_if_not_exists`), so a
2371/// dedicated per-site table is the shape that does not accrete one exception per dialect.
2372///
2373/// Two-level spelling convention (uniform across the dialect-data knobs): a top-level
2374/// [`FeatureSet`] assembly spells every field explicitly, while a *sub-preset* const of a
2375/// knob like this one may struct-update-derive from a sibling with `..Self::OTHER` (the
2376/// [`SelectSyntax::DUCKDB`] precedent) — the base preset stays the exhaustive source of
2377/// truth and the derived one records only its deltas.
2378///
2379/// [`SelectSyntax::DUCKDB`]: crate::dialect::SelectSyntax
2380#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2381pub struct ExistenceGuards {
2382    /// Accept `IF EXISTS` on `DROP`/`ALTER TABLE` (and on their column/constraint
2383    /// sub-actions) and `IF NOT EXISTS` on `ADD COLUMN` (PostgreSQL, MySQL, SQLite;
2384    /// not ANSI, whose `DROP`/`ALTER` has no existence guard). One flag gates the
2385    /// drop/alter/add-column existence guards together because a dialect that spells one
2386    /// spells them all; the `ALTER TABLE`/`ADD COLUMN` sites additionally require
2387    /// [`IndexAlterSyntax::alter_table_extended`] (SQLite keeps this guard on for its
2388    /// `DROP … IF EXISTS` while its plain-`ALTER` surface stays off).
2389    pub if_exists: bool,
2390    /// Accept `IF NOT EXISTS` on a *plain* (non-materialized) `CREATE VIEW` (SQLite).
2391    /// PostgreSQL admits `IF NOT EXISTS` only on a `CREATE MATERIALIZED VIEW` (that
2392    /// form is always accepted, independent of this flag), and MySQL has no view
2393    /// existence guard at all, so off there; SQLite spells `CREATE VIEW IF NOT EXISTS`
2394    /// over a regular view. When off, the `IF NOT EXISTS` is left unconsumed on a plain
2395    /// view and surfaces as a clean parse error.
2396    pub view_if_not_exists: bool,
2397    /// Accept the `CREATE DATABASE IF NOT EXISTS <name>` existence guard (MySQL; also
2398    /// SQLite has no `CREATE DATABASE` at all, so off there). PostgreSQL's `CREATE
2399    /// DATABASE` has no `IF NOT EXISTS`, so this is a dedicated site rather than a reuse
2400    /// of [`if_exists`](Self::if_exists) — that one is on in PostgreSQL, which must still
2401    /// reject `CREATE DATABASE IF NOT EXISTS`. When off, the `IF NOT EXISTS` is left
2402    /// unconsumed and surfaces as a clean parse error.
2403    pub create_database_if_not_exists: bool,
2404}
2405
2406/// Dialect-owned SELECT-core syntax extensions accepted by the parser.
2407///
2408/// The SELECT-body forms — the projection / select-list, the set-operation-operand, and
2409/// the `VALUES`-constructor surface — after the row-limiting/locking query tail and the
2410/// grouping/ordering clauses split out into [`QueryTailSyntax`] and [`GroupingSyntax`] at
2411/// this struct's 16-field line. Acceptance is explicit dialect data, not a parser-side type
2412/// check: most flags are widening grammar gates (when off, the introducing keyword is left
2413/// unconsumed and the clause surfaces as a clean parse error), and a few are narrowing
2414/// well-formedness enforcements (per the flag-naming rule above).
2415#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2416pub struct SelectSyntax {
2417    /// Accept PostgreSQL `SELECT DISTINCT ON (<expr>, ...)`.
2418    pub distinct_on: bool,
2419    /// Accept PostgreSQL's `SELECT … INTO [TEMP] <table>` create-table form (the
2420    /// `INTO` target sits between the projection and `FROM`, materializing the result
2421    /// into a new relation). When off, `INTO` is left unconsumed and surfaces as a
2422    /// clean parse error. This gates *only* the create-table form: bare standard
2423    /// `SELECT … INTO <variable>` is PSM host/local-variable assignment — a different
2424    /// construct — so ANSI leaves this off, and MySQL has no `SELECT INTO <table>`.
2425    pub select_into: bool,
2426    /// Accept an empty SELECT target list — a projection with zero items (`SELECT`,
2427    /// `SELECT;`, `SELECT FROM t`, `SELECT WHERE …`). libpg_query's raw grammar makes
2428    /// the projection optional before any clause (PostgreSQL rejects it later, at
2429    /// parse-analysis, which is past our parse-level parity contract), so the honest
2430    /// closure accepts it under the PostgreSQL preset only. When off (ANSI/MySQL, which
2431    /// both require ≥1 select item), the projection's first-item requirement stands and
2432    /// a bare `SELECT` is a clean parse error.
2433    pub empty_target_list: bool,
2434    /// Accept DuckDB's `QUALIFY <predicate>` post-window filter clause, written after
2435    /// the `WINDOW` clause (DuckDB's grammar order: `… HAVING … WINDOW … QUALIFY …`;
2436    /// verified against DuckDB 1.5.4). When off (ANSI/PostgreSQL/MySQL/SQLite, none of
2437    /// which have the clause), the `QUALIFY` keyword is left unconsumed and surfaces
2438    /// as a clean parse error — the same reject mechanism the other SELECT gates use.
2439    /// This flag gates only the *clause*; whether `QUALIFY` is usable as a plain
2440    /// identifier is the orthogonal reservation data (the `reserved_*` keyword sets:
2441    /// DuckDB reserves it like `HAVING`, every other shipped dialect leaves it a free
2442    /// identifier). On for DuckDB / Lenient, off elsewhere.
2443    pub qualify: bool,
2444    /// Accept a string literal as a column alias (`SELECT 1 AS 'x'`, and MySQL's
2445    /// `SELECT 1 AS "x"` where `"…"` is a string), MySQL's rule. The alias parser admits
2446    /// a `String` token after `AS`, materialises its value as the alias identifier, and
2447    /// records the source quote ([`QuoteStyle::Single`](crate::ast::QuoteStyle::Single) /
2448    /// [`Double`](crate::ast::QuoteStyle::Double)) so it renders back quoted. When off,
2449    /// a string in alias position is left unconsumed and surfaces as a clean parse error
2450    /// (the standard requires an identifier). On for MySQL / Lenient, off elsewhere.
2451    pub alias_string_literals: bool,
2452    /// Accept a string literal as a *bare* (`AS`-less) column alias (`SELECT 1 'x'`), on
2453    /// top of the `AS`-introduced form [`alias_string_literals`](Self::alias_string_literals)
2454    /// gates. SQLite and MySQL read a string in bare-alias position as the column name
2455    /// (engine-measured: `SELECT 1 'x'` prepares on both, naming the column `x`), while
2456    /// **DuckDB accepts only the `AS 'x'` form and rejects the bare `SELECT 1 'x'`** (probed
2457    /// on 1.5.4) — so the bare position is its own axis rather than a rider on
2458    /// [`alias_string_literals`](Self::alias_string_literals). A separate axis rather than
2459    /// widening that flag because the two enablers split: DuckDB arms the `AS` form here but
2460    /// not the bare one. When off, a string in bare-alias position is left unconsumed and
2461    /// surfaces as a clean parse error. On for SQLite / MySQL / Lenient, off elsewhere.
2462    ///
2463    /// MySQL's bare string alias overlaps same-line adjacent-string concatenation
2464    /// ([`same_line_adjacent_concat`](StringLiteralSyntax::same_line_adjacent_concat)):
2465    /// `SELECT 'a' 'b'` is the single value `'ab'`, while `SELECT 1 'x'` is a bare alias
2466    /// (both engine-measured on mysql:8.4.10). No carve-out flag is needed — parse ordering
2467    /// resolves it: a string primary greedily folds every following unprefixed string
2468    /// continuation into its own value before the alias parser runs, so a trailing string
2469    /// reaches the bare-alias branch only when the preceding expression was not itself a
2470    /// string. A prefixed string (`N'…'`, `_charset'…'`, bit) is never a bare alias
2471    /// (rejected as an identifier), matching the engine's rejects (`SELECT 'a' _utf8'b'`).
2472    pub bare_alias_string_literals: bool,
2473    /// Accept DuckDB's FROM-first SELECT order: a query primary may lead with the
2474    /// `FROM` clause (`FROM <tables> [SELECT [DISTINCT] <projection>] …`), the projection
2475    /// written after it, or omitted entirely — the bare `FROM <tables>` is an implicit
2476    /// `SELECT *`. Semantically the ordinary SELECT (DuckDB serializes `FROM t SELECT x`
2477    /// and `SELECT x FROM t` to the same tree; probed on 1.5.4), so it parses to the
2478    /// canonical [`Select`](crate::ast::Select) tagged
2479    /// [`SelectSpelling::FromFirst`](crate::ast::SelectSpelling). The projection, when
2480    /// present, must sit immediately after the `FROM` clause (DuckDB syntax-errors on
2481    /// `FROM t WHERE x SELECT y` / `FROM t GROUP BY a SELECT a`), so it is parsed only in
2482    /// that position and every following clause parses in its ordinary place. When off
2483    /// (ANSI/PostgreSQL/MySQL/SQLite, none of which admit a statement-position `FROM`), a
2484    /// leading `FROM` is never a query start, so it surfaces as a clean parse error — the
2485    /// over-acceptance guard the differential oracle relies on. The gate is read wherever
2486    /// a query primary may begin (statement, set operand, scalar/`IN` subquery, CTE body,
2487    /// derived table), so the one flag composes everywhere. On for DuckDB / Lenient, off
2488    /// elsewhere.
2489    pub from_first: bool,
2490    /// Accept DuckDB's `UNION [ALL] BY NAME` name-matched set operation: pair the two
2491    /// inputs' columns by name (padding missing columns with NULL) instead of by
2492    /// position ([`SetExpr::SetOperation::by_name`](crate::ast::SetExpr) — a flag on
2493    /// the set-op node, orthogonal to `all`). DuckDB restricts `BY NAME` to `UNION`
2494    /// (`INTERSECT BY NAME` / `EXCEPT BY NAME` are syntax errors; probed on 1.5.4), so
2495    /// the parser consumes the modifier only after `UNION`; after `INTERSECT`/`EXCEPT`
2496    /// the `BY` keyword is left unconsumed and surfaces as the usual operand reject.
2497    /// When off (ANSI/PostgreSQL/MySQL/SQLite, none of which have the modifier), the
2498    /// `BY` after a set operator is likewise left unconsumed and a bare `UNION BY NAME`
2499    /// is a clean parse error — the same reject mechanism the other SELECT gates use.
2500    /// On for DuckDB / Lenient, off elsewhere.
2501    pub union_by_name: bool,
2502    /// Accept DuckDB's `*` / `t.*` wildcard modifiers — the `EXCLUDE (…)`,
2503    /// `REPLACE (expr AS col)`, and `RENAME (col AS new)` tail that rewrites which
2504    /// columns the wildcard expands to ([`WildcardOptions`](crate::ast::WildcardOptions)
2505    /// on the wildcard select item). DuckDB fixes their surface order (`EXCLUDE`, then
2506    /// `REPLACE`, then `RENAME`, each at most once; any other order is a syntax error,
2507    /// probed on 1.5.4), which the parser enforces by parsing them in that sequence.
2508    /// One flag, not three: DuckDB ships the trio as a single grammar production and no
2509    /// shipped dialect adopts a subset (the paired-flag doctrine). This gates the
2510    /// select-list/`RETURNING` wildcard tail; the sibling `COLUMNS(…)` expression form
2511    /// is [`CallSyntax::columns_expression`](CallSyntax::columns_expression),
2512    /// a separate grammar position (expression vs projection item). When off
2513    /// (ANSI/PostgreSQL/MySQL/SQLite), the `EXCLUDE`/`REPLACE`/`RENAME` keyword after a
2514    /// `*` is left unconsumed and surfaces as a clean parse error — the over-acceptance
2515    /// guard the differential oracle relies on. On for DuckDB / Lenient, off elsewhere.
2516    pub wildcard_modifiers: bool,
2517    /// Accept a trailing `[AS] alias` on a *qualified* wildcard select item (`SELECT t.* x`,
2518    /// `SELECT t.* AS x`, `SELECT s.t.* x`), folding it onto the
2519    /// [`QualifiedWildcard`](crate::ast::SelectItem::QualifiedWildcard) item's `alias` slot
2520    /// with the source [`AliasSpelling`](crate::ast::AliasSpelling) (bare vs `AS`).
2521    ///
2522    /// PostgreSQL treats `t.*` as an ordinary column-reference expression (`columnref`, an
2523    /// `a_expr`), so it flows through the very same `target_el: a_expr [AS] label` projection
2524    /// alias an ordinary value takes: the bare form admits the `BareColLabel` reserved-word
2525    /// set (`SELECT t.* select` parses, `SELECT t.* from`/`order` reject) and the `AS` form the
2526    /// full `ColLabel` set (`SELECT t.* AS from` parses) — measured against libpg_query, which
2527    /// matches the parser's ordinary projection-alias boundary exactly, so the alias is parsed
2528    /// by reusing it. The bare `*` wildcard is the *separate* non-aliasable `target_el:
2529    /// '*'` production, so this gate never touches it (a bare-`*` alias is DuckDB's rename-all,
2530    /// which rides [`wildcard_modifiers`](Self::wildcard_modifiers)).
2531    ///
2532    /// Deliberately its own axis, *not* folded into [`wildcard_modifiers`](Self::wildcard_modifiers):
2533    /// the two behaviours have different measured boundaries — the `EXCLUDE`/`REPLACE`/`RENAME`
2534    /// modifier tail and the bare-`*` rename-all alias are DuckDB-only, whereas a qualified
2535    /// wildcard's plain alias is accepted by **PostgreSQL and DuckDB** (engine-probed: PG's
2536    /// libpg_query and DuckDB 1.5.4 accept `t.* x`/`t.* AS x`, while SQLite/MySQL and the SQL
2537    /// standard special-case `t.*` as a non-aliasable wildcard production and reject it —
2538    /// measured Reject on rusqlite/mysql:8 with the table provisioned). One behaviour = one flag.
2539    /// When off (ANSI/MySQL/SQLite and the ANSI-derived presets), the alias word is left
2540    /// unconsumed after `t.*` and surfaces as a clean parse error. On for PostgreSQL / DuckDB /
2541    /// Lenient, off elsewhere.
2542    pub qualified_wildcard_alias: bool,
2543    /// Accept a *parenthesized query* as a set-operation / statement / CTE-body / CTAS /
2544    /// `INSERT`-source operand — `(SELECT …) UNION (SELECT …)`, `((SELECT …)) LIMIT 1`,
2545    /// `CREATE TABLE t AS (SELECT …)` (PostgreSQL `select_with_parens`; the canonical
2546    /// set-op AST). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no
2547    /// parenthesized compound operand — a `select-core` is `SELECT …`/`VALUES …`, never
2548    /// `( … )` — so it is off; a leading `(` in operand position is then a syntax error
2549    /// (engine-measured via rusqlite). The parenthesized query that SQLite *does* admit —
2550    /// a `FROM` table-or-subquery grouping (`FROM ((SELECT 1))`) and an expression-position
2551    /// scalar subquery (`SELECT ((SELECT 1))`) — is a complete standalone primary, not an
2552    /// operand, and stays accepted with the flag off (the parser threads a grouping
2553    /// context through those two positions); a paren-query there that is *extended* by a
2554    /// set operator or an `ORDER BY`/`LIMIT` tail (`FROM ((SELECT 1) UNION (SELECT 2))`,
2555    /// `FROM ((SELECT 1) LIMIT 1)`) is the syntax error SQLite reports.
2556    pub parenthesized_query_operands: bool,
2557    /// Reject a `VALUES` table-value constructor whose rows differ in width
2558    /// (`VALUES (1, 2), (3)`) at *parse* time. Unlike the additive gates above, this is a
2559    /// well-formedness *enforcement*: on rejects the ragged constructor, off accepts it.
2560    ///
2561    /// Equal row degree is a universal SQL rule, but engines check it at different phases,
2562    /// and this validator models per-dialect *parse* acceptance. DuckDB checks at parse —
2563    /// `Parser Error: VALUES lists must all be the same length`, in every VALUES position
2564    /// (standalone, derived table, `INSERT`; measured on 1.5.4) — so the DuckDb preset
2565    /// turns it on. PostgreSQL's raw grammar (libpg_query) and MySQL accept a ragged
2566    /// constructor and defer the arity check to parse-analysis / bind, past our parse-level
2567    /// parity contract, so it stays off there (their presets keep accepting it, exactly as
2568    /// `empty_target_list` accepts a bare `SELECT` under the PostgreSQL preset). `LENIENT`
2569    /// is a pure-acceptance superset, so it also leaves this off. Both counts the check
2570    /// compares — the two rows' arities — are present in the parse tree, so this is a
2571    /// shape-level gate, not a semantic one. On for DuckDB only; off elsewhere.
2572    pub values_rows_require_equal_arity: bool,
2573    /// Accept a bare-parenthesized row (`VALUES (1), (2)`) as a `VALUES` table-value
2574    /// constructor in *query* position — a top-level query body, a set-operation operand,
2575    /// a CTE body, or a derived table. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient. MySQL
2576    /// spells the query-position constructor `VALUES ROW(1), ROW(2)` (the `TABLE`/`VALUES`
2577    /// row-constructor grammar): a bare `(…)` row there is the syntax error MySQL reports
2578    /// (engine-measured on mysql:8 — `VALUES (1)` / `SELECT … FROM (VALUES (1)) …` /
2579    /// `VALUES (1) UNION …` all `ER_PARSE_ERROR`), so it is off there. Scoped to query
2580    /// position only: the `INSERT … VALUES (…)` source list is a distinct grammar that
2581    /// admits bare rows on every dialect (MySQL included), so it is parsed on a separate
2582    /// path this gate does not touch.
2583    pub values_row_constructor: bool,
2584    /// Reject a reserved word as an `AS`-introduced *projection* alias
2585    /// (`SELECT 1 AS <label>`), routing the position to the stricter
2586    /// [`reserved_bare_alias`](FeatureSet::reserved_bare_alias) set instead of the
2587    /// permissive [`reserved_as_label`](FeatureSet::reserved_as_label). Off for
2588    /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient, whose projection `AS` alias is a
2589    /// PostgreSQL-style `ColLabel`: PostgreSQL admits every keyword there
2590    /// (`SELECT a AS select` parses) via its empty `reserved_as_label`, and SQLite draws
2591    /// no `ColId`/`ColLabel` split so its non-empty `reserved_as_label` already rejects
2592    /// them — neither needs this gate. MySQL has no `ColLabel` relaxation: an `AS` alias
2593    /// rejects exactly the words a *bare* alias does (`SELECT 1 AS range`/`AS left`/`AS
2594    /// delete` are `ER_PARSE_ERROR` on mysql:8, while the non-reserved `SELECT 1 AS any`
2595    /// parses), so it is on there. Scoped to the projection `AS` alias only: the
2596    /// dotted-name continuation (`t.range`, a qualified-name label MySQL admits) is a
2597    /// separate position that keeps the permissive `reserved_as_label` set.
2598    pub as_alias_rejects_reserved: bool,
2599    /// Accept a single trailing comma before a list's closing delimiter in the list
2600    /// positions DuckDB tolerates it: the `SELECT` projection list (`SELECT a, b, FROM
2601    /// t`), the query-position and `INSERT` `VALUES` row lists and each parenthesized
2602    /// row (`VALUES (1), (2),` / `VALUES (1, 2,)`), the `[…]` / `ARRAY[…]` list and
2603    /// `{…}` struct and `MAP {…}` collection literals, the `IN (…)` list, the `GROUP BY`
2604    /// key list and its `ROLLUP(…)` / `CUBE(…)` / `GROUPING SETS (…)` sub-lists
2605    /// (`GROUP BY a, b,` / `GROUP BY ROLLUP(a, b,)`), the wildcard-modifier lists
2606    /// (`* EXCLUDE (a,)`, `* REPLACE (e AS c,)`, `* RENAME (a AS b,)`), the
2607    /// `COALESCE` special-form argument list (`coalesce(1, 2,)`), and the
2608    /// `CREATE TABLE` table-element list (`CREATE TABLE t (a INT, b INT,)`), after a
2609    /// column or a constraint element alike. The comma is
2610    /// discarded — the list shape is unchanged, so no AST node carries it and the render
2611    /// drops it (canonical form, a lossy-spelling trade); it is not semantically
2612    /// meaningful.
2613    ///
2614    /// Scoped to those list sites only, because DuckDB is *not* uniform: engine-probed
2615    /// (1.5.4), an ordinary function-argument list (`greatest(1, 2,)`), a bare
2616    /// parenthesized / row constructor (`(1, 2,)`), an `ORDER BY` / `PARTITION BY` list,
2617    /// and an `INSERT` *column* list (`INSERT INTO t (a, b,) …`) all reject the trailing
2618    /// comma, so this gate is applied per accepting list site rather than in the shared
2619    /// `parse_comma_separated`. `COALESCE` is the lone function-call exception —
2620    /// `coalesce(1, 2,)` accepts because DuckDB parses it as a grammar special form, not
2621    /// a `func_application`, whereas every sibling (`greatest`/`least`/`nullif`/`concat`)
2622    /// keeps rejecting the comma. Only a *single* trailing comma is admitted (`[1, 2, ,]`
2623    /// and a leading `[,]` stay parse errors). When off (ANSI/PostgreSQL/MySQL/SQLite,
2624    /// none of which admit a trailing comma), the dangling comma is left for the item
2625    /// parser to reject — the same clean parse error those dialects report. On for DuckDB
2626    /// / Lenient, off elsewhere.
2627    pub trailing_comma: bool,
2628    /// Accept DuckDB's prefix colon alias — an alias written *before* its value as
2629    /// `<alias> : <value>`, in two positions: a projection item (`SELECT j : 42`, the
2630    /// alias `j` on the value `42`) and a `FROM` table factor (`FROM b : a`, the alias
2631    /// `b` on the relation `a` — the alias precedes the table). It is pure sugar for the
2632    /// standard trailing `AS` alias: DuckDB records it in the ordinary alias field with
2633    /// no spelling tag and canonically re-emits it as `AS` (json round-trip on 1.5.4:
2634    /// `SELECT j : 42` → `SELECT 42 AS j`, `FROM b : a` → `FROM a AS b`), so it folds
2635    /// onto the existing [`SelectItem::Expr`](crate::ast::SelectItem) /
2636    /// [`TableFactor`](crate::ast::TableFactor) alias field and renders as `AS` — never a
2637    /// new node. The `<alias>` is a single bare (`BareColLabel`) or quoted identifier: a
2638    /// qualified `a.b :`, a call `f() :`, and a reserved word are all rejected, and the
2639    /// prefix is mutually exclusive with a trailing alias (`SELECT x : 42 AS y` /
2640    /// `FROM b : a c` reject — the value's own alias parse is suppressed once a prefix is
2641    /// read, so a trailing alias is left unconsumed and rejects).
2642    ///
2643    /// This is a *grammar-position* gate, not a lexical trigger: a lone `:` always
2644    /// tokenizes as a `Colon` punctuation token, and reading it as an alias separator at a
2645    /// select-item / table-factor head is a parser decision (like slice `a[x:y]` and struct
2646    /// `{a: b}` are parser readings of the same byte), so no [`LexicalConflict`] variant
2647    /// governs it. The one genuine hazard is a *parser-position* collision with
2648    /// [`ExpressionSyntax::semi_structured_access`] — a top-level `base : key` path would
2649    /// read the same `<ident> :` head — so the two must not be enabled together
2650    /// ([`GrammarConflict::PrefixColonAliasVersusSemiStructuredAccess`]). On for DuckDB and
2651    /// Lenient, both of which leave `semi_structured_access` off, so no shipped preset has the
2652    /// collision; a future dialect enabling both would silently mis-read one — the hazard the
2653    /// tokenizer-trigger-only lexical registry cannot catch, now tracked by the grammar
2654    /// registry.
2655    pub prefix_colon_alias: bool,
2656    /// Accept the Hive/Spark `LATERAL VIEW [OUTER] <generator>(args) <alias>
2657    /// [AS <col> [, …]]` table-generating clauses, written after the whole `FROM`
2658    /// clause and before `WHERE` and repeatable (Hive LanguageManual LateralView:
2659    /// `fromClause: FROM baseTable (lateralView)*`; Spark `SqlBaseParser.g4` places
2660    /// `lateralView*` at the same fromClause tail), modelled as
2661    /// [`Select::lateral_views`](crate::ast::Select) (see
2662    /// [`LateralView`](crate::ast::LateralView) for the typed shape, the sqlparser-rs
2663    /// parity reshapings, and the recorded acceptance bound — there is no Hive/Spark
2664    /// oracle, so the two engines' published grammars are the acceptance evidence).
2665    ///
2666    /// **Leading-keyword dispatch, position- and follow-token-partitioned against
2667    /// LATERAL derived tables.** `LATERAL` also introduces the standard derived-table /
2668    /// function factor ([`TableFactorSyntax::lateral`]). The two occupy disjoint
2669    /// grammar positions — a table-factor head (after `FROM`/`,`/a join keyword) versus
2670    /// after the *complete* FROM relation list — and split on the follow token (`VIEW`
2671    /// here; `(` or a function/subquery head there), so the dispatch is unambiguous
2672    /// under every preset combination and needs no [`GrammarConflict`] entry: this
2673    /// clause claims a `LATERAL` only when `VIEW` follows and only once the FROM list
2674    /// is complete, a cursor position the factor grammar never holds.
2675    ///
2676    /// **On for Hive, Databricks, and Lenient.** Hive and Databricks have no
2677    /// differential oracle, so this is a no-oracle acceptance addition carried by the
2678    /// two conservative presets and the permissive union: every oracle-compared dialect
2679    /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a
2680    /// post-FROM `LATERAL` as unconsumed input — so conformance sweeps see zero
2681    /// movement.
2682    pub lateral_view_clause: bool,
2683    /// Accept the Oracle-style `[START WITH <cond>] CONNECT BY [NOCYCLE] <cond>`
2684    /// hierarchical query clause, written **after `WHERE` and before `GROUP BY`** with
2685    /// `START WITH` and `CONNECT BY` in either order, modelled as
2686    /// [`Select::connect_by`](crate::ast::Select) (see
2687    /// [`HierarchicalClause`](crate::ast::HierarchicalClause) for the typed shape, the
2688    /// either-order fidelity tag, and the recorded acceptance bounds). The clause also
2689    /// enables the [`UnaryOperator::Prior`](crate::ast::UnaryOperator) operator, but only
2690    /// inside the `CONNECT BY` condition — the global expression grammar is unchanged, so
2691    /// a bare `prior` stays an ordinary column name.
2692    ///
2693    /// **Grammar evidence (no Oracle/Snowflake oracle).** Snowflake's public docs are the
2694    /// citable grammar (there is no Oracle preset): they document
2695    /// `START WITH … CONNECT BY [PRIOR] col = [PRIOR] col`. Oracle contributes the
2696    /// either-order rule, the after-`WHERE` position, and `NOCYCLE` (which Snowflake's
2697    /// docs explicitly omit) — modelling the Oracle superset is a documented
2698    /// conservative-direction over-acceptance, captured on the owning ticket.
2699    ///
2700    /// **On for Snowflake and Lenient.** Snowflake has no differential oracle, so this is
2701    /// a no-oracle acceptance addition carried by the Snowflake preset and the permissive
2702    /// union. Databricks/Spark does *not* get it: Databricks documents recursive CTEs
2703    /// (`WITH RECURSIVE`) instead of `CONNECT BY` and does not support the Oracle
2704    /// `CONNECT BY … START WITH` syntax (Databricks SQL reference), so it stays off there
2705    /// alongside every oracle-compared dialect (ANSI/PostgreSQL/MySQL/SQLite/DuckDB),
2706    /// which parse-reject a post-`WHERE` `CONNECT BY`/`START WITH` as unconsumed input —
2707    /// so conformance sweeps see zero movement.
2708    pub connect_by_clause: bool,
2709}
2710
2711/// Dialect-owned query-tail syntax accepted by the parser.
2712///
2713/// The clauses parsed by the query-tail sequence that runs after the SELECT body. Split
2714/// out of [`SelectSyntax`] at its 16-field line as the query-tail axis, distinct from the
2715/// SELECT-core and grouping axes. Each flag is a grammar gate: when off the introducing
2716/// keyword is left unconsumed and the clause surfaces as a clean parse error.
2717#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2718pub struct QueryTailSyntax {
2719    /// Accept the standard `OFFSET <n> { ROW | ROWS }` and `FETCH { FIRST | NEXT }
2720    /// <count> { ROW | ROWS } ONLY` row-limiting spelling.
2721    pub fetch_first: bool,
2722    /// Accept the MySQL/MariaDB/SQLite `LIMIT <offset>, <count>` two-argument
2723    /// comma form. It is a *spelling* of the same row limit as
2724    /// `LIMIT <count> OFFSET <offset>`, so it folds into the one canonical
2725    /// [`Limit`](crate::ast::Limit) shape tagged
2726    /// [`LimitSyntax::LimitOffset`](crate::ast::LimitSyntax) — never a
2727    /// new node. The comma binds the offset *first*, the count second (the reverse
2728    /// of `LIMIT <count> OFFSET <offset>`), which is exactly why it must be dialect
2729    /// data: reading the arguments in the wrong order silently swaps them.
2730    pub limit_offset_comma: bool,
2731    /// Accept a trailing row-locking clause (`FOR UPDATE`/`FOR SHARE [OF …]
2732    /// [NOWAIT|SKIP LOCKED]`, plus MySQL's legacy `LOCK IN SHARE MODE`), written after
2733    /// `LIMIT`. PostgreSQL and MySQL share this modern surface, so it folds into one
2734    /// canonical [`LockingClause`](crate::ast::LockingClause) on the query,
2735    /// gated here. When off (ANSI/SQLite/DuckDB, none of which admit a query-tail lock
2736    /// clause), the `FOR`/`LOCK` keyword is left unconsumed and surfaces as a clean
2737    /// parse error — the same reject mechanism the other query-tail gates use. On for
2738    /// PostgreSQL / MySQL / Lenient, off elsewhere. The gate covers the shared modern
2739    /// surface — a single `FOR UPDATE`/`FOR SHARE` clause with an `OF <table>, …` list
2740    /// and a wait tail. PostgreSQL's `NO KEY UPDATE`/`KEY SHARE` strengths and its
2741    /// stacked clauses ride the further [`key_lock_strengths`](QueryTailSyntax::key_lock_strengths)
2742    /// and [`stacked_locking_clauses`](QueryTailSyntax::stacked_locking_clauses) gates; the clause
2743    /// *before* `LIMIT` is still deferred.
2744    pub locking_clauses: bool,
2745    /// Accept PostgreSQL's `FOR NO KEY UPDATE` / `FOR KEY SHARE` row-locking strengths —
2746    /// the two levels between `FOR UPDATE` and `FOR SHARE`
2747    /// ([`LockStrength::NoKeyUpdate`](crate::ast::LockStrength) /
2748    /// [`LockStrength::KeyShare`](crate::ast::LockStrength)). Requires
2749    /// [`locking_clauses`](QueryTailSyntax::locking_clauses) (it refines the strength keyword after
2750    /// `FOR`; the dependency is [`FeatureDependencyViolation::KeyLockStrengthsWithoutLockingClauses`]);
2751    /// when off, the `NO`/`KEY` after `FOR` is never a strength lead, so
2752    /// `FOR NO KEY UPDATE` / `FOR KEY SHARE` surface as clean parse errors — the
2753    /// over-acceptance guard MySQL relies on (its grammar has only `UPDATE`/`SHARE`,
2754    /// engine-verified). PostgreSQL alone spells the `KEY`/`NO KEY` refinements
2755    /// (libpg_query `for_locking_strength`), so this is on for PostgreSQL / Lenient, off
2756    /// elsewhere. `FOR KEY UPDATE` and `FOR NO KEY SHARE` stay rejected under both
2757    /// settings — PostgreSQL pairs `NO KEY` only with `UPDATE` and `KEY` only with
2758    /// `SHARE` (probed on libpg_query, pg-locking-clause-strengths-and-stacking).
2759    pub key_lock_strengths: bool,
2760    /// Accept several *stacked* row-locking clauses on one query
2761    /// (`FOR UPDATE OF a FOR SHARE OF b`) — PostgreSQL applies a distinct lock per table
2762    /// group, so [`Query::locking`](crate::ast::Query) holds a list. Requires
2763    /// [`locking_clauses`](QueryTailSyntax::locking_clauses) (it repeats the shared clause; the
2764    /// dependency is [`FeatureDependencyViolation::StackedLockingClausesWithoutLockingClauses`]);
2765    /// when off, exactly one clause is parsed and any following `FOR`/`LOCK` is left
2766    /// unconsumed, surfacing as a trailing-input parse error — the over-acceptance guard
2767    /// MySQL relies on (its grammar admits exactly one locking clause, engine-verified by
2768    /// `mysql-select-tails-locking-hints-partition`). On for PostgreSQL / Lenient, off
2769    /// elsewhere.
2770    pub stacked_locking_clauses: bool,
2771    /// Accept DuckDB's `USING SAMPLE <entry>` query-level sample clause
2772    /// ([`Select::sample`](crate::ast::Select::sample)), written after `QUALIFY` and before
2773    /// the enclosing query's `ORDER BY` (`SELECT … USING SAMPLE 3 ORDER BY …`). The entry is
2774    /// DuckDB's `tablesample_entry`: a count-first `<size> [ROWS|PERCENT|%] [ '(' method
2775    /// [',' seed] ')' ]` or a method-first `method '(' <size> ')' [REPEATABLE '(' seed ')']`.
2776    /// On for DuckDB / Lenient, off elsewhere; when off the `USING` keyword in that position
2777    /// is left to fail as an unexpected statement token (matching the engines that lack the
2778    /// clause). Distinct from the table-factor `TABLESAMPLE`
2779    /// ([`TableExpressionSyntax::table_sample`](TableExpressionSyntax)).
2780    pub using_sample: bool,
2781    /// Accept a *leading* `OFFSET <count> [LIMIT <count>]` row-skip written without a
2782    /// preceding `LIMIT` (PostgreSQL's `[LIMIT …] [OFFSET …]` where either may come
2783    /// first). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite spells the skip only as
2784    /// `LIMIT <count> OFFSET <count>` / `LIMIT <offset>, <count>` — a bare `OFFSET` with no
2785    /// `LIMIT` is a syntax error there — so it is off; the `OFFSET` keyword is then left
2786    /// unconsumed and surfaces as a clean parse error. The `OFFSET` that trails a `LIMIT`
2787    /// is unaffected (parsed by the `LIMIT` branch).
2788    pub leading_offset: bool,
2789    /// Accept an arbitrary *expression* as a `LIMIT`/`OFFSET` row count. On for
2790    /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient, whose row limits admit a general expression
2791    /// (`LIMIT 1 + 1`, `LIMIT (SELECT n FROM cfg)`). MySQL restricts the count to an
2792    /// unsigned integer literal or a `?` placeholder (its `limit_clause` grammar), so it is
2793    /// off there: an operand that parses to anything else is the syntax error MySQL reports
2794    /// (engine-measured-rejected on mysql:8 — `LIMIT 1 + 1` / `LIMIT (SELECT 1)`). The whole
2795    /// operand is still parsed, then rejected on shape, so the diagnostic points at it. The
2796    /// plain `LIMIT <int>` / `LIMIT ?` / comma / `OFFSET <int>` forms are unaffected.
2797    pub limit_expressions: bool,
2798    /// Accept DuckDB's percentage `LIMIT` — a numeric-literal count directly followed by
2799    /// a `%` operator or the `PERCENT` keyword (`LIMIT 40 PERCENT`, `LIMIT 35%`), which
2800    /// returns that fraction of the result rows rather than a row number. On for
2801    /// DuckDB/Lenient only; off for ANSI/PostgreSQL/MySQL/SQLite, whose `LIMIT` has no
2802    /// percentage form. With the flag off the marker is left unconsumed — a `%` folds as
2803    /// modulo (needing a right operand) and a trailing `PERCENT` is leftover input — so
2804    /// both surface as the parse error those dialects report. DuckDB folds the marker
2805    /// only onto a bare numeric literal at a clause boundary: `LIMIT 10 % 3` stays
2806    /// ordinary modulo, and `LIMIT a PERCENT` / `LIMIT (1 + 1) PERCENT` are DuckDB syntax
2807    /// errors (verified on 1.5.4), so the gate does not over-accept them.
2808    pub limit_percent: bool,
2809    /// Enforce PostgreSQL's `gram.y` validity checks on `FETCH FIRST … WITH TIES` — the two
2810    /// semantic guards `insertSelectOptions` raises during raw parsing (so `pg_query`, a
2811    /// parse-only oracle, rejects them): `WITH TIES` requires a governing `ORDER BY` at the
2812    /// same query level (`WITH TIES cannot be specified without ORDER BY clause`), and
2813    /// `WITH TIES` cannot combine with a `SKIP LOCKED` locking clause (`SKIP LOCKED and
2814    /// WITH TIES options cannot be used together`). On for PostgreSQL only; other dialects
2815    /// with `fetch_first` keep accepting both forms unchanged. When off, neither guard fires.
2816    /// The name carries the load-bearing `ORDER BY` guard; the `SKIP LOCKED` combination
2817    /// guard is the same `gram.y` check site and rides along.
2818    pub with_ties_requires_order_by: bool,
2819    /// Accept BigQuery/ZetaSQL **query pipe syntax**: a trailing chain of `|>` operators
2820    /// that post-process a query result one step at a time
2821    /// (`FROM t |> WHERE x |> SELECT a`), modelled as
2822    /// [`Query::pipe_operators`](crate::ast::Query::pipe_operators). This gate does double
2823    /// duty — it is read by the *tokenizer* to munch `|>` (pipe-arrow) as a single token
2824    /// (off, the two bytes stay `|` then `>`, so no dialect's lexing shifts) and by the
2825    /// *parser* to admit the trailing `|>`-operator chain. When off (every shipped preset),
2826    /// a `|>` after a query is never a pipe separator and surfaces as a clean parse error,
2827    /// exactly as today.
2828    ///
2829    /// **Off for every shipped preset.** This surface belongs to no engine we oracle
2830    /// against — the BigQuery preset that would home it deliberately defers it (a
2831    /// considered judgment; see that preset's module docs) — so with no differential
2832    /// oracle to verify it, every shipped dialect (including DuckDB, which inherits the
2833    /// PostgreSQL value) leaves it off, and conformance sweeps see zero movement. `LENIENT`
2834    /// leaves it off *for now* as well: although the lenient charter admits any
2835    /// conflict-free pure-acceptance form (and `|>` *is* conflict-free — its munch is
2836    /// feature-gated, shadowing nothing), the framework ships only the reference `WHERE`
2837    /// operator, so enabling it in `LENIENT` today would make the "parse anything" preset
2838    /// accept `|> WHERE` while rejecting every other pipe operator — a fragment a reader of
2839    /// the lenient module could not predict, violating that module's honesty bar. Once the
2840    /// pipe-operator surface is coherent (the `planner-parity-pipe-*` tickets land),
2841    /// `LENIENT` should flip this on as a pure-acceptance addition.
2842    pub pipe_syntax: bool,
2843    /// Accept ClickHouse `LIMIT n [OFFSET m] BY expr, …` — per-group row limiting,
2844    /// written after `ORDER BY` and before the ordinary `LIMIT` tail (both may appear
2845    /// in one query), modelled as [`Query::limit_by`](crate::ast::Query::limit_by). The
2846    /// parser reads a leading `LIMIT` speculatively and treats it as `LIMIT BY` only
2847    /// when a `BY` follows the count and optional `OFFSET`; otherwise it rewinds and the
2848    /// token is the ordinary `LIMIT`, so a plain `LIMIT n` / `LIMIT n OFFSET m` parses
2849    /// identically whether this gate is on or off.
2850    ///
2851    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle,
2852    /// so this is a no-oracle acceptance addition carried by the ClickHouse preset and the
2853    /// permissive union (the `apply_join` precedent): every oracle-compared dialect
2854    /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a `BY`
2855    /// after `LIMIT` — so conformance sweeps see zero movement.
2856    pub limit_by_clause: bool,
2857    /// Accept ClickHouse `SETTINGS name = value, …` — query-level setting overrides
2858    /// written after the ordinary `LIMIT` tail, modelled as
2859    /// [`Query::settings`](crate::ast::Query::settings). `SETTINGS` is matched as a
2860    /// contextual keyword (it stays an ordinary identifier elsewhere); each pair is an
2861    /// identifier `=` value, the value a general expression.
2862    ///
2863    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle,
2864    /// so this is a no-oracle acceptance addition carried by the ClickHouse preset and the
2865    /// permissive union (the `limit_by_clause` precedent): every oracle-compared dialect
2866    /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a trailing
2867    /// `SETTINGS …` as unconsumed input — so conformance sweeps see zero movement.
2868    pub settings_clause: bool,
2869    /// Accept ClickHouse `FORMAT <name>` — the output-format clause that closes the
2870    /// query, modelled as [`Query::format`](crate::ast::Query::format). `FORMAT` is
2871    /// matched as a contextual keyword (it stays an ordinary identifier elsewhere); the
2872    /// format name is a bare, case-sensitive identifier (`JSON`, `TabSeparated`, `Null`),
2873    /// not a string literal.
2874    ///
2875    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle,
2876    /// so this is a no-oracle acceptance addition carried by the ClickHouse preset and the
2877    /// permissive union (the `settings_clause` precedent): every oracle-compared dialect
2878    /// (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves it off — they parse-reject a trailing
2879    /// `FORMAT …` as unconsumed input — so conformance sweeps see zero movement.
2880    pub format_clause: bool,
2881    /// Accept MSSQL's `FOR XML {RAW|AUTO|EXPLICIT|PATH} [, …]` and
2882    /// `FOR JSON {AUTO|PATH} [, …]` result-shaping tails, which serialize the result
2883    /// set as XML/JSON rather than a rowset, modelled as
2884    /// [`Query::for_clause`](crate::ast::Query::for_clause) (see [`ForClause`](crate::ast::ForClause)).
2885    /// The modes and their directives (`ELEMENTS [XSINIL|ABSENT]`, `BINARY BASE64`,
2886    /// `TYPE`, `ROOT ['name']`, `INCLUDE_NULL_VALUES`, `WITHOUT_ARRAY_WRAPPER`) are typed
2887    /// data; the accepted grammar follows the MSSQL `FOR XML` / `FOR JSON`
2888    /// documentation (no MSSQL oracle, so that is the recorded acceptance bound).
2889    ///
2890    /// **Leading-keyword dispatch, follow-token partitioned against locking.** `FOR`
2891    /// also introduces the row-locking clauses ([`locking_clauses`](QueryTailSyntax::locking_clauses)).
2892    /// The two share the `FOR` lead but split on the *follow* token — `XML`/`JSON` here
2893    /// versus `UPDATE`/`SHARE`/`NO`/`KEY` for locking — so the dispatch is unambiguous
2894    /// under every preset combination and needs no
2895    /// [`GrammarConflict`] entry: the locking parser
2896    /// declines a `FOR` whose follow token is `XML`/`JSON` (so a stacked
2897    /// `FOR UPDATE … FOR XML` still reaches this clause), and this clause declines a
2898    /// `FOR` whose follow token is anything else.
2899    ///
2900    /// **On for MSSQL and Lenient.** MSSQL has no differential oracle, so this is a
2901    /// no-oracle acceptance addition carried by the MSSQL preset and the permissive
2902    /// union: every oracle-compared dialect (ANSI/PostgreSQL/MySQL/SQLite/DuckDB) leaves
2903    /// it off — they parse-reject a trailing `FOR XML`/`FOR JSON` as unconsumed input
2904    /// (or, where `locking_clauses` is on, reject `XML`/`JSON` after `FOR`) — so
2905    /// conformance sweeps see zero movement.
2906    pub for_xml_json_clause: bool,
2907}
2908
2909/// Dialect-owned `GROUP BY` / `ORDER BY` grouping-and-ordering syntax accepted by the
2910/// parser.
2911///
2912/// The grouping-set constructs and the clause-level grouping/ordering modes and
2913/// quantifiers. Split out of [`SelectSyntax`] at its 16-field line as the
2914/// grouping/ordering axis, distinct from the SELECT-core and query-tail axes. Each
2915/// flag is a grammar gate: when off the keyword falls through to the ordinary
2916/// expression/grouping-item grammar or surfaces as a clean parse error.
2917#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2918pub struct GroupingSyntax {
2919    /// Accept the SQL:1999 (feature T431) grouping-set constructs as `GROUP BY`
2920    /// items: `ROLLUP (…)`, `CUBE (…)`, `GROUPING SETS (…)`, and the empty grouping
2921    /// set `()`. PostgreSQL lowers these in GROUP BY item position for *any* case
2922    /// spelling — an unquoted `rollup (a, b)` is the grouping construct, never a
2923    /// call to a user function `rollup` (quote it, `"rollup"(a, b)`, to call the
2924    /// function) — so the parser models them as [`GroupByItem`](crate::ast::GroupByItem)
2925    /// nodes, not [`FunctionCall`](crate::ast::FunctionCall) expressions. When off,
2926    /// the keywords fall through to the expression grammar (an ordinary function
2927    /// call), which is how MySQL reads them; MySQL's own grouping surface is the
2928    /// distinct trailing `WITH ROLLUP`, not modelled here. On for ANSI (the T431
2929    /// standard) / PostgreSQL / Lenient, off for MySQL.
2930    pub grouping_sets: bool,
2931    /// Accept MySQL's trailing `GROUP BY <keys> WITH ROLLUP` modifier — MySQL's only
2932    /// grouping-set surface (it has no SQL:1999 `ROLLUP (…)` item form). It is a
2933    /// *spelling* of the same super-aggregate as `ROLLUP (…)`, so it canonicalizes
2934    /// into the one [`GroupByItem::Rollup`](crate::ast::GroupByItem) shape tagged
2935    /// [`RollupSpelling::WithRollup`](crate::ast::RollupSpelling) — never a
2936    /// new node. When off, the trailing `WITH ROLLUP` is left unconsumed and surfaces
2937    /// as a clean parse error; PostgreSQL/ANSI spell the construct `ROLLUP (…)`, so
2938    /// accepting `WITH ROLLUP` there would be an over-acceptance. On for MySQL /
2939    /// Lenient, off elsewhere. (MySQL 8.0.1+ also permits `WITH ROLLUP` alongside
2940    /// `ORDER BY`; older versions did not — a version wrinkle we do not model.)
2941    pub with_rollup: bool,
2942    /// Accept PostgreSQL's `ORDER BY <expr> USING <operator>` sort form (`gram.y`
2943    /// `sortby: a_expr USING qual_all_Op opt_nulls_order`), which sorts by a named
2944    /// ordering operator (`USING <`, `USING OPERATOR(schema.op)`) instead of
2945    /// `ASC`/`DESC`. PostgreSQL-only; ANSI and MySQL have only `ASC`/`DESC`, so there
2946    /// the `USING` keyword is left unconsumed and surfaces as a trailing-input parse
2947    /// error — the same reject mechanism the other unsupported clauses use.
2948    pub order_by_using: bool,
2949    /// Accept DuckDB's `GROUP BY ALL` clause mode: group by every non-aggregated
2950    /// projection column, resolved at bind time
2951    /// ([`Select::group_by_all`](crate::ast::Select) — a mode with an empty key
2952    /// list, never a [`GroupByItem`](crate::ast::GroupByItem)). `ALL` cannot mix
2953    /// with explicit keys or grouping sets (DuckDB syntax-errors on `GROUP BY ALL,
2954    /// x` and `GROUP BY ROLLUP(x), ALL`; probed on 1.5.4), so the branch consumes
2955    /// exactly the one keyword. When off, `ALL` after `GROUP BY` falls through to
2956    /// the expression grammar, where every shipped dialect reserves it — a clean
2957    /// parse error. A *separate* flag from [`order_by_all`](GroupingSyntax::order_by_all),
2958    /// not one paired gate: the paired-flag doctrine (the `straight_join` /
2959    /// `table_options` precedent) covers grammar points a dialect only ever ships
2960    /// together, but these are two independent constructs on two clauses that real
2961    /// engines adopt separately (Snowflake ships `GROUP BY ALL` with no
2962    /// `ORDER BY ALL`), so pairing them would bake a DuckDB coincidence into the
2963    /// vocabulary. On for DuckDB / Lenient, off elsewhere.
2964    pub group_by_all: bool,
2965    /// Accept PostgreSQL's `GROUP BY {DISTINCT | ALL} <grouping items>` set-quantifier
2966    /// (SQL:2016 feature T434): a `DISTINCT`/`ALL` prefix on the whole grouping clause
2967    /// that governs deduplication of the generated grouping sets
2968    /// ([`Select::group_by_quantifier`](crate::ast::Select)). The quantifier requires a
2969    /// non-empty grouping list — PostgreSQL rejects a bare `GROUP BY ALL` /
2970    /// `GROUP BY DISTINCT` (probed on pg_query PG-17) — which is what keeps it MECE with
2971    /// [`group_by_all`](GroupingSyntax::group_by_all), DuckDB's mode where `ALL` *is* the entire
2972    /// clause. A *separate* flag from `group_by_all` for that reason: the two constructs
2973    /// spell an overlapping keyword (`ALL`) but mean opposite things (a modifier on a
2974    /// list vs. a standalone mode), and no shipped dialect but Lenient enables both.
2975    /// Under Lenient (both on) the forms stay disambiguated by lookahead — a bare
2976    /// `GROUP BY ALL` is the DuckDB mode, `GROUP BY ALL <items>` is the PostgreSQL
2977    /// quantifier — so the widening is conflict-free. When off, the `DISTINCT`/`ALL`
2978    /// keyword after `GROUP BY` falls through to the grouping-item grammar, where every
2979    /// shipped dialect reserves it — a clean parse error. On for PostgreSQL / Lenient,
2980    /// off elsewhere.
2981    pub group_by_set_quantifier: bool,
2982    /// Accept DuckDB's `ORDER BY ALL [ASC | DESC] [NULLS FIRST | LAST]` clause
2983    /// mode: sort by every projection column, left to right
2984    /// ([`Query::order_by_all`](crate::ast::Query) — a mode of the whole clause,
2985    /// never a sort-key expression). `ALL` cannot mix with explicit keys
2986    /// (`ORDER BY ALL, x` / `ORDER BY x, ALL` syntax-error; probed on 1.5.4) and
2987    /// takes no `USING` tail. The gate covers only the *query-level* clause, the
2988    /// one position DuckDB gives mode semantics: window `ORDER BY ALL` is a
2989    /// dedicated DuckDB parse error ("Cannot ORDER BY ALL in a window
2990    /// expression"), and the aggregate-internal `agg(x ORDER BY ALL)` form —
2991    /// which DuckDB reads as a `COLUMNS(*)` star expansion producing one output
2992    /// per column — is a different grammar position: a sort *key*
2993    /// ([`Expr::Columns`](crate::ast::Expr::Columns)), gated with the node's own
2994    /// [`CallSyntax::columns_expression`](CallSyntax::columns_expression).
2995    /// When off, `ALL` after `ORDER BY` falls through to
2996    /// the expression grammar, where every shipped dialect reserves it — a clean
2997    /// parse error. On for DuckDB / Lenient, off elsewhere; see
2998    /// [`group_by_all`](GroupingSyntax::group_by_all) for why the two are separate flags.
2999    pub order_by_all: bool,
3000}
3001
3002/// Dialect-owned utility-statement syntax extensions accepted by the parser.
3003///
3004/// The non-standard utility statements whose leading keyword a dialect dispatches — the
3005/// statement-keyword analogue of [`MutationSyntax::merge`]. Whether each is dispatched is
3006/// explicit dialect data: when a flag is off the keyword is left undispatched and surfaces
3007/// as an unknown statement, the same reject mechanism `merge`/`replace_into` use for a
3008/// leading keyword. `EXPLAIN` is not gated here (it is accepted dialect-agnostically for
3009/// now). The introspection, physical-maintenance, and access-control statements split out
3010/// into [`ShowSyntax`], [`MaintenanceSyntax`], and [`AccessControlSyntax`] as this struct
3011/// crossed its 16-field line, so those axes live there rather than here.
3012#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3013pub struct UtilitySyntax {
3014    /// Accept the PostgreSQL `COPY <table>|(<query>) {FROM|TO} <endpoint> ...` bulk
3015    /// data-transfer statement. PostgreSQL-only (also its generic/permissive supersets);
3016    /// off in ANSI and MySQL, which have no `COPY`, so there the leading `COPY` keyword
3017    /// is not dispatched and surfaces as an unknown statement.
3018    pub copy: bool,
3019    /// Accept the Snowflake `COPY INTO <target> FROM <source> [<opt> = <val> ...]` bulk
3020    /// load/unload statement — a distinct grammar from the PostgreSQL `COPY` gated by
3021    /// [`copy`](Self::copy): fixed `INTO … FROM …` direction, `KEY = VALUE` options with a
3022    /// nested `FILE_FORMAT = (…)` list, and the `FILES`/`PATTERN`/`VALIDATION_MODE` clauses.
3023    /// The two share only the leading `COPY` keyword; the dispatcher branches on the `INTO`
3024    /// that follows. On for Snowflake and Lenient; off elsewhere (including PostgreSQL and
3025    /// DuckDB, whose `COPY` is the `copy`-gated `{FROM | TO}` transfer), where a `COPY INTO`
3026    /// surfaces as an unknown statement.
3027    pub copy_into: bool,
3028    /// Accept Snowflake stage references `@stage` / `@~` / `@%table` (with optional
3029    /// `/path` segments) as a dedicated token and as a `COPY INTO` endpoint.
3030    /// On for Snowflake and Lenient; off elsewhere so `@` keeps its other claimants.
3031    pub stage_references: bool,
3032    /// Accept the PostgreSQL `COMMENT ON <object> IS '<text>' | NULL` object-metadata
3033    /// statement. PostgreSQL-only (and its permissive superset); off in ANSI and MySQL,
3034    /// which have no `COMMENT ON`, so there the leading `COMMENT` keyword is not
3035    /// dispatched and surfaces as an unknown statement.
3036    pub comment_on: bool,
3037    /// Accept the SQLite `PRAGMA [<schema> .] <name> [= <value> | (<value>)]`
3038    /// configuration statement. SQLite-only (and the permissive superset); off in
3039    /// ANSI, PostgreSQL, and MySQL, which have no `PRAGMA`, so there the leading
3040    /// `PRAGMA` keyword is not dispatched and surfaces as an unknown statement.
3041    pub pragma: bool,
3042    /// Accept the SQLite `ATTACH [DATABASE] <expr> AS <schema>` statement *and* its
3043    /// `DETACH [DATABASE] <schema>` inverse. One flag gates both leading keywords
3044    /// because they are a single dialect unit — a dialect with `ATTACH` has `DETACH`
3045    /// (mirroring how `existence_guards.if_exists` gates its paired grammar
3046    /// points together). SQLite-only (and the permissive superset); off in ANSI,
3047    /// PostgreSQL, and MySQL, so there the leading keywords are not dispatched and
3048    /// surface as unknown statements.
3049    pub attach: bool,
3050    /// Accept the MySQL `KILL [CONNECTION | QUERY] <id>` thread/query-termination
3051    /// statement. MySQL-only (and the permissive superset); off in ANSI, PostgreSQL, and
3052    /// SQLite, which have no `KILL`, so there the leading keyword is not dispatched and
3053    /// surfaces as an unknown statement — the `copy`/`comment_on` leading-keyword-gate
3054    /// precedent.
3055    pub kill: bool,
3056    /// Accept the MySQL `HANDLER` low-level cursor family — `HANDLER <t> OPEN [[AS] alias]`,
3057    /// `HANDLER <t> READ …`, and `HANDLER <t> CLOSE` — direct index-level storage-engine
3058    /// access that bypasses the optimizer. One flag gates the leading `HANDLER` keyword (all
3059    /// three verbs follow the opened table) — the `copy`/`kill` leading-keyword-gate
3060    /// precedent. MySQL-only among the shipped presets, and a pure addition in the permissive
3061    /// superset (the leading `HANDLER` collides with no other statement); off in ANSI,
3062    /// PostgreSQL, and SQLite, which have no `HANDLER`, so there it is not dispatched and
3063    /// surfaces as an unknown statement. See
3064    /// [`HandlerStatement`](crate::ast::HandlerStatement).
3065    pub handler_statements: bool,
3066    /// Accept the MySQL plugin/component install-management family — `INSTALL PLUGIN <name>
3067    /// SONAME <lib>`, `INSTALL COMPONENT <urn> … [SET …]`, `UNINSTALL PLUGIN <name>`, and
3068    /// `UNINSTALL COMPONENT <urn> …`. One flag gates the leading `INSTALL` and `UNINSTALL`
3069    /// keywords together, an install/uninstall pair kept as one dialect unit like
3070    /// `attach`/`detach`. MySQL-only among the shipped presets, and a pure addition in the
3071    /// permissive superset (the leading `INSTALL`/`UNINSTALL` collide with no other statement);
3072    /// off in ANSI, PostgreSQL, and SQLite, which have neither, so there they are not dispatched
3073    /// and surface as an unknown statement. See
3074    /// [`InstallStatement`](crate::ast::InstallStatement) and
3075    /// [`UninstallStatement`](crate::ast::UninstallStatement).
3076    pub plugin_component_statements: bool,
3077    /// Accept the MySQL `SHUTDOWN` server-shutdown statement — a nullary leading keyword. A
3078    /// leading-keyword gate like `kill`; MySQL-only among the shipped presets and a pure
3079    /// addition in the permissive superset (the leading `SHUTDOWN` collides with no other
3080    /// statement); off in ANSI, PostgreSQL, and SQLite, where it is not dispatched and surfaces
3081    /// as an unknown statement. Separate from [`restart`](UtilitySyntax::restart): `SHUTDOWN`
3082    /// and `RESTART` are distinct statements, not an inverse pair, so each takes its own flag
3083    /// (the `vacuum`/`reindex` precedent). See [`Statement::Shutdown`](crate::ast::Statement).
3084    pub shutdown: bool,
3085    /// Accept the MySQL `RESTART` server-restart statement — a nullary leading keyword. A
3086    /// leading-keyword gate like `kill`; MySQL-only among the shipped presets and a pure
3087    /// addition in the permissive superset; off in ANSI, PostgreSQL, and SQLite, where it
3088    /// surfaces as an unknown statement. Separate from [`shutdown`](UtilitySyntax::shutdown) —
3089    /// distinct behaviours, distinct flags. See [`Statement::Restart`](crate::ast::Statement).
3090    pub restart: bool,
3091    /// Accept the MySQL `CLONE` data-directory provisioning statement — `CLONE LOCAL DATA
3092    /// DIRECTORY [=] '<dir>'` and `CLONE INSTANCE FROM <user>[@<host>]:<port> IDENTIFIED BY
3093    /// '<pw>' [DATA DIRECTORY [=] '<dir>'] [REQUIRE [NO] SSL]`. One flag gates the leading
3094    /// `CLONE` keyword (both `LOCAL`/`INSTANCE` forms follow it — one statement, two forms, like
3095    /// `flush`); MySQL-only and a pure addition in the permissive superset; off in ANSI,
3096    /// PostgreSQL, and SQLite. See [`CloneStatement`](crate::ast::CloneStatement).
3097    pub clone: bool,
3098    /// Accept the MySQL `IMPORT TABLE FROM '<file>' [, …]` tablespace-import statement. A
3099    /// leading-keyword gate on `IMPORT` distinguished from DuckDB's `IMPORT DATABASE`
3100    /// ([`export_import_database`](UtilitySyntax::export_import_database)) by the second keyword
3101    /// (`TABLE` vs `DATABASE`): both may be on in the permissive superset without colliding.
3102    /// MySQL-only among the shipped presets; off in ANSI, PostgreSQL, SQLite, and DuckDB. See
3103    /// [`ImportTableStatement`](crate::ast::ImportTableStatement).
3104    pub import_table: bool,
3105    /// Accept the MySQL `HELP '<topic>'` help-lookup statement — a leading-keyword gate like
3106    /// `kill`. MySQL-only among the shipped presets and a pure addition in the permissive
3107    /// superset (the leading `HELP` collides with no other statement); off in ANSI, PostgreSQL,
3108    /// and SQLite. See [`HelpStatement`](crate::ast::HelpStatement).
3109    pub help_statement: bool,
3110    /// Accept the MySQL `BINLOG '<base64-event>'` binary-log-event replay statement — a
3111    /// leading-keyword gate like `kill`. MySQL-only among the shipped presets and a pure
3112    /// addition in the permissive superset; off in ANSI, PostgreSQL, and SQLite. See
3113    /// [`BinlogStatement`](crate::ast::BinlogStatement).
3114    pub binlog: bool,
3115    /// Accept the MySQL MyISAM key-cache statement pair — `CACHE INDEX <t> [<keys>][, ...]
3116    /// [PARTITION (...)] IN <cache>` and `LOAD INDEX INTO CACHE <t> [PARTITION (...)] [<keys>]
3117    /// [IGNORE LEAVES][, ...]`. One flag gates both leading keywords (`CACHE` and the
3118    /// `LOAD INDEX` lookahead) because they are a single dialect unit — key-cache assignment
3119    /// and preload travel together (the `attach`/`detach`, `prepared_statements` single-flag
3120    /// precedent). MySQL-only among the shipped presets, and a pure addition in the permissive
3121    /// superset; off in ANSI, PostgreSQL, and SQLite, where the leading keywords are not
3122    /// dispatched (a leading `LOAD` without `INDEX` still reaches the `load_extension` gate).
3123    /// See [`CacheIndexStatement`](crate::ast::CacheIndexStatement) and
3124    /// [`LoadIndexStatement`](crate::ast::LoadIndexStatement).
3125    pub key_cache_statements: bool,
3126    /// Accept the `USE` catalog/schema-switch statement — DuckDB `USE <catalog> [. <schema>]`
3127    /// and MySQL `USE <schema>`. A leading-keyword gate like `pragma`: on for DuckDB, MySQL,
3128    /// and the permissive superset; off in ANSI, PostgreSQL, and SQLite, which have no `USE`
3129    /// statement, so there the leading `USE` keyword is not dispatched and surfaces as an
3130    /// unknown statement. (A *non-leading* `USE` is the MySQL index-hint keyword, consumed by
3131    /// the `FROM` grammar, so it never reaches this statement-leading position.) The accepted
3132    /// name arity rides [`use_qualified_name`](UtilitySyntax::use_qualified_name).
3133    pub use_statement: bool,
3134    /// Accept a dotted `USE <catalog> . <schema>` name (DuckDB) rather than the single
3135    /// unqualified `USE <schema>` (MySQL). Refines the name grammar of the base `USE`
3136    /// statement, so it requires [`use_statement`](UtilitySyntax::use_statement): without it
3137    /// the leading `USE` is not dispatched and this flag is inert, the dependency the
3138    /// [`UseQualifiedNameWithoutUseStatement`](crate::dialect::FeatureDependencyViolation::UseQualifiedNameWithoutUseStatement)
3139    /// registry variant records. On for DuckDB and the permissive superset; off for MySQL,
3140    /// whose `USE ident` grammar `ER_PARSE_ERROR`s any dotted name (engine-measured on
3141    /// mysql:8), and off (vacuously) wherever `use_statement` is off. DuckDB still rejects a
3142    /// three-part `USE a.b.c` even with this on — that arity bound is enforced in the parser.
3143    pub use_qualified_name: bool,
3144    /// Accept the prepared-statement lifecycle: `PREPARE <name> [(<types>)] AS
3145    /// <statement>`, `EXECUTE <name> [(<args>)]`, and `DEALLOCATE [PREPARE] <name>`. One
3146    /// flag gates the three leading keywords because they are a single dialect unit — a
3147    /// dialect that prepares statements executes and frees them too (the
3148    /// `attach`/`detach` single-flag precedent). On for DuckDB, PostgreSQL, and Lenient;
3149    /// off elsewhere, where the leading keywords surface as unknown statements.
3150    /// (`EXECUTE` is only a *leading* keyword here; the `EXECUTE` privilege in `GRANT
3151    /// EXECUTE` is unaffected.) The parenthesized `PREPARE` type list is a separate,
3152    /// differently-shaped surface gated by
3153    /// [`prepare_typed_parameters`](UtilitySyntax::prepare_typed_parameters): this flag alone only
3154    /// admits the bare `PREPARE <name> AS <statement>` form. Never both on with MySQL's
3155    /// [`prepared_statements_from`](UtilitySyntax::prepared_statements_from) — the pair is the
3156    /// registered [`GrammarConflict::PreparedStatementsVersusPreparedStatementsFrom`].
3157    pub prepared_statements: bool,
3158    /// Accept the PostgreSQL `PREPARE name ( <type> [, ...] ) AS <statement>`
3159    /// parenthesized parameter-type list between the name and `AS` — full type names
3160    /// (parameterized like `numeric(10,2)`, arrayed like `int[]`), at least one,
3161    /// PostgreSQL rejects an empty `()`. Depends on
3162    /// [`prepared_statements`](UtilitySyntax::prepared_statements) being on too (the type list is
3163    /// a widening of that grammar's name position, not a standalone surface); the
3164    /// dependency is
3165    /// [`FeatureDependencyViolation::PrepareTypedParametersWithoutPreparedStatements`].
3166    /// On for PostgreSQL and Lenient; off for DuckDB, which structurally rejects the
3167    /// whole typed-parameter-list form ("Prepared statement argument types are not
3168    /// supported, use CAST") — so it keeps `prepared_statements` for the bare form only.
3169    pub prepare_typed_parameters: bool,
3170    /// Accept MySQL's prepared-statement lifecycle: `PREPARE <name> FROM {'<text>' | @<var>}`,
3171    /// `EXECUTE <name> [USING @<var>, ...]`, and `{DEALLOCATE | DROP} PREPARE <name>`. One
3172    /// flag gates the three leading keywords (plus the `DROP PREPARE` synonym) because they
3173    /// are a single dialect unit — the `prepared_statements` single-flag precedent.
3174    ///
3175    /// A *different grammar on the same three keywords* from DuckDB's typed-`AS`
3176    /// [`prepared_statements`](UtilitySyntax::prepared_statements): MySQL prepares from a
3177    /// statement *source* (a string literal or a `@`-variable, never an inline-parsed
3178    /// statement), executes with a `USING` clause of `@`-variable references (never
3179    /// parenthesized expressions), and requires the `PREPARE` keyword after `{DEALLOCATE |
3180    /// DROP}`. The two are mutually exclusive per preset (each arms at most one), the split
3181    /// mirroring [`do_statement`](UtilitySyntax::do_statement) vs
3182    /// [`do_expression_list`](UtilitySyntax::do_expression_list) on the `DO` keyword; a preset
3183    /// never arms both, so the shared leading keywords dispatch unambiguously. MySQL-only among
3184    /// the shipped presets; off elsewhere (the permissive superset keeps the DuckDB typed-`AS`
3185    /// reading, since the `FROM`/`USING` surfaces have no positional-argument spelling). Never
3186    /// both on with [`prepared_statements`](UtilitySyntax::prepared_statements) — the pair is the
3187    /// registered [`GrammarConflict::PreparedStatementsVersusPreparedStatementsFrom`], whose
3188    /// `DEALLOCATE` tail (mandatory `PREPARE` under this flag) is incoherent with the DuckDB-first
3189    /// dispatch of the `PREPARE`/`EXECUTE` heads. See
3190    /// [`PrepareFromStatement`](crate::ast::PrepareFromStatement) and
3191    /// [`ExecuteUsingStatement`](crate::ast::ExecuteUsingStatement).
3192    pub prepared_statements_from: bool,
3193    /// Accept the DuckDB `CALL <name>(<args>)` routine/table-function invocation
3194    /// statement. Its own flag rather than sharing
3195    /// [`prepared_statements`](UtilitySyntax::prepared_statements): `CALL` is an independent
3196    /// procedure-call statement, not part of the prepare/execute/deallocate lifecycle
3197    /// (the `vacuum`/`reindex`/`analyze` separate-flags precedent). DuckDB-only among the
3198    /// shipped fitted presets (and the permissive superset); off elsewhere, where a
3199    /// leading `CALL` surfaces as an unknown statement.
3200    pub call: bool,
3201    /// Accept MySQL's bare `CALL <name>` form — a routine invocation with no parenthesized
3202    /// argument list at all (`CALL_SYM sp_name opt_paren_expr_list`, the `opt_paren_expr_list`
3203    /// %empty alternative) — on top of the base [`call`](UtilitySyntax::call) statement (the
3204    /// dependency is [`FeatureDependencyViolation::CallBareNameWithoutCall`]). MySQL-only among
3205    /// the shipped presets (and Lenient); DuckDB's parentheses are mandatory (a bare
3206    /// `CALL pragma_version` is a syntax error), so it is off there and a `CALL name` with no
3207    /// following `(` rejects.
3208    pub call_bare_name: bool,
3209    /// Accept the `LOAD <string>` extension/shared-library load statement. On for
3210    /// PostgreSQL/DuckDB/Lenient (PostgreSQL `LOAD 'plpgsql'`, DuckDB `LOAD 'tpch'` —
3211    /// both accept a string argument); off in ANSI/MySQL/SQLite, where the leading `LOAD`
3212    /// surfaces as an unknown statement. The DuckDB bare-identifier argument rides the
3213    /// separate [`load_bare_name`](UtilitySyntax::load_bare_name) gate.
3214    pub load_extension: bool,
3215    /// Accept DuckDB's bare-identifier `LOAD <name>` argument (`LOAD tpch`) on top of the
3216    /// base [`load_extension`](UtilitySyntax::load_extension) statement (the dependency is
3217    /// [`FeatureDependencyViolation::LoadBareNameWithoutLoadExtension`]). DuckDB-only among the
3218    /// shipped presets (and Lenient); PostgreSQL's `LOAD` requires a string
3219    /// (`LOAD tpch` is a pg_query parser error), so it is off there.
3220    pub load_bare_name: bool,
3221    /// Accept MySQL's `LOAD {DATA | XML} … INFILE … INTO TABLE …` bulk-import statement — a
3222    /// DIFFERENT behaviour on the leading `LOAD` keyword from the PostgreSQL/DuckDB
3223    /// [`load_extension`](UtilitySyntax::load_extension) statement, dispatched on the two-word
3224    /// `LOAD DATA`/`LOAD XML` lookahead so the two never collide even where both gates are on
3225    /// (the `do_statement`/`do_expression_list` split precedent). MySQL-only among the shipped
3226    /// presets (and Lenient); off elsewhere, where a leading `LOAD DATA` surfaces as an unknown
3227    /// statement. Covers the classic documented clause train (`PARTITION`, `CHARACTER SET`,
3228    /// `FIELDS`/`LINES`, `IGNORE n LINES`, the column/`@var` list, `SET`, and — grammar-shared —
3229    /// `ROWS IDENTIFIED BY`); the MySQL 8.4 secondary-engine bulk-load extension clauses (`URL`/
3230    /// `S3` sources, `COUNT`, `COMPRESSION`, `PARALLEL`, `MEMORY`, `ALGORITHM`) are a separate
3231    /// feature not covered by this gate.
3232    pub load_data: bool,
3233    /// Accept a `SESSION | LOCAL | GLOBAL` scope qualifier before a `RESET <name>` target
3234    /// (DuckDB `RESET SESSION x`, `RESET GLOBAL x`; DuckDB parse-accepts all three, though
3235    /// `RESET LOCAL` is a runtime not-implemented). DuckDB-only among the shipped presets
3236    /// (and Lenient); PostgreSQL's `RESET` takes no scope prefix (`RESET SESSION x` is a
3237    /// pg_query parser error — its `RESET SESSION AUTHORIZATION` is a distinct special
3238    /// form, not modelled), so it is off there and a scope keyword after `RESET` rejects.
3239    pub reset_scope: bool,
3240    /// Accept a DuckDB `IF EXISTS` guard on `DETACH DATABASE IF EXISTS <name>` (on top of
3241    /// the [`attach`](UtilitySyntax::attach) `DETACH` statement; the dependency is
3242    /// [`FeatureDependencyViolation::DetachIfExistsWithoutAttach`]). DuckDB-only among the shipped
3243    /// presets (and Lenient); DuckDB admits the guard only after the `DATABASE` keyword
3244    /// (`DETACH IF EXISTS x` is a parser error, `DETACH DATABASE IF EXISTS x` parses —
3245    /// probed on 1.5.4). SQLite's `DETACH` has no `IF EXISTS`, so it is off there.
3246    pub detach_if_exists: bool,
3247    /// Accept the PostgreSQL `DO [LANGUAGE <lang>] '<body>'` anonymous code block. Its own
3248    /// leading-keyword gate, like [`copy`](UtilitySyntax::copy): PostgreSQL-only among the shipped
3249    /// presets (and Lenient); DuckDB has no `DO` statement (probed on 1.5.4: `DO $$...$$`
3250    /// is a parser error), and MySQL/SQLite have none, so there the leading `DO` keyword is
3251    /// not dispatched and surfaces as an unknown statement. The block body is an opaque
3252    /// procedural-language string, not re-parsed here (see
3253    /// [`DoStatement`](crate::ast::DoStatement)). Never both on with
3254    /// [`do_expression_list`](UtilitySyntax::do_expression_list) — the pair is the registered
3255    /// [`GrammarConflict::DoStatementVersusDoExpressionList`].
3256    pub do_statement: bool,
3257    /// Accept the MySQL `DO <expr> [, <expr> ...]` evaluate-and-discard statement — a
3258    /// *different behaviour on the same `DO` keyword* from the PostgreSQL anonymous code block
3259    /// gated by [`do_statement`](UtilitySyntax::do_statement). The two are mutually exclusive
3260    /// per dialect (each preset arms at most one), the split mirroring transaction-`BEGIN`
3261    /// vs compound-block-`BEGIN`; a preset never arms both, so the shared leading `DO` keyword
3262    /// dispatches unambiguously. MySQL-only among the shipped presets; off elsewhere (the
3263    /// permissive superset keeps the PostgreSQL code-block reading, since the `DO LANGUAGE`
3264    /// form has no expression-list spelling). Enabling both at once is the registered
3265    /// [`GrammarConflict::DoStatementVersusDoExpressionList`] — the code-block branch shadows the
3266    /// expression list, so `DO 'x'` mis-parses and `DO 1, 2` over-rejects. See
3267    /// [`DoExpressionsStatement`](crate::ast::DoExpressionsStatement).
3268    pub do_expression_list: bool,
3269    /// Accept the MySQL `LOCK {TABLES | TABLE} <tbl> [[AS] <alias>] {READ [LOCAL] | WRITE}
3270    /// [, ...]` explicit table-locking statement and its `UNLOCK {TABLES | TABLE}` release
3271    /// counterpart (one gate for the pair, the [`rename_statement`](Self::rename_statement)
3272    /// precedent: MySQL's `lock`/`unlock` grammar rules carry both — a dialect with one has
3273    /// both). This is the *per-table lock-kind* reading of the leading `LOCK` keyword —
3274    /// a [`do_statement`](Self::do_statement)/[`do_expression_list`](Self::do_expression_list)-style
3275    /// behaviour split: PostgreSQL's `LOCK [TABLE] <rel>, … [IN <mode> MODE] [NOWAIT]`
3276    /// statement-level mode-list reading is a different behaviour on the same keyword and,
3277    /// when implemented, takes its own gate — a preset would arm at most one, so the shared
3278    /// leading `LOCK`/`UNLOCK` keywords dispatch unambiguously. MySQL-only among the shipped
3279    /// presets (plus the Lenient superset, where it is a pure addition *today* — no other
3280    /// `LOCK`-keyword statement exists; the union will owe a reading decision when the
3281    /// PostgreSQL form lands). Off elsewhere, where the leading keyword is not dispatched and
3282    /// surfaces as an unknown statement. See
3283    /// [`LockTablesStatement`](crate::ast::LockTablesStatement) /
3284    /// [`UnlockTablesStatement`](crate::ast::UnlockTablesStatement).
3285    pub lock_tables: bool,
3286    /// Accept the MySQL `LOCK INSTANCE FOR BACKUP` / `UNLOCK INSTANCE` instance-wide
3287    /// backup-lock pair (one gate for both, as with [`lock_tables`](Self::lock_tables) —
3288    /// the same `lock`/`unlock` grammar rules carry them). A separate flag from
3289    /// `lock_tables` because the two surfaces are independent behaviours that only happen to
3290    /// share MySQL: the backup lock is a MySQL-8-specific administrative statement with no
3291    /// table list (MariaDB, for one, spells it `BACKUP LOCK` instead while sharing `LOCK
3292    /// TABLES`), and it is additionally collision-free with the future PostgreSQL mode-list
3293    /// reading (`LOCK instance …` continues with `IN`/`NOWAIT`/end there, never `FOR`).
3294    /// MySQL-only among the shipped presets (plus the Lenient superset). See
3295    /// [`InstanceLockStatement`](crate::ast::InstanceLockStatement).
3296    pub lock_instance: bool,
3297    /// Accept SQLite's `{DEFERRED | IMMEDIATE | EXCLUSIVE}` transaction-mode modifier
3298    /// between `BEGIN` and the optional `TRANSACTION` keyword (stored on
3299    /// [`TransactionStatement::Begin`](crate::ast::TransactionStatement::Begin)'s `mode`
3300    /// field). On for SQLite/Lenient; off elsewhere. PostgreSQL's `BEGIN` takes its own,
3301    /// differently-shaped modifier set (`ISOLATION LEVEL …` / `READ ONLY|WRITE` / `[NOT]
3302    /// DEFERRABLE`, the existing [`TransactionMode`](crate::ast::TransactionMode) list),
3303    /// deliberately not modelled here, so it stays off there and the leading modifier
3304    /// keyword falls through to today's error (engine-probed: `pg_query` rejects `BEGIN
3305    /// DEFERRED`/`BEGIN IMMEDIATE`/`BEGIN EXCLUSIVE`).
3306    pub begin_transaction_mode: bool,
3307    /// Accept the MySQL `XA` distributed-transaction family — `XA {START | BEGIN} xid [JOIN |
3308    /// RESUME]`, `XA END xid [SUSPEND [FOR MIGRATE]]`, `XA PREPARE xid`, `XA COMMIT xid [ONE
3309    /// PHASE]`, `XA ROLLBACK xid`, and `XA RECOVER [CONVERT XID]` (the X/Open two-phase-commit
3310    /// verbs; `sql_yacc.yy` `xa:`). One flag gates the whole family because it is a single
3311    /// dialect unit reached through one unique leading `XA` keyword (the
3312    /// [`kill`](UtilitySyntax::kill) leading-keyword-gate precedent, not a keyword shared with
3313    /// another dialect). On for MySQL and the Lenient superset (a pure addition there — no other
3314    /// dialect claims `XA`); off elsewhere, where the leading `XA` keyword is not dispatched and
3315    /// surfaces as an unknown statement. Live mysql:8.4.10: every grammar-valid form answers
3316    /// `ER_UNSUPPORTED_PS` 1295 (recognized, not preparable over the wire). See
3317    /// [`XaStatement`](crate::ast::XaStatement).
3318    pub xa_transactions: bool,
3319    /// Accept the MySQL standalone `RENAME TABLE <a> TO <b>[, ...]` and `RENAME USER <u>
3320    /// TO <v>[, ...]` object-rename statements (both →
3321    /// [`Statement::Rename`](crate::ast::Statement::Rename)). A leading-keyword gate like
3322    /// [`kill`](Self::kill): off outside MySQL (and the Lenient superset), where the
3323    /// leading `RENAME` keyword is not dispatched and surfaces as an unknown statement. The
3324    /// two forms share one gate because MySQL's single `rename` grammar rule carries both —
3325    /// a dialect with one has both. Distinct from the `ALTER TABLE ... RENAME TO`
3326    /// sub-clause, which is consumed by the `ALTER TABLE` grammar and never reaches this
3327    /// leading position.
3328    pub rename_statement: bool,
3329    /// Accept the MySQL diagnostics-area family — `SIGNAL`, `RESIGNAL`, and
3330    /// `GET [CURRENT | STACKED] DIAGNOSTICS` — as top-level statements (a leading-keyword gate
3331    /// like [`kill`](UtilitySyntax::kill)). One flag for all three: they are the single
3332    /// cohesive behaviour of manipulating the diagnostics area (raise / re-raise / read).
3333    ///
3334    /// Its own axis, NOT the body-context
3335    /// [`compound_statements`](StatementDdlGates::compound_statements) flag: these three attach
3336    /// to MySQL's top-level `simple_statement` production and are engine-recognized at top
3337    /// level (measured `1295`/`ER_UNSUPPORTED_PS` over the PREPARE oracle — grammar-valid,
3338    /// merely not preparable), exactly where a compound `BEGIN … END` block is NOT (a bare
3339    /// top-level `BEGIN` is transaction-start). Because the body dispatcher falls through to
3340    /// the top-level one for non-compound keywords, this single gate serves both surfaces. On
3341    /// for MySQL (and Lenient); off elsewhere, where the leading `SIGNAL`/`RESIGNAL`/`GET`
3342    /// keyword is left unconsumed and surfaces as an unknown statement.
3343    pub signal_diagnostics: bool,
3344    /// Accept the DuckDB `EXPORT DATABASE ['<db>' TO] '<path>' [<copy-options>]` catalogue
3345    /// dump *and* its `IMPORT DATABASE '<path>'` inverse. One flag gates both leading
3346    /// keywords because they are a single dialect unit — the two halves of one
3347    /// export/import round-trip, a dialect that dumps a database replays it (the same
3348    /// pairing reasoning [`attach`](Self::attach) uses for `ATTACH`/`DETACH`). DuckDB-only
3349    /// (and the permissive superset); off in ANSI, PostgreSQL, MySQL, and SQLite, which have
3350    /// no `EXPORT`/`IMPORT DATABASE`, so there the leading keywords are not dispatched and
3351    /// surface as unknown statements — the `copy`/`attach` leading-keyword-gate precedent.
3352    pub export_import_database: bool,
3353    /// Accept the DuckDB `UPDATE EXTENSIONS [( <name>, ... )]` extension-refresh statement
3354    /// ([`Statement::UpdateExtensions`](crate::ast::Statement::UpdateExtensions)). A
3355    /// *refinement* of the leading `UPDATE`, not a bare leading-keyword gate: a top-level
3356    /// `UPDATE` whose next word is the DuckDB-unreserved `EXTENSIONS` keyword is this
3357    /// statement only when that word is followed by the parenthesized name list or the
3358    /// statement end; an `UPDATE extensions SET …` (a table literally named `extensions`,
3359    /// or `… AS e SET …`) still routes to the DML `UPDATE`, exactly as DuckDB's own grammar
3360    /// resolves the shared prefix (engine-probed on 1.5.4). Off for every non-DuckDB preset
3361    /// (bar the Lenient superset), where the `EXTENSIONS` lookahead is never taken and every
3362    /// `UPDATE` reaches the DML parser unchanged.
3363    pub update_extensions: bool,
3364    /// Accept the MySQL `FLUSH [NO_WRITE_TO_BINLOG | LOCAL] <target>` server-administration
3365    /// statement ([`Statement::Flush`](crate::ast::Statement::Flush)) — the `{TABLE | TABLES}
3366    /// [<list>] [WITH READ LOCK | FOR EXPORT]` form and the comma-separated keyword-target
3367    /// list (`PRIVILEGES`, `LOGS`, `STATUS`, `RELAY LOGS FOR CHANNEL …`, …). A leading-keyword
3368    /// gate like [`kill`](Self::kill): on for MySQL (and the Lenient superset), off elsewhere,
3369    /// where the leading `FLUSH` keyword is not dispatched and surfaces as an unknown
3370    /// statement.
3371    pub flush: bool,
3372    /// Accept the MySQL `PURGE BINARY LOGS {TO '<log>' | BEFORE <datetime>}` binary-log purge
3373    /// statement ([`Statement::Purge`](crate::ast::Statement::Purge)). A leading-keyword gate
3374    /// like [`kill`](Self::kill): on for MySQL (and the Lenient superset), off elsewhere,
3375    /// where the leading `PURGE` keyword is not dispatched and surfaces as an unknown
3376    /// statement. Named for the only surviving 8.4 form — the deprecated `MASTER` synonym was
3377    /// removed, so this gate carries `BINARY LOGS` alone.
3378    pub purge_binary_logs: bool,
3379    /// Accept the MySQL replication-administration family
3380    /// ([`Statement::Replication`](crate::ast::Statement::Replication)) — `CHANGE REPLICATION
3381    /// SOURCE TO <options>`, `CHANGE REPLICATION FILTER <rules>`, `START`/`STOP REPLICA`, and
3382    /// `START`/`STOP GROUP_REPLICATION`. One gate for all five measured families because they
3383    /// are one cohesive dialect unit: MySQL's replication-control surface, reached through the
3384    /// replication-specific leading-keyword sequences (`CHANGE REPLICATION`, `START`/`STOP
3385    /// REPLICA`, `START`/`STOP GROUP_REPLICATION`). A dialect either implements MySQL
3386    /// replication administration or it does not — Group Replication rides the same unit as
3387    /// classic asynchronous replication (both are the server's replication control, not a
3388    /// severable syntax axis). A *refinement* of the shared `START`/`STOP`/`CHANGE` leading
3389    /// keywords (not a bare leading-keyword gate like [`kill`](Self::kill)): the dispatch
3390    /// claims `START`/`STOP` only when the next word is `REPLICA`/`GROUP_REPLICATION` and
3391    /// `CHANGE` only before `REPLICATION`, so `START TRANSACTION` and every other use of those
3392    /// keywords is untouched. On for MySQL (and the Lenient superset); off elsewhere, where
3393    /// the sequences are not dispatched and surface as unknown statements. MySQL 8.4 removed
3394    /// the legacy `MASTER`/`SLAVE` spellings, so only the `REPLICATION`/`REPLICA` grammar is
3395    /// accepted.
3396    pub replication_statements: bool,
3397}
3398
3399/// Dialect-owned SHOW/DESCRIBE introspection-statement syntax accepted by the parser.
3400///
3401/// The session `SHOW`/`SET`/`RESET` reader, the typed `SHOW <object>` catalogue
3402/// listings, and the MySQL/DuckDB `DESCRIBE`/`SUMMARIZE` introspection statements. Split
3403/// out of [`UtilitySyntax`] at its 16-field line as the introspection axis. Each flag is a
3404/// leading-keyword dispatch gate: when off the keyword is not dispatched and surfaces as
3405/// an unknown statement (or the typed-`SHOW` lookahead falls through to the session reader).
3406#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3407pub struct ShowSyntax {
3408    /// Accept MySQL's `DESCRIBE`/`DESC` keywords as EXPLAIN synonyms *and* the MySQL
3409    /// `{DESCRIBE | DESC | EXPLAIN} <table> [<column> | '<pattern>']` table-metadata
3410    /// overload. MySQL-only (and the permissive superset). When on: the leading `DESCRIBE`
3411    /// and `DESC` keywords are dispatched into the EXPLAIN grammar (spelling recorded on
3412    /// [`ExplainStatement`](crate::ast::ExplainStatement)), and all three EXPLAIN-family
3413    /// keywords additionally accept a table name in place of an explainable statement
3414    /// (yielding a [`DescribeStatement`](crate::ast::DescribeStatement)). When off
3415    /// (ANSI/PostgreSQL/SQLite): `DESCRIBE`/`DESC` are not statement leaders and `EXPLAIN`
3416    /// keeps its plain query-plan-only grammar, so a table after `EXPLAIN` is rejected as
3417    /// PostgreSQL does. `EXPLAIN` itself stays ungated (accepted everywhere).
3418    pub describe: bool,
3419    /// Accept DuckDB's leading-keyword `{DESCRIBE | SUMMARIZE} <query> | <table>`
3420    /// introspection statement ([`Statement::ShowRef`](crate::ast::Statement::ShowRef)).
3421    /// DuckDB desugars it to `SELECT * FROM (<SHOW_REF>)`, so it reuses the same `SHOW_REF`
3422    /// core ([`ShowRef`](crate::ast::ShowRef)) as the parenthesized table factor, only at
3423    /// statement-leading position. DuckDB-only (and the permissive superset).
3424    ///
3425    /// Distinct from [`describe`](ShowSyntax::describe): that flag is MySQL's overload of the
3426    /// EXPLAIN keyword (`DESCRIBE` as a query-plan synonym, plus the `<table> [<column>]`
3427    /// metadata form) and never touches `SUMMARIZE`; this one is DuckDB's `SHOW_REF`
3428    /// utility, whose `DESCRIBE`/`SUMMARIZE` return the target's schema / summary statistics
3429    /// as a relation. The two share the `DESCRIBE` leader but are *not* a
3430    /// [`GrammarConflict`]: every real-dialect preset enables at most one, and Lenient — the
3431    /// one preset with both on — resolves the shared leader deterministically by dispatch
3432    /// order (`DESCRIBE`/`DESC` route to the MySQL EXPLAIN synonym above; only `SUMMARIZE`
3433    /// reaches this `SHOW_REF` utility), a conflict-free permissive union rather than a mutual
3434    /// exclusion, so no registry variant governs it. Boundary with
3435    /// [`TableFactorSyntax::show_ref`](crate::dialect::TableFactorSyntax::show_ref):
3436    /// that flag owns the same core inside a `FROM (…)` table factor; this one owns the
3437    /// statement-leading spelling — one production, two grammar positions, one flag each.
3438    pub describe_summarize: bool,
3439    /// Accept the session statements `SET <var> …` / `RESET …` / `SHOW …` (PostgreSQL/
3440    /// MySQL; the standard `SET CONSTRAINTS`/`SET SESSION CHARACTERISTICS` also route
3441    /// here). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no session-variable
3442    /// statement, so it is off; the leading keyword is then not dispatched and surfaces as
3443    /// an unknown statement. `SET TRANSACTION` is transaction control (claimed earlier in
3444    /// dispatch), so it is unaffected by this gate.
3445    pub session_statements: bool,
3446    /// Accept the typed `SHOW [EXTENDED] [FULL] [ALL] TABLES [{FROM | IN} <db>] [LIKE
3447    /// '<pat>' | WHERE <expr>]` catalogue-listing statement
3448    /// ([`Statement::Show`](crate::ast::Statement::Show)). On for MySQL/DuckDB/Lenient;
3449    /// off for ANSI/PostgreSQL/SQLite.
3450    ///
3451    /// This is a *refinement* of the generic-`SHOW` dispatch, so its boundary with the
3452    /// two other `SHOW` seams is MECE:
3453    /// - [`session_statements`](ShowSyntax::session_statements) owns the generic top-level
3454    ///   `SHOW <var>` that reads one configuration parameter. When `show_tables` is on,
3455    ///   a top-level `SHOW` whose next word (past the optional `EXTENDED`/`FULL`/`ALL`
3456    ///   modifiers) is `TABLES` is claimed here instead; every other `SHOW <var>` still
3457    ///   falls through to the session statement. So PostgreSQL (this flag off) keeps
3458    ///   parsing `SHOW tables` as a generic session `SHOW` — the parse it already accepts
3459    ///   — and only MySQL/DuckDB reinterpret the `TABLES` keyword as the catalogue listing.
3460    /// - [`TableFactorSyntax::show_ref`](crate::dialect::TableFactorSyntax::show_ref)
3461    ///   owns the parenthesized `(SHOW <name>)` / `(DESCRIBE …)` *table factor* that only
3462    ///   appears inside a `FROM (…)` and yields a relation; it never reaches the
3463    ///   statement-leading position this flag governs.
3464    ///
3465    /// One flag per typed-`SHOW` subform (the sibling `SHOW COLUMNS`/`SHOW CREATE TABLE`/
3466    /// `SHOW FUNCTIONS` tickets add their own), because their per-dialect availability
3467    /// differs — the `copy`/`comment_on` separate-flags precedent. Once on, the single
3468    /// flag admits the whole modifier union permissively (MySQL's `FULL`/`LIKE`/`WHERE`,
3469    /// DuckDB's `ALL`), the DESCRIBE/PRAGMA single-flag-utility precedent — the corpus
3470    /// verdict gate catches only corpus-present over-acceptance.
3471    pub show_tables: bool,
3472    /// Accept the typed `SHOW [EXTENDED] [FULL] {COLUMNS | FIELDS} {FROM | IN} <tbl>
3473    /// [{FROM | IN} <db>] [LIKE '<pat>' | WHERE <expr>]` column-listing statement
3474    /// ([`Statement::Show`](crate::ast::Statement::Show) with
3475    /// [`ShowTarget::Columns`](crate::ast::ShowTarget)). On for MySQL/Lenient only; off for
3476    /// ANSI/PostgreSQL/SQLite/DuckDB.
3477    ///
3478    /// A separate gate from [`show_tables`](ShowSyntax::show_tables) because its per-dialect
3479    /// availability differs: DuckDB accepts `SHOW [ALL] TABLES` but has *no* `SHOW COLUMNS`
3480    /// grammar at all (every `SHOW {COLUMNS | FIELDS} …` form is `ER_PARSE_ERROR` on DuckDB
3481    /// 1.5.4, engine-probed — it uses `DESCRIBE` / `SHOW <table>` instead), so a shared flag
3482    /// would over-accept there. Same MECE refinement of the generic-`SHOW` dispatch as
3483    /// `show_tables`: a top-level `SHOW` whose next word past the optional `EXTENDED`/`FULL`
3484    /// modifiers is `COLUMNS` or the `FIELDS` synonym is claimed here; every other
3485    /// `SHOW <var>` still falls through to [`session_statements`](ShowSyntax::session_statements),
3486    /// and the parenthesized `(SHOW <name>)`
3487    /// [`show_ref`](crate::dialect::TableFactorSyntax::show_ref) table factor is
3488    /// untouched. Once on, the single flag admits the whole MySQL modifier/filter union
3489    /// permissively (the DESCRIBE/PRAGMA single-flag-utility precedent).
3490    pub show_columns: bool,
3491    /// Accept the typed `SHOW CREATE TABLE <tbl>` statement — the `CREATE TABLE` DDL that
3492    /// would recreate the named table ([`Statement::Show`](crate::ast::Statement::Show) with
3493    /// [`ShowTarget::Create`](crate::ast::ShowTarget) and
3494    /// [`ShowCreateKind::Table`](crate::ast::ShowCreateKind)). On for MySQL/Lenient only; off
3495    /// for ANSI/PostgreSQL/SQLite/DuckDB. The other `SHOW CREATE <kind>` object kinds ride
3496    /// [`show_admin`](ShowSyntax::show_admin).
3497    ///
3498    /// A separate gate from [`show_tables`](ShowSyntax::show_tables)/[`show_columns`](ShowSyntax::show_columns)
3499    /// because its per-dialect availability differs: MySQL has `SHOW CREATE TABLE` but DuckDB
3500    /// has no such grammar (it uses `SELECT sql FROM duckdb_tables` / `.schema`), so a shared
3501    /// flag would over-accept there. Same MECE refinement of the generic-`SHOW` dispatch: a
3502    /// top-level `SHOW` whose next two words are `CREATE TABLE` is claimed here; a bare
3503    /// `SHOW create` (the two-keyword lookahead requires `TABLE` to follow) and every other
3504    /// `SHOW <var>` still fall through to [`session_statements`](ShowSyntax::session_statements),
3505    /// so PostgreSQL's generic `SHOW <var>` reading `create` as the variable name is
3506    /// undisturbed. There are no `EXTENDED`/`FULL` modifiers on this subform (MySQL docs).
3507    /// Only the `TABLE` object kind is modelled; `SHOW CREATE {DATABASE | VIEW | …}` is
3508    /// deferred to sibling tickets.
3509    pub show_create_table: bool,
3510    /// Accept the typed `SHOW [{USER | SYSTEM | ALL}] FUNCTIONS [{FROM | IN} <schema>]
3511    /// [[LIKE] {<function_name> | '<regex>'}]` function-listing statement
3512    /// ([`Statement::Show`](crate::ast::Statement::Show) with
3513    /// [`ShowTarget::Functions`](crate::ast::ShowTarget)). On for Databricks/Lenient only;
3514    /// off for ANSI/PostgreSQL/MySQL/SQLite/DuckDB.
3515    ///
3516    /// A separate gate from the other `show_*` subforms because its per-dialect
3517    /// availability differs — and it is the first typed-`SHOW` flag on under Databricks,
3518    /// where the other three are off. Spark/Databricks are the only shipped engines with a
3519    /// bare `SHOW FUNCTIONS` listing (doc-cited grammar); MySQL's `SHOW FUNCTION STATUS` is
3520    /// a *different* routine-catalogue statement (deferred to its own ticket), and DuckDB
3521    /// has no `SHOW FUNCTIONS` grammar at all — `SHOW <name>` there is a `DESCRIBE` alias,
3522    /// so `SHOW functions` describes a table named `functions` (engine-probed on 1.5.4), a
3523    /// generic-`SHOW` reinterpretation a shared flag would corrupt. Same MECE refinement of
3524    /// the generic-`SHOW` dispatch: a top-level `SHOW` whose next word (past the optional
3525    /// `USER`/`SYSTEM`/`ALL` scope) is `FUNCTIONS` is claimed here; a bare `SHOW <var>`
3526    /// (including `SHOW ALL` with no `FUNCTIONS`) still falls through to
3527    /// [`session_statements`](ShowSyntax::session_statements). Once on, the single flag admits the
3528    /// whole modifier/filter union permissively (the DESCRIBE/PRAGMA single-flag-utility
3529    /// precedent).
3530    pub show_functions: bool,
3531    /// Accept the typed `SHOW {FUNCTION | PROCEDURE} STATUS [LIKE '<pat>' | WHERE <expr>]`
3532    /// stored-routine catalogue listing
3533    /// ([`Statement::Show`](crate::ast::Statement::Show) with
3534    /// [`ShowTarget::RoutineStatus`](crate::ast::ShowTarget)). On for MySQL/Lenient only;
3535    /// off for ANSI/PostgreSQL/SQLite/DuckDB/Databricks.
3536    ///
3537    /// A separate gate from [`show_functions`](ShowSyntax::show_functions) because it is a
3538    /// *different statement*, not the same one under another dialect: MySQL's routine
3539    /// catalogue takes the singular `FUNCTION`/`PROCEDURE` object keyword plus a mandatory
3540    /// `STATUS` and lists a row per stored routine, where the Spark/Databricks
3541    /// `show_functions` listing takes the bare plural `FUNCTIONS` and lists function names.
3542    /// Their per-dialect availability is disjoint (MySQL rejects `SHOW FUNCTIONS`,
3543    /// Databricks rejects `SHOW FUNCTION STATUS`), so a shared flag would over-accept in
3544    /// both directions — the one-flag-per-typed-`SHOW`-subform precedent. Same MECE
3545    /// refinement of the generic-`SHOW` dispatch as the sibling gates: a top-level `SHOW`
3546    /// whose next two words are `FUNCTION STATUS` or `PROCEDURE STATUS` is claimed here. The
3547    /// two-keyword lookahead steals only that full prefix; `FUNCTION`/`PROCEDURE` are reserved
3548    /// keywords, so a bare `SHOW FUNCTION` cannot be a generic session `SHOW <var>` (like
3549    /// `SHOW CREATE`, it is a parse error both ways), and every other `SHOW <var>` still falls
3550    /// through to [`session_statements`](ShowSyntax::session_statements). There is no scope keyword and
3551    /// no `{FROM | IN}` qualifier — `SHOW FUNCTION STATUS FROM db` is `ER_PARSE_ERROR` on
3552    /// mysql:8 (engine-probed) — only the optional `LIKE`/`WHERE` narrowing, which reuses the
3553    /// shared [`ShowFilter`](crate::ast::ShowFilter).
3554    pub show_routine_status: bool,
3555    /// Accept the trailing `VERBOSE` on a generic session `SHOW` — `SHOW ALL VERBOSE`,
3556    /// `SHOW <setting> VERBOSE` — carried on
3557    /// [`SessionStatement::Show::verbose`](crate::ast::SessionStatement). On for Lenient
3558    /// only; off for every oracle-backed dialect.
3559    ///
3560    /// `VERBOSE` here is the sqlparser-rs/DataFusion planner spelling, *not* a database
3561    /// grammar: `pg_query` and DuckDB both reject `SHOW ALL VERBOSE` and
3562    /// `SHOW <setting> VERBOSE` (engine-probed), so no dialect with a real oracle can turn
3563    /// it on without diverging. It refines only the session `SHOW` seam and never the
3564    /// typed-`SHOW` dispatch: the `TABLES`/`COLUMNS`/`CREATE TABLE`/`FUNCTIONS` lookaheads
3565    /// each insist on their keyword after the modifiers, so `SHOW ALL VERBOSE` (no
3566    /// `TABLES`) falls through to the session branch and `SHOW ALL TABLES` stays typed —
3567    /// the seams remain MECE. A behaviour-named tail flag, not gated on
3568    /// [`session_statements`](ShowSyntax::session_statements): where session `SHOW` is off
3569    /// (SQLite) the keyword is never dispatched, so this flag is inert there regardless.
3570    pub show_verbose: bool,
3571    /// Accept the MySQL server-administration / catalogue-introspection `SHOW` sub-command
3572    /// family — the ~40 `SHOW` productions beyond the individually-gated
3573    /// `TABLES`/`COLUMNS`/`CREATE TABLE`/`{FUNCTION|PROCEDURE} STATUS` subforms:
3574    /// `SHOW DATABASES`, `SHOW [GLOBAL|SESSION] {STATUS|VARIABLES}`, `SHOW PLUGINS`,
3575    /// `SHOW [STORAGE] ENGINES`, `SHOW ENGINE <e> {STATUS|MUTEX|LOGS}`, `SHOW PRIVILEGES`,
3576    /// `SHOW {CHARACTER SET|CHARSET}`, `SHOW COLLATION`, `SHOW EVENTS`, `SHOW TABLE STATUS`,
3577    /// `SHOW OPEN TABLES`, `SHOW [FULL] TRIGGERS`, `SHOW [FULL] PROCESSLIST`,
3578    /// `SHOW CREATE {VIEW|DATABASE|EVENT|PROCEDURE|FUNCTION|TRIGGER} <name>`,
3579    /// `SHOW [EXTENDED] {INDEX|INDEXES|KEYS} FROM <t>`, `SHOW GRANTS`,
3580    /// `SHOW {WARNINGS|ERRORS} [LIMIT …]` / `SHOW COUNT(*) {WARNINGS|ERRORS}`,
3581    /// `SHOW BINARY LOGS`, `SHOW REPLICAS`, `SHOW BINARY LOG STATUS`,
3582    /// `SHOW REPLICA STATUS [FOR CHANNEL …]`, `SHOW PROFILES`, and
3583    /// `SHOW {PROCEDURE|FUNCTION} CODE <name>` (all → [`Statement::Show`](crate::ast::Statement::Show)).
3584    /// On for MySQL/Lenient only; off for every other preset.
3585    ///
3586    /// This is a *single* behaviour flag for the whole family rather than one flag per
3587    /// sub-command (the `show_tables`/`show_columns` precedent) because, unlike those, this
3588    /// family's per-dialect availability does not vary — every member is MySQL-only and they
3589    /// travel together; the sub-command identity is DATA (the [`ShowTarget`](crate::ast::ShowTarget)
3590    /// `Listing`/`Bare`/`Create`/`Index`/`Engine`/… axis), not a separate behaviour axis.
3591    /// The parser reaches it through one table-driven dispatch (`parse_show_admin_statement`),
3592    /// not one arm per keyword. Same MECE refinement of the generic-`SHOW` dispatch as the
3593    /// sibling gates: the lookahead insists on one of the family's lead keywords, so any
3594    /// other `SHOW <var>` still falls through to
3595    /// [`session_statements`](ShowSyntax::session_statements). The gate also covers the
3596    /// grammatically heavier subforms with operands (`SHOW GRANTS FOR <user> [USING …]`,
3597    /// `SHOW CREATE USER`, `SHOW PROFILE`, `SHOW {BINLOG | RELAYLOG} EVENTS`); when off, every
3598    /// family keyword falls through unclaimed.
3599    pub show_admin: bool,
3600}
3601
3602/// Dialect-owned physical-maintenance-statement syntax accepted by the parser.
3603///
3604/// The storage-maintenance statements and their operands. Split out of [`UtilitySyntax`]
3605/// at its 16-field line as the maintenance axis, distinct from the introspection and
3606/// access-control axes. Each flag is a leading-keyword dispatch gate: when off the keyword
3607/// is not dispatched and surfaces as an unknown statement.
3608#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3609pub struct MaintenanceSyntax {
3610    /// Accept the SQLite `VACUUM [<schema>] [INTO <expr>]` database-compaction
3611    /// statement. SQLite-only (and the permissive superset). It takes its own flag
3612    /// rather than sharing one with `reindex`/`analyze` because the three are
3613    /// independent maintenance statements, not an inverse pair like `ATTACH`/`DETACH`
3614    /// — the `copy`/`comment_on` precedent (separate flags even though every shipped
3615    /// dialect toggles them together). PostgreSQL also has a (differently-shaped)
3616    /// `VACUUM`, which is not modelled, so this stays off there; when off the leading
3617    /// keyword is not dispatched and surfaces as an unknown statement.
3618    pub vacuum: bool,
3619    /// Accept DuckDB's `VACUUM [ANALYZE] [<table> [(<col>, …)]]` statistics/compaction
3620    /// statement — a *separate* leading-`VACUUM` base gate from SQLite's
3621    /// [`vacuum`](MaintenanceSyntax::vacuum) (the two dialects' `VACUUM` operand grammars
3622    /// are disjoint: SQLite's `[<schema>] INTO <expr>` versus DuckDB's `[ANALYZE] <table>
3623    /// (<cols>)`), so the leading keyword dispatches when *either* is on and the parser
3624    /// reads whichever tail its gate admits. On for DuckDB/Lenient; off elsewhere. DuckDB's
3625    /// `VACUUM` admits only the `ANALYZE` option — 1.5.4's transform throws
3626    /// `NotImplementedException` on `FULL`/`FREEZE`/`VERBOSE`/`disable_page_skipping`, so
3627    /// those never parse and this gate never over-accepts them. Independent of
3628    /// [`vacuum`](MaintenanceSyntax::vacuum): a dialect can admit one `VACUUM` grammar
3629    /// without the other.
3630    ///
3631    /// The column list is bundled here rather than split into a
3632    /// `vacuum_analyze_columns` sibling of
3633    /// [`analyze_columns`](MaintenanceSyntax::analyze_columns) — a deliberate
3634    /// granularity asymmetry. The `ANALYZE` split is measured necessity: two engines
3635    /// share the leading `ANALYZE` but differ on the column list (SQLite has
3636    /// [`analyze`](MaintenanceSyntax::analyze) without
3637    /// [`analyze_columns`](MaintenanceSyntax::analyze_columns); DuckDB has both). No
3638    /// second engine shares this `VACUUM` grammar, and DuckDB's grammar ties the column
3639    /// list to the table operand inseparably — engine-measured on 1.5.4, `VACUUM (a)`
3640    /// without a table reads the parens as the PG-legacy *options* list
3641    /// (`Parser Error: unrecognized VACUUM option "a"`), not columns — so a split flag
3642    /// would have no independent surface to gate. Split only when a second dialect's
3643    /// measured grammar demands it.
3644    pub vacuum_analyze: bool,
3645    /// Accept the SQLite `REINDEX [<name>]` index-rebuild statement. SQLite-only (and
3646    /// the permissive superset); its own flag for the same reason as
3647    /// [`vacuum`](MaintenanceSyntax::vacuum). Off elsewhere.
3648    pub reindex: bool,
3649    /// Accept the SQLite `ANALYZE [<name>]` / DuckDB `ANALYZE [<table>]` statistics
3650    /// statement (a *leading* `ANALYZE`; the `ANALYZE` option inside `EXPLAIN` is
3651    /// unaffected). On for SQLite/DuckDB (and the permissive superset); its own flag for
3652    /// the same reason as [`vacuum`](MaintenanceSyntax::vacuum). Off elsewhere.
3653    pub analyze: bool,
3654    /// Accept DuckDB's optional parenthesized column list on top of the base
3655    /// [`analyze`](MaintenanceSyntax::analyze) statement (`ANALYZE <table> (<col>, …)`; the
3656    /// dependency is [`FeatureDependencyViolation::AnalyzeColumnsWithoutAnalyze`]). DuckDB-only
3657    /// among the shipped presets (and Lenient); SQLite's `ANALYZE` takes no column list, so
3658    /// it is off there and the trailing `(` surfaces as a parser error.
3659    ///
3660    /// This split exists because two engines share the base
3661    /// [`analyze`](MaintenanceSyntax::analyze) statement and differ only on the column
3662    /// list; the DuckDB `VACUUM` column list has no such second consumer, so it stays
3663    /// bundled inside [`vacuum_analyze`](MaintenanceSyntax::vacuum_analyze) — see that
3664    /// flag's doc for the measured justification of the granularity asymmetry.
3665    pub analyze_columns: bool,
3666    /// Accept the bare `CHECKPOINT` write-ahead-log flush statement. On for
3667    /// PostgreSQL/DuckDB/Lenient (both engines accept a bare `CHECKPOINT` — measured on
3668    /// pg_query PG-17 and DuckDB 1.5.4); off in ANSI/MySQL/SQLite, where the leading
3669    /// keyword is not dispatched and surfaces as an unknown statement. The DuckDB `FORCE`
3670    /// modifier and database operand ride the separate
3671    /// [`checkpoint_database`](MaintenanceSyntax::checkpoint_database) gate (PostgreSQL rejects both).
3672    pub checkpoint: bool,
3673    /// Accept DuckDB's `[FORCE] CHECKPOINT [<database>]` operands on top of the base
3674    /// [`checkpoint`](MaintenanceSyntax::checkpoint) statement (the dependency is
3675    /// [`FeatureDependencyViolation::CheckpointDatabaseWithoutCheckpoint`]): the optional
3676    /// `FORCE` modifier and the
3677    /// optional single database name. DuckDB-only among the shipped presets (and Lenient);
3678    /// PostgreSQL's `CHECKPOINT` takes no operands (`FORCE CHECKPOINT` and `CHECKPOINT db`
3679    /// are pg_query parser errors), so it is off there and both forms reject.
3680    pub checkpoint_database: bool,
3681    /// Accept the MySQL admin-table maintenance verbs `{ANALYZE | CHECK | CHECKSUM |
3682    /// OPTIMIZE | REPAIR} {TABLE | TABLES} <table-list> [options]` (all →
3683    /// [`Statement::TableMaintenance`](crate::ast::Statement::TableMaintenance)). One
3684    /// behaviour gate for the whole five-verb family rather than one flag per verb: the
3685    /// verb is DATA on the [`TableMaintenanceKind`](crate::ast::TableMaintenanceKind) axis,
3686    /// not a separate behaviour axis — every member is MySQL-only and they travel together
3687    /// (the `show_admin` precedent). On for MySQL/Lenient only; off in every other preset,
3688    /// where the leading verb is not dispatched and surfaces as an unknown statement.
3689    ///
3690    /// The dispatch is MECE with the SQLite/DuckDB leading-`ANALYZE`
3691    /// [`analyze`](MaintenanceSyntax::analyze) gate: MySQL's `ANALYZE` always takes
3692    /// `{TABLE | TABLES}` (optionally after the `NO_WRITE_TO_BINLOG | LOCAL` prefix), so
3693    /// the lookahead insists on that follow-set before claiming the keyword; a bare
3694    /// `ANALYZE` still falls through to the sibling gate.
3695    pub table_maintenance: bool,
3696}
3697
3698/// Dialect-owned access-control-statement syntax accepted by the parser.
3699///
3700/// The `GRANT`/`REVOKE` permission statements and their extended object/prefix grammar.
3701/// Split out of [`UtilitySyntax`] at its 16-field line as the access-control axis. Each
3702/// flag is a grammar gate: when off the keyword is not dispatched, or the extended object
3703/// grammar rejects, as the dialect requires.
3704#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3705pub struct AccessControlSyntax {
3706    /// Accept the access-control statements `GRANT …` / `REVOKE …` (SQL:2016 E081;
3707    /// PostgreSQL/MySQL). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no
3708    /// permission system, so it is off; the leading keyword is then not dispatched and
3709    /// surfaces as an unknown statement.
3710    pub access_control: bool,
3711    /// Accept the PostgreSQL/standard *extended* `GRANT`/`REVOKE` object and prefix
3712    /// grammar on top of the base [`access_control`](AccessControlSyntax::access_control) statements (the
3713    /// dependency is [`FeatureDependencyViolation::AccessControlExtendedObjectsWithoutAccessControl`]): a
3714    /// schema-scoped object (`ON SCHEMA s`, `ON DATABASE d`), the `ON ALL <kind> IN
3715    /// SCHEMA s` bulk form, and the `{GRANT | ADMIN} OPTION FOR` `REVOKE` prefix. On for
3716    /// ANSI/PostgreSQL/DuckDB/Lenient. MySQL admits `GRANT`/`REVOKE` but not these forms —
3717    /// its object grammar is `ON [TABLE | FUNCTION | PROCEDURE] priv_level`, and `SCHEMA` /
3718    /// `DATABASE` are reserved words that cannot introduce a priv_level, so those objects
3719    /// and the `OPTION FOR` prefix are the syntax error MySQL reports (engine-measured on
3720    /// mysql:8 — `GRANT … ON SCHEMA s`, `REVOKE GRANT OPTION FOR … `, `REVOKE … ON SCHEMA
3721    /// s` all `ER_PARSE_ERROR`), so it is off there. The bare/`TABLE`/`FUNCTION`/`PROCEDURE`
3722    /// objects, `WITH GRANT OPTION`, and the role-membership `GRANT r TO u` / `REVOKE r
3723    /// FROM u` forms are unaffected (MySQL accepts them), so the gate never over-rejects
3724    /// MySQL's supported surface. (SQLite has no permission system, so
3725    /// [`access_control`](AccessControlSyntax::access_control) is already off there and this is moot.)
3726    /// Never both on with the MySQL account route
3727    /// [`access_control_account_grants`](AccessControlSyntax::access_control_account_grants) — the
3728    /// pair is the registered [`GrammarConflict::AccountGrantsVersusExtendedObjects`], where the
3729    /// route dispatches its grammar before this extended-object reading is consulted.
3730    pub access_control_extended_objects: bool,
3731    /// Accept the MySQL account-management DDL family — `CREATE`/`ALTER`/`DROP USER`,
3732    /// `CREATE`/`DROP ROLE` — with its shared account-name (`user@host` / `CURRENT_USER`),
3733    /// authentication (`IDENTIFIED BY`/`WITH`), TLS (`REQUIRE`), resource (`WITH …`), and
3734    /// password/lock option surface. On for MySQL/Lenient. Off elsewhere (no other dialect
3735    /// models MySQL accounts): the leading `USER`/`ROLE` after `CREATE`/`ALTER`/`DROP` is then
3736    /// not dispatched and surfaces as the ordinary `TABLE`-expectation parse error. Independent
3737    /// of [`access_control`](AccessControlSyntax::access_control) (GRANT/REVOKE): a dialect can
3738    /// admit privilege statements without the account-management DDL, and vice versa.
3739    pub user_role_management: bool,
3740    /// Dispatch `GRANT`/`REVOKE` through the MySQL account-based grammar rather than the
3741    /// standard/PostgreSQL one (the dependency is
3742    /// [`FeatureDependencyViolation::AccountGrantsWithoutAccessControl`]): the object is a
3743    /// `priv_level` (`*`, `*.*`, `db.*`, `db.tbl`) rather than a typed object with a name list,
3744    /// every grantee/role is a `user@host` account rather than a role spec, and the grammar adds
3745    /// `PROXY` grants, the `AS <user> [WITH ROLE …]` grantor context, the
3746    /// `[IF EXISTS] … [IGNORE UNKNOWN USER]` `REVOKE` guards, and the `REVOKE ALL PRIVILEGES,
3747    /// GRANT OPTION` form — while dropping `GRANTED BY`, `CASCADE`/`RESTRICT`, and the
3748    /// `{GRANT | ADMIN} OPTION FOR` prefix (all engine-measured `ER_PARSE_ERROR` on mysql:8.4.10).
3749    ///
3750    /// On for MySQL only. It is a *route*, not an additive layer: the MySQL `priv_level`/account
3751    /// object and grantee grammar structurally conflicts with the PostgreSQL typed-object/role-spec
3752    /// grammar (one input, one AST — they cannot both be represented), so a dialect cannot enable
3753    /// both grant grammars at once — enabling this route alongside
3754    /// [`access_control_extended_objects`](Self::access_control_extended_objects) is the registered
3755    /// [`GrammarConflict::AccountGrantsVersusExtendedObjects`], the route deadening the
3756    /// extended-object reading. That is why the Lenient permissive superset keeps this *off* —
3757    /// it retains the richer PostgreSQL-extended grant grammar (schema objects, `GRANTED BY`,
3758    /// `CASCADE`, routine signatures) that
3759    /// [`access_control_extended_objects`](Self::access_control_extended_objects) governs, which a
3760    /// route to the MySQL grammar would forfeit. Requires
3761    /// [`access_control`](Self::access_control): the MySQL forms are still `GRANT`/`REVOKE`
3762    /// statements, unreachable without the base dispatch.
3763    pub access_control_account_grants: bool,
3764}
3765
3766/// Dialect-owned type-name vocabulary extensions accepted by the parser.
3767///
3768/// The standard/PostgreSQL scalar type names are always recognized; these flags
3769/// gate type-name surfaces which the shared vocabulary does not cover.
3770/// Each is a recognition gate, not a parser-side dialect check: when off,
3771/// a name like `TINYINT` is not matched as a built-in and falls through to the
3772/// user-defined-type path (so ANSI/PostgreSQL read it as an ordinary type name),
3773/// while a structural form like `ENUM('a','b')` surfaces as a clean parse error
3774/// (its value list is not a numeric type modifier). The modelling — new
3775/// [`DataType`](crate::ast::DataType) variants, spelling tags, and value-list/wrapper
3776/// shapes — lives with the AST; this struct only decides *which dialects recognize it*.
3777#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3778pub struct TypeNameSyntax {
3779    /// Recognize extended scalar type names: the `TINYINT`/`MEDIUMINT`
3780    /// integer widths, bare `DOUBLE`, `DATETIME`, the `TINYTEXT`/`MEDIUMTEXT`/
3781    /// `LONGTEXT` character-LOB family, and the `TINYBLOB`/`BLOB`/`MEDIUMBLOB`/
3782    /// `LONGBLOB` binary-LOB family.
3783    pub extended_scalar_type_names: bool,
3784    /// Recognize the `ENUM(...)` value-list type in data-type position — MySQL's column
3785    /// type and DuckDB's `x::ENUM('a', 'b')` cast target (which rides the same
3786    /// [`DataType::Enum`](crate::ast::DataType) shape; DuckDB's *`CREATE TYPE ... AS ENUM`*
3787    /// statement uses a separate dedicated production, see
3788    /// [`CreateTypeDefinition`](crate::ast::CreateTypeDefinition)). Split from
3789    /// [`set_type`](Self::set_type) because DuckDB has `ENUM` but no `SET` type
3790    /// (`x::SET('a','b')` is an unknown-type error there, not a value-list type).
3791    pub enum_type: bool,
3792    /// Recognize the `SET(...)` value-list type in data-type position (MySQL only). Kept
3793    /// distinct from [`enum_type`](Self::enum_type): the two share one value-list shape but
3794    /// `SET` is MySQL-specific, so DuckDB enables only `ENUM`.
3795    pub set_type: bool,
3796    /// Recognize the `SIGNED`/`UNSIGNED`/`ZEROFILL` numeric modifiers (as a postfix
3797    /// on a numeric type) and the standalone `SIGNED`/`UNSIGNED` integer cast
3798    /// targets, e.g. `CAST(x AS UNSIGNED)` (MySQL).
3799    pub numeric_modifiers: bool,
3800    /// Recognize an optional display width `(M)` on a built-in integer type name —
3801    /// `INT(11)`, `TINYINT(1)`, `BIGINT(20)` — stored on the integer
3802    /// [`DataType`](crate::ast::DataType) variant's `display_width` field. Canonical
3803    /// to MySQL (deprecated in 8.0.17+ but ubiquitous in dumps); SQLite accepts it
3804    /// through affinity type-name absorption. When off, the trailing `(` on a
3805    /// built-in integer is not consumed and surfaces as a clean parse error, so
3806    /// ANSI/PostgreSQL reject `INT(11)` (verified against `pg_query`). Independent of
3807    /// [`extended_scalar_type_names`](Self::extended_scalar_type_names) (SQLite wants the width
3808    /// without the `TINYINT`/`MEDIUMINT` scalar names) and of
3809    /// [`numeric_modifiers`](Self::numeric_modifiers) (`(M)` is a prefix arg on the
3810    /// type name; `UNSIGNED`/`ZEROFILL` are a separate postfix).
3811    pub integer_display_width: bool,
3812    /// Recognize DuckDB's anonymous composite / nested type constructors in type
3813    /// position: `STRUCT(a INT, ...)` and the standard `ROW(...)` spelling of the same
3814    /// shape, the tagged `UNION(tag T, ...)`, and `MAP(K, V)`. A grammar-position gate
3815    /// keyed on the keyword immediately followed by `(`; when off, the leading word falls
3816    /// through to the user-defined-type path (a bare `struct`/`map` name still resolves),
3817    /// so ANSI/PostgreSQL reject the anonymous form — PostgreSQL has only *named*
3818    /// composite types and spells `ROW` as a value constructor, never a type (verified
3819    /// against a live server: `x::STRUCT(a int)` / `x::ROW(a int)` / `x::MAP(int,text)`
3820    /// each syntax-error). On for DuckDb/Lenient. The array-type suffixes (`T[]`/`T[n]`/
3821    /// `T ARRAY[n]`) are *not* gated here — PostgreSQL accepts them too — they ride the
3822    /// always-parsed array-suffix grammar.
3823    pub composite_types: bool,
3824    /// Accept BigQuery angle-bracket type forms `STRUCT<field TYPE, …>` and `ARRAY<T>`
3825    /// in type position (`CAST(x AS STRUCT<a INT64>)`, column definitions). On for
3826    /// BigQuery/Lenient. Distinct from [`composite_types`](Self::composite_types)
3827    /// (DuckDB paren form `STRUCT(a INT)`) and from expression-position
3828    /// [`struct_constructor`](crate::dialect::ExpressionSyntax::struct_constructor).
3829    pub angle_bracket_types: bool,
3830    /// Require an explicit length parameter on `VARCHAR` and `VARBINARY`
3831    /// (`VARCHAR(255)`, `VARBINARY(16)`). On for MySQL, whose `VARCHAR`/`VARBINARY` are a
3832    /// syntax error without a length (`CREATE TABLE t (a VARCHAR)` is an `ER_PARSE_ERROR` on
3833    /// mysql:8, while the fixed-width `CHAR`/`BINARY` default to length 1 and stay valid).
3834    /// Off for ANSI/PostgreSQL/SQLite/DuckDB/Lenient, where a length-less `VARCHAR` is
3835    /// accepted; when off the missing size is simply left `None`.
3836    pub varchar_requires_length: bool,
3837    /// Accept time-zone-aware temporal type names — `TIMESTAMPTZ` / `TIMESTAMP WITH TIME
3838    /// ZONE`, `TIMETZ` / `TIME WITH TIME ZONE`. On for ANSI/PostgreSQL/SQLite/DuckDB/Lenient.
3839    /// MySQL has no zoned temporal type (its `TIMESTAMP` stores UTC but the *type* carries no
3840    /// zone qualifier), so `TIMESTAMPTZ` and the `WITH TIME ZONE` spellings are an
3841    /// `ER_PARSE_ERROR` on mysql:8; it is off there and a parsed temporal type carrying a
3842    /// zone qualifier is rejected. The zone-less `TIMESTAMP`/`TIME`/`DATETIME` forms are
3843    /// unaffected.
3844    pub zoned_temporal_types: bool,
3845    /// Accept an EMPTY type-parameter parenthesis list on the `DECIMAL`/`DEC`/`NUMERIC`
3846    /// type names — `DECIMAL()`, `DEC()`, `NUMERIC()` — meaning the default precision/scale.
3847    /// DuckDB normalizes `DECIMAL()` to `DECIMAL(18,3)`, byte-identical to a bare `DECIMAL`
3848    /// (probed on 1.5.4: `typeof(x::DECIMAL())` == `typeof(x::DECIMAL)` == `DECIMAL(18,3)`),
3849    /// so the empty form carries no information and folds onto the same `precision: None,
3850    /// scale: None` [`DataType::Decimal`](crate::ast::DataType) shape — the canonical render
3851    /// drops the parens (an ADR-0011 spelling trade; the verbatim `()` survives on the node
3852    /// span). On for DuckDb/Lenient. Off elsewhere, where the empty `(` on a `DECIMAL` needs
3853    /// a precision and the missing modifier surfaces as a clean parse error (verified against
3854    /// `pg_query`: PostgreSQL rejects `DECIMAL()`).
3855    ///
3856    /// Scoped to the DECIMAL family (the surface the core-tranche corpus exercises). DuckDB
3857    /// also admits empty parens on its generic/user-resolved type names and a handful of
3858    /// dedicated built-ins (`DOUBLE()`, `TEXT()`, `DATE()`, `JSON()`, `TIMESTAMPTZ()`,
3859    /// `UUID()`, `HUGEINT()`, …) via the same `opt_type_modifiers` grammar, while the other
3860    /// hard-coded keyword types keep rejecting it (`VARCHAR()`/`TIMESTAMP()`/`FLOAT()` need a
3861    /// value; `INT()`/`REAL()`/`BOOLEAN()` admit no parens at all) — an asymmetric,
3862    /// separately-testable extension deferred (probe matrix on `duckdb-empty-type-parens`).
3863    pub empty_type_parens: bool,
3864    /// Recognize MySQL's character-set annotation on a char-family type — the grammar's
3865    /// `opt_charset_with_opt_binary` production: `CHARACTER SET <name>`, the `CHARSET`
3866    /// synonym, the `ASCII`/`UNICODE`/`BYTE` shortcuts, and/or the `BINARY` binary-collation
3867    /// modifier, in either order (`CHAR CHARACTER SET x BINARY`, `CHAR BINARY ASCII`). Stored
3868    /// on the [`DataType::Character`](crate::ast::DataType) node's `charset` field because it
3869    /// is part of the *type* — it must immediately follow the type and its length, and is an
3870    /// `ER_PARSE_ERROR` on mysql:8 once a column attribute intervenes (`CHAR(5) NOT NULL
3871    /// CHARACTER SET x`), unlike the free-floating `COLLATE` column attribute. Admitted in
3872    /// both the cast target (`CAST(x AS CHAR(5) CHARACTER SET utf8mb4)`) and column-definition
3873    /// positions that funnel through the shared type grammar, on the non-national spellings
3874    /// only (`CHAR`/`CHARACTER`/`VARCHAR`; the `NCHAR`/`NATIONAL` forms fix their own charset
3875    /// and reject it). On for MySQL/Lenient. Off elsewhere — PostgreSQL rejects `CHARACTER
3876    /// SET` in its modern grammar (verified against `pg_query`: `CHAR(5) CHARACTER SET utf8`
3877    /// is a syntax error), so when off the annotation keyword is left unconsumed and surfaces
3878    /// as a clean parse error.
3879    pub character_set_annotation: bool,
3880    /// Accept a leading sign on a `numeric`/`decimal` precision/scale type modifier
3881    /// (`numeric(5, -2)`, `numeric(-3, 6)`). PostgreSQL parses the modifier arguments as a
3882    /// general expression list at raw-parse time, so a signed integer is accepted and only
3883    /// validated later. On for PostgreSQL/Lenient. Off elsewhere (ANSI/MySQL/SQLite/DuckDB
3884    /// require an unsigned modifier), where the leading `-` on a modifier surfaces as a clean
3885    /// parse error.
3886    pub signed_type_modifier: bool,
3887    /// Recognize ClickHouse's `Nullable(T)` parametric type combinator in type position —
3888    /// the inner type extended with a `NULL` value, carried on the
3889    /// [`DataType::Wrapped`](crate::ast::DataType) shape. A grammar-position gate keyed on
3890    /// the keyword immediately followed by `(` (the composite-type precedent), so a bare
3891    /// `Nullable` with no `(` stays an ordinary type/column name and, when off, the whole
3892    /// `Nullable(...)` head falls through to the user-defined-type path. The inner type is a
3893    /// full recursive type, so `Nullable(DECIMAL(10, 2))` / `Nullable(String)[]` parse;
3894    /// ClickHouse's `Nullable(Nullable(T))` / `Nullable(Array(T))` composability rejects are
3895    /// a bind-time `DB::Exception`, not a grammar error, so they parse-accept here.
3896    ///
3897    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
3898    /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
3899    /// `composite_types` / `format_clause` precedent): off for every oracle-compared preset,
3900    /// which parse-reject `Nullable(...)` (its head resolves to a user-defined type name whose
3901    /// `(String)` modifier list then fails to parse).
3902    pub nullable_type: bool,
3903    /// Recognize ClickHouse's `LowCardinality(T)` parametric type combinator in type
3904    /// position — a dictionary-encoding wrapper transparent to query semantics, carried on
3905    /// the [`DataType::Wrapped`](crate::ast::DataType) shape (the same single-inner-type
3906    /// wrapper as `nullable_type`, its own flag per one-behaviour-one-flag). A
3907    /// grammar-position gate keyed on the keyword immediately followed by `(`, so a bare
3908    /// `LowCardinality` with no `(` stays an ordinary type/column name and, when off, the
3909    /// whole `LowCardinality(...)` head falls through to the user-defined-type path. The
3910    /// inner type is a full recursive type, so the canonical `LowCardinality(Nullable(String))`
3911    /// composition and `LowCardinality(DECIMAL(10, 2))` parse; ClickHouse constrains which
3912    /// inner `T` is valid at type resolution (a bind-time `DB::Exception`, not a grammar
3913    /// error), so any single inner type parse-accepts here.
3914    ///
3915    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
3916    /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
3917    /// `composite_types` / `nullable_type` precedent): off for every oracle-compared preset,
3918    /// which parse-reject `LowCardinality(...)` (its head resolves to a user-defined type
3919    /// name whose `(String)` modifier list then fails to parse).
3920    pub low_cardinality_type: bool,
3921    /// Recognize ClickHouse's `FixedString(N)` type constructor in type position — a
3922    /// fixed-length byte string of exactly `N` bytes, carried on the
3923    /// [`DataType::FixedString`](crate::ast::DataType) shape. Unlike the `Nullable`/
3924    /// `LowCardinality` wrappers its argument is a scalar length, not an inner type, so it
3925    /// is its own variant, not a [`WrappedTypeKind`](crate::ast::WrappedTypeKind) arm; its
3926    /// own flag per one-behaviour-one-flag. A grammar-position gate keyed on the keyword
3927    /// immediately followed by `(` (the composite/wrapper precedent), so a bare
3928    /// `FixedString` with no `(` stays an ordinary type/column name and, when off, the whole
3929    /// `FixedString(...)` head falls through to the user-defined-type path. `N` is mandatory
3930    /// (a bare `FixedString` is an invalid ClickHouse spelling) and parsed as any `u32`
3931    /// literal; ClickHouse's positive-length requirement (`FixedString(0)` reject) is a
3932    /// bind-time `DB::Exception`, not a grammar error, so it parse-accepts here.
3933    ///
3934    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
3935    /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
3936    /// `nullable_type` / `low_cardinality_type` precedent): off for every oracle-compared
3937    /// preset, which parse-reject `FixedString(...)` (its head resolves to a user-defined
3938    /// type name whose `(N)` modifier list then fails to parse).
3939    pub fixed_string_type: bool,
3940    /// Recognize ClickHouse's `DateTime64(P[, 'timezone'])` type constructor in type
3941    /// position — a sub-second timestamp carried on the
3942    /// [`DataType::DateTime64`](crate::ast::DataType) shape. Its own flag per
3943    /// one-behaviour-one-flag; like [`fixed_string_type`](Self::fixed_string_type) its
3944    /// leading argument is a mandatory scalar (the precision `P`), not an inner type, so it
3945    /// is a dedicated variant, not a [`WrappedTypeKind`](crate::ast::WrappedTypeKind) arm.
3946    /// The optional second argument is a single-quoted time-zone string literal, not the
3947    /// ANSI `WITH TIME ZONE` flag. A grammar-position gate keyed on the keyword immediately
3948    /// followed by `(` (the composite/wrapper precedent), so a bare `DateTime64` with no `(`
3949    /// stays an ordinary type/column name and, when off, the whole `DateTime64(...)` head
3950    /// falls through to the user-defined-type path. `P` is parsed as any `u32` literal;
3951    /// ClickHouse's documented `0..=9` range is a bind-time reject, not a grammar error, so
3952    /// it parse-accepts here.
3953    ///
3954    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
3955    /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
3956    /// `fixed_string_type` precedent): off for every oracle-compared preset. Off-gate the
3957    /// boundary is asymmetric — `DateTime64(3)` still parse-accepts as a user-defined type
3958    /// name with a `(3)` numeric-modifier list, but `DateTime64(3, 'UTC')` parse-*rejects*,
3959    /// because the string second argument does not fit the `u32`-only modifier grammar.
3960    pub datetime64_type: bool,
3961    /// Recognize ClickHouse's `Nested(name1 Type1, name2 Type2, ...)` named-field composite
3962    /// type in type position — a repeated group carried on the
3963    /// [`DataType::Nested`](crate::ast::DataType) shape (the named-field
3964    /// [`StructTypeField`](crate::ast::StructTypeField) list of `composite_types`, but a
3965    /// distinct variant and its own flag per one-behaviour-one-flag: `Nested` round-trips as
3966    /// `Nested`, never `STRUCT`, and its semantics are a repeated structure, not a product).
3967    /// A grammar-position gate keyed on the keyword immediately followed by `(` (the
3968    /// composite/wrapper precedent), so a bare `Nested` with no `(` stays an ordinary
3969    /// type/column name and, when off, the whole `Nested(...)` head falls through to the
3970    /// user-defined-type path. A field type is a full recursive type, so `Nested(x
3971    /// Nested(...))` parses; ClickHouse's nesting-level limit is a `flatten_nested` setting /
3972    /// bind concern, not a grammar error, so it parse-accepts here.
3973    ///
3974    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so
3975    /// the no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
3976    /// `datetime64_type` precedent): off for every oracle-compared preset, which parse-reject
3977    /// `Nested(a UInt8)` — its head resolves to a user-defined type name whose modifier list
3978    /// is `u32`-only, so the two-word `a UInt8` field has no grammar to fit (the wrapper
3979    /// off-gate reject, not the asymmetric `DateTime64(3)` accept).
3980    pub nested_type: bool,
3981    /// Recognize ClickHouse's fixed-bit-width integer type names — the signed
3982    /// `Int8`/`Int16`/`Int32`/`Int64`/`Int128`/`Int256` family and their unsigned
3983    /// `UInt8`…`UInt256` siblings — carried on the
3984    /// [`DataType::FixedWidthInt`](crate::ast::DataType) shape. One flag for the whole
3985    /// bit-width family: the names always travel together in a dialect, exactly as MySQL's
3986    /// `TINYINT`/`MEDIUMINT`/… ride one [`extended_scalar_type_names`](Self::extended_scalar_type_names)
3987    /// gate, so this is one behaviour, not one flag per width. Unlike the
3988    /// `Nullable`/`FixedString` constructors these names take no arguments, so recognition is a
3989    /// bare-name gate (the `TINYINT`/`extended_scalar` precedent, not the keyword-then-`(`
3990    /// lookahead): when off, a bare `Int256` simply falls through to the user-defined-type path
3991    /// (its trivial off-gate boundary, like a bare `Nullable`).
3992    ///
3993    /// **On for the ClickHouse preset and Lenient.** ClickHouse has no differential oracle, so the
3994    /// no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the `nullable_type`
3995    /// / `fixed_string_type` / `datetime64_type` precedent): off for every oracle-compared
3996    /// preset, which read `Int256` as an ordinary user-defined type name.
3997    pub bit_width_integer_names: bool,
3998    /// Widen the type-name grammar to SQLite's liberal affinity form: a column/cast type is
3999    /// any run of one-or-more space-separated words (`UNSIGNED BIG INT`, `LONG INTEGER`, the
4000    /// misspelled `INTEGEB PRIMARI KEY`) with an optional *two*-argument parenthesized
4001    /// modifier (`VARCHAR(123,456)`, `FLOATING POINT(5,10)`), carried on the
4002    /// [`DataType::Liberal`](crate::ast::DataType) shape. SQLite has no closed type
4003    /// vocabulary — every declared type is affinity text — so its grammar's `typename`
4004    /// accepts an arbitrary `ids ...` token run terminated by a column-constraint keyword, a
4005    /// comma, or a close paren (engine-probed on rusqlite/sqlite3 3.53.2 & 3.43.2).
4006    ///
4007    /// A strict FALLBACK: the typed variants and the single-word user-defined path win
4008    /// wherever they can faithfully represent the input, so a bare `INT`, `DOUBLE PRECISION`,
4009    /// `VARCHAR(255)`, `NATIONAL CHARACTER(15)`, or single-word affinity `BANANA` keep their
4010    /// existing shapes with the flag on; only a trailing type-word or a two-argument paren
4011    /// list that no typed / user-defined parse can hold falls to `DataType::Liberal`. The
4012    /// word run terminates at a column-constraint keyword (`PRIMARY`/`NOT`/`NULL`/`UNIQUE`/
4013    /// `CHECK`/`DEFAULT`/`COLLATE`/`REFERENCES`/`CONSTRAINT`/`AS`/`GENERATED`), so
4014    /// `GENERATED ALWAYS AS` generated columns are unaffected.
4015    ///
4016    /// **On for SQLite and Lenient.** Off elsewhere, where a multi-word type name or a
4017    /// two-argument built-in modifier surfaces as a clean parse error (the standard/
4018    /// PostgreSQL/MySQL/DuckDB have a closed type vocabulary; `pg_query` rejects
4019    /// `LONG INTEGER` and `VARCHAR(123,456)`).
4020    pub liberal_type_names: bool,
4021    /// Admit a string-literal argument in a user-defined type name's modifier list —
4022    /// DuckDB's `GEOMETRY('OGC:CRS84')` coordinate-system annotation, and more generally
4023    /// any `type_name('constant', ...)` where DuckDB's grammar accepts constants (string or
4024    /// numeric) as type modifiers (engine-measured on DuckDB 1.5.4: `MYTYPE('abc')` reaches
4025    /// the binder — a parse-accept — while a non-constant like `(['abc'])` stays a parser
4026    /// error). The modifiers ride the [`DataType::UserDefined`](crate::ast::DataType)
4027    /// shape's `modifiers` list as [`Literal`](crate::ast::Literal)s.
4028    ///
4029    /// When off, only unsigned-integer modifiers parse (`FOO(3)`), so a string modifier
4030    /// surfaces as a clean parse error — the standard/PostgreSQL/MySQL user-type grammar
4031    /// admits no string modifier there. **On for DuckDB only.** Numeric modifiers parse
4032    /// under every dialect regardless of this flag; it gates *only* the string form.
4033    pub string_type_modifiers: bool,
4034}
4035
4036/// Dialect-owned expression *postfix and constructor* syntax accepted by the parser.
4037///
4038/// The postfix operators that navigate a value and the constructor / typed-literal forms
4039/// that build one, each gated by dialect data: a flag decides whether the parser *admits*
4040/// the form, and when off the leading punctuation/keyword surfaces as a clean parse error.
4041/// The infix/prefix operator spellings and the function-call-tail forms
4042/// split out into [`OperatorSyntax`] and [`CallSyntax`] once this struct crossed its
4043/// documented 16-field line; this half keeps only the shapes that build or navigate a
4044/// single expression value. Most gates are over lexemes that always tokenize (a lone `:`
4045/// and `::` are structural punctuation); a flag whose form needs a dialect-gated *lexeme*
4046/// says "also gates the tokenizer" in its first paragraph.
4047#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4048pub struct ExpressionSyntax {
4049    /// Accept the `expr::type` typecast operator.
4050    pub typecast_operator: bool,
4051    /// Accept `base[index]` element access and `base[lower:upper]` slicing.
4052    pub subscript: bool,
4053    /// Accept DuckDB's three-bound slice `base[lower:upper:step]` on top of the two-bound
4054    /// slice [`subscript`](Self::subscript) admits. A pure parser gate (the `:` separators
4055    /// and the `-` placeholder always tokenize): reachable only once
4056    /// [`subscript`](Self::subscript) has opened the brackets (the dependency is
4057    /// [`FeatureDependencyViolation::SliceStepWithoutSubscript`]), it admits the second `:` and,
4058    /// as the middle bound, DuckDB's bare `-`
4059    /// open-upper placeholder (`base[lower:-:step]`) — an empty middle `base[lower::step]`
4060    /// stays a parse error. On for DuckDB only; PostgreSQL slices are two-bound, so it keeps
4061    /// this off despite `subscript`, and a `base[a:b:c]` there is a clean parse error at the
4062    /// second `:`.
4063    pub slice_step: bool,
4064    /// Accept `expr COLLATE collation`.
4065    pub collate: bool,
4066    /// Accept `expr AT TIME ZONE zone`.
4067    pub at_time_zone: bool,
4068    /// Accept semi-structured value paths written as `base:key[0].field`.
4069    ///
4070    /// A postfix grammar gate over `:` followed by an identifier-like key, then optional
4071    /// `.` and `[...]` path suffixes. It contends with
4072    /// [`ParameterSyntax::named_colon`] on the same `:`+identifier trigger, so enabling
4073    /// both is a [`LexicalConflict::ColonParameterVersusSliceBound`] conflict: the scanner
4074    /// would otherwise turn `:key` into a parameter before the postfix parser could read
4075    /// the path. It also contends one layer up, at the *grammar* head, with
4076    /// [`SelectSyntax::prefix_colon_alias`] — whose alias-before-value form reads the same
4077    /// leading `<ident> :` — so enabling both is a
4078    /// [`GrammarConflict::PrefixColonAliasVersusSemiStructuredAccess`] conflict (no shipped
4079    /// preset pairs them).
4080    pub semi_structured_access: bool,
4081    /// Accept the `ARRAY[...]` / `ARRAY(<query>)` array constructors.
4082    pub array_constructor: bool,
4083    /// Accept PostgreSQL multidimensional array literals — a bare-bracket sub-row
4084    /// `[...]` as an element inside an [`array_constructor`](Self::array_constructor),
4085    /// as in `ARRAY[[1,2],[3,4]]` and deeper nestings.
4086    ///
4087    /// A pure grammar gate on the array-constructor element position (its dependency on
4088    /// [`array_constructor`](Self::array_constructor) is
4089    /// [`FeatureDependencyViolation::MultidimArrayLiteralsWithoutArrayConstructor`]), independent of
4090    /// [`collection_literals`](Self::collection_literals): the bare-bracket row is only
4091    /// a value in an array context (a top-level `[1,2]` is still rejected under
4092    /// PostgreSQL), and PostgreSQL enforces that each bracket level is uniform — every
4093    /// element is a sub-row or every element is a scalar, never a mix
4094    /// (`ARRAY[[1,2],3]`, `ARRAY[1,[2,3]]`, and `ARRAY[[1,2],ARRAY[3,4]]` are parse
4095    /// errors, matching PG's `expr_list` / `array_expr_list` split). Ragged nestings
4096    /// (`ARRAY[[1,2],[3]]`) parse-accept — PostgreSQL rejects them at bind time, not in
4097    /// the grammar. A sub-row is represented as an ordinary bracket-spelled
4098    /// [`ArrayExpr::Elements`](crate::ast::ArrayExpr), so it renders and shapes exactly
4099    /// like a DuckDB list level. DuckDB reaches the same surface through
4100    /// [`collection_literals`](Self::collection_literals) (a top-level list *is* a value
4101    /// there, and levels may mix), so it keeps this off and overrides the POSTGRES
4102    /// spread.
4103    pub multidim_array_literals: bool,
4104    /// Accept the DuckDB collection literals: the bare-bracket list `[a, b, …]`, the
4105    /// struct `{'k': v, …}`, and the map `MAP {k: v, …}` / `MAP(<keys>, <values>)`.
4106    ///
4107    /// A pure grammar/lexical gate on the primary-expression `[`, `{`, and `MAP`
4108    /// leads. The bracket list contends for the same `[` trigger as an
4109    /// [`identifier_quotes`](FeatureSet::identifier_quotes) style opening with `[`
4110    /// (T-SQL/SQLite/Lenient bracket identifiers), so enabling both is the
4111    /// [`LexicalConflict::BracketIdentifierVersusArraySyntax`] conflict — a dialect
4112    /// picks bracket identifiers *or* `[` collection/array syntax. The `key: value`
4113    /// separator likewise contends with [`ParameterSyntax::named_colon`] on the
4114    /// `:`+identifier trigger
4115    /// ([`LexicalConflict::ColonParameterVersusSliceBound`], shared with the slice
4116    /// bound). When off, `[`/`{` in primary position surface as a clean parse error
4117    /// (`MAP` falls back to an ordinary name).
4118    pub collection_literals: bool,
4119    /// Accept explicit `ROW(...)` and implicit `(a, b, …)` row constructors.
4120    pub row_constructor: bool,
4121    /// Accept BigQuery's `STRUCT(...)` value constructor — the typeless `STRUCT(1, 2)`
4122    /// and named `STRUCT(x AS a, y AS b)` forms and the typed
4123    /// `STRUCT<a INT64, b STRING>(1, 'x')` form — on the canonical
4124    /// [`StructConstructorExpr`](crate::ast::StructConstructorExpr) shape.
4125    ///
4126    /// A pure parser lookahead gate, the sibling of
4127    /// [`row_constructor`](Self::row_constructor)/[`array_constructor`](Self::array_constructor):
4128    /// the (contextual, non-reserved) `STRUCT` word opens the constructor only when
4129    /// immediately followed by `(` (typeless) or `<` (typed), mirroring the `ROW(`/`ARRAY[`
4130    /// disambiguation — the tokenizer is untouched, and the `<` opener is committed on a
4131    /// single-token lookahead (no rewind), sound because in a preset that admits this form
4132    /// `STRUCT` is not a bare column so `struct < x` is not a competing comparison. When
4133    /// off, `STRUCT(...)` is left to the ordinary call path and stays an
4134    /// [`Expr::Function`](crate::ast::Expr::Function) catalog-function call — the
4135    /// non-interference boundary every non-BigQuery preset (PostgreSQL included) keeps.
4136    ///
4137    /// **On for BigQuery and Lenient.** BigQuery documents the form and has no differential
4138    /// oracle here (self-consistency + gate-off rejection are the tests); Lenient unions it
4139    /// in. Off for every other preset: DuckDB builds structs with the `{...}` literal /
4140    /// `struct_pack()` / `row()` rather than a `STRUCT(...)` keyword form, and the
4141    /// Spark-family `struct(...)` builtin is not verifiable without an engine, so those
4142    /// presets keep `struct(...)` an ordinary call rather than risk reshaping it. The typed
4143    /// field list here is parsed inline; a bare `STRUCT<...>` in *type* position
4144    /// (`CAST(x AS STRUCT<...>)`) is a separate type-name surface not gated by this flag.
4145    pub struct_constructor: bool,
4146    /// Accept `(expr).field` composite field selection.
4147    pub field_selection: bool,
4148    /// Accept the `.*` composite/whole-row star selector in a *value* position: the
4149    /// composite expansion `(expr).*` off a parenthesized primary, and a whole-row
4150    /// `tbl.*` used as a value (inside a `ROW(...)` field, a function argument, a
4151    /// comparison, or a `tbl.*::type` cast) rather than as a select-list projection
4152    /// target. PostgreSQL admits this `.*` indirection wherever an `a_expr` is, at the
4153    /// same tight precedence as [`field_selection`](Self::field_selection) (so
4154    /// `(a).* + 1` groups as `((a).*) + 1`); engine-probed on pg_query PG-17. Gated
4155    /// apart from `field_selection` because DuckDB parse-accepts `(struct).field` but
4156    /// parse-rejects every `.*` value expansion (engine-probed on DuckDB 1.5.4), so it
4157    /// is off there and on for PostgreSQL/Lenient. When off, a `.` followed by `*` is
4158    /// left unconsumed and surfaces as the usual downstream parse error. This governs
4159    /// only the *value*-position star; a plain select-list/`RETURNING` `tbl.*` remains
4160    /// a [`SelectItem::QualifiedWildcard`](crate::ast::SelectItem::QualifiedWildcard)
4161    /// under every preset.
4162    pub field_wildcard: bool,
4163    /// Accept the prefix-typed string literal — a type name whose reading becomes a
4164    /// literal when a string constant follows the type prefix: the standard temporal
4165    /// forms `DATE '…'` / `TIME '…'` / `TIMESTAMP '…'` and the PostgreSQL generalized
4166    /// `<type> '…'` (`float8 '1.5'`, folded onto a
4167    /// [`CastSyntax::PrefixTyped`](crate::ast::CastSyntax) cast). One flag covers both,
4168    /// because the two travel together — a dialect with the temporal forms has the
4169    /// generalized one. On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has neither
4170    /// (`date '…'` juxtaposes a name and a string, a clean parse error there), so it is
4171    /// off; the type keyword then falls back to its ordinary column/function reading and
4172    /// the trailing string surfaces as the usual parse error.
4173    ///
4174    /// The prefix-typed `INTERVAL '…' <fields>` literal is split onto its own
4175    /// [`typed_interval_literal`](Self::typed_interval_literal) refinement, because MySQL
4176    /// has the other temporal literals but no first-class interval literal.
4177    pub typed_string_literals: bool,
4178    /// Arm the prefix-typed `INTERVAL '<amount>' <fields>` literal — the ANSI/PostgreSQL
4179    /// interval literal that folds onto
4180    /// [`LiteralKind::Interval`](crate::ast::LiteralKind::Interval), including the ANSI
4181    /// `HOUR TO SECOND` composite and the `SECOND(p)` unit precision. A refinement of
4182    /// [`typed_string_literals`](Self::typed_string_literals) (the interval literal is only
4183    /// reached when that is on), split out because MySQL admits the other prefix-typed
4184    /// temporal literals (`DATE`/`TIME`/`TIMESTAMP`) yet has no interval literal at all:
4185    /// every typed `INTERVAL '…'` form — standalone *or* in a `+`/`-` operand — is
4186    /// `ER_PARSE_ERROR` (1064) on mysql:8.4.10 (engine-measured), the only valid MySQL
4187    /// interval being the operator-position `INTERVAL <expr> <unit>` modelled by
4188    /// [`mysql_interval_operator`](Self::mysql_interval_operator).
4189    ///
4190    /// On for ANSI/PostgreSQL/DuckDB/Lenient. Off for MySQL (the operator reader owns its
4191    /// valid unit-bearing forms; every form that reaches this literal path there — the ANSI
4192    /// `TO`/precision spellings the operator reader declines, and the unit-less
4193    /// `INTERVAL '1'` — is one MySQL rejects, so the literal path declines and the
4194    /// `INTERVAL` keyword falls back to its ordinary reading, a parse error on the trailing
4195    /// string). Off for SQLite, where [`typed_string_literals`](Self::typed_string_literals)
4196    /// is already off and no prefix-typed literal is reached.
4197    pub typed_interval_literal: bool,
4198    /// Accept DuckDB's relaxed `INTERVAL` literal spellings on top of the standard
4199    /// quoted `INTERVAL '1' DAY` form: an unquoted integer amount (`INTERVAL 3 DAY`),
4200    /// a parenthesized-expression amount (`INTERVAL (days) DAY`), and plural unit
4201    /// spellings (`INTERVAL 3 DAYS`, `INTERVAL '1' hours`). All three fold onto the one
4202    /// [`LiteralKind::Interval`](crate::ast::LiteralKind::Interval) shape: the
4203    /// amount round-trips from the literal's span exactly as the quoted string
4204    /// does, so only the unit qualifier lands on the tag, and a plural unit folds
4205    /// onto its singular [`IntervalFields`](crate::ast::IntervalFields) — the plural `s`
4206    /// round-trips from the span (the documented spelling trade). The unquoted/parenthesized
4207    /// amount forms require a trailing unit (a bare `INTERVAL 3` is a DuckDB *binding*
4208    /// error, not a syntax one). On for DuckDB and Lenient; off elsewhere, where the
4209    /// non-standard spellings surface as the usual parse error.
4210    pub relaxed_interval_syntax: bool,
4211    /// Accept the MySQL operator-position interval quantity `INTERVAL <expr> <unit>` —
4212    /// the `INTERVAL 3 DAY` operand of MySQL date arithmetic — as an
4213    /// [`Expr::Interval`](crate::ast::Expr::Interval) node.
4214    ///
4215    /// A behaviour distinct from the ANSI/PostgreSQL/DuckDB typed-string interval *literal*
4216    /// ([`typed_string_literals`](Self::typed_string_literals) /
4217    /// [`relaxed_interval_syntax`](Self::relaxed_interval_syntax), both folding onto
4218    /// [`LiteralKind::Interval`](crate::ast::LiteralKind::Interval)): MySQL's `INTERVAL` is
4219    /// not a first-class value but the second argument of the `Item_date_add_interval`
4220    /// production, so it carries an arbitrary amount *expression* (integer, decimal, string,
4221    /// `?`, `@var`, `n + 1`, `(expr)`, negative) and a **mandatory** unit keyword drawn from
4222    /// MySQL's underscore vocabulary — the simple units `MICROSECOND`…`YEAR` (plus `WEEK`,
4223    /// `QUARTER`) and the composites `SECOND_MICROSECOND`, `MINUTE_SECOND`, `DAY_HOUR`,
4224    /// `YEAR_MONTH`, … (engine-measured on mysql:8.4.10). It admits **no** ANSI `TO`
4225    /// composite and **no** `(p)` unit precision (`INTERVAL '1' HOUR TO SECOND` and
4226    /// `INTERVAL '1' SECOND(3)` are `ER_PARSE_ERROR` there); a `TO`/`(` after the unit makes
4227    /// the operator reader decline so the typed-string interval literal path
4228    /// ([`typed_interval_literal`](Self::typed_interval_literal)) owns those spellings under
4229    /// Lenient/PostgreSQL. Under MySQL that literal path is off, so the declined ANSI
4230    /// spellings reject (matching the engine).
4231    ///
4232    /// **On for MySQL and Lenient.** When on, an `INTERVAL` in expression-prefix position is
4233    /// read as this node before the typed-string literal path (MySQL has no first-class
4234    /// interval literal); a form that is not a valid MySQL operator interval — unit-less, an
4235    /// ANSI `TO`/precision spelling, a bare `INTERVAL(a, b)` index function — rewinds and
4236    /// falls through, so under Lenient the DuckDB relaxed amounts, plural units, unit-less
4237    /// PostgreSQL literals, and the ANSI `TO`/precision interval literals still parse (under
4238    /// MySQL they reject, [`typed_interval_literal`](Self::typed_interval_literal) being off).
4239    /// The node is modelled as a primary (highest binding power), so MySQL's *position*
4240    /// restriction is deliberately not enforced: a standalone `SELECT INTERVAL 3 DAY`, a
4241    /// leading `INTERVAL 3 DAY - x` (only `+` leads on mysql:8), and `INTERVAL 3 DAY IS NULL`
4242    /// over-accept rather than over-reject the valid operand/`DATE_ADD`/frame positions a
4243    /// general grammar cannot distinguish. Off elsewhere, where `INTERVAL` keeps its
4244    /// literal-or-column reading.
4245    pub mysql_interval_operator: bool,
4246    /// Accept DuckDB's `#n` positional column reference — a select-list column named by
4247    /// its 1-based output position ([`Expr::PositionalColumn`](crate::ast::Expr::PositionalColumn)),
4248    /// used mainly in `ORDER BY #1` / `GROUP BY #2` but valid wherever a value expression
4249    /// is. Also gates the tokenizer: the `#<digits>` lexeme is scanned only under a
4250    /// dialect that sets this (DuckDB), so elsewhere `#` stays a stray byte, a MySQL line
4251    /// comment, or PostgreSQL's XOR operator per that dialect's data. Because it claims
4252    /// the `#` trigger, it is mutually exclusive with the two other `#` claimants — the
4253    /// [`hash_bitwise_xor`](FeatureSet::hash_bitwise_xor) XOR operator
4254    /// ([`LexicalConflict::HashXorOperatorVersusPositionalColumn`]) and a
4255    /// [`CommentSyntax::line_comment_hash`] line comment
4256    /// ([`LexicalConflict::HashCommentVersusPositionalColumn`]); a `#`-led identifier byte
4257    /// class instead resolves by scan order (the identifier scan precedes this arm), like
4258    /// the XOR case. On for DuckDB only. Lenient, which already commits `#` to a MySQL
4259    /// line comment, cannot also enable it. When off, `#1` surfaces per the dialect's
4260    /// other `#` reading (a clean parse error in ANSI/SQLite).
4261    pub positional_column: bool,
4262    /// Accept DuckDB's python-style keyword lambda `lambda x, y: body` — a prefix
4263    /// production, distinct from the [`OperatorSyntax::lambda_expressions`] single-arrow
4264    /// form and folded onto the same [`Expr::Lambda`](crate::ast::Expr::Lambda) node with
4265    /// a [`LambdaParamSpelling::Keyword`](crate::ast::LambdaParamSpelling) spelling tag.
4266    /// DuckDB 1.3.0 introduced it and prefers it over the deprecated arrow; the two
4267    /// spellings are separate flags because DuckDB's roadmap keeps the keyword while
4268    /// dropping the arrow. When on, a `lambda` word in expression-prefix position opens
4269    /// the production unconditionally rather than reading as an ordinary column — matching
4270    /// DuckDB, which reserves `lambda` — so a bare `lambda`, or one not followed by
4271    /// `<params>:`, is a parse error. On for DuckDB and Lenient.
4272    pub lambda_keyword: bool,
4273}
4274
4275/// Dialect-owned infix/prefix *operator* syntax accepted by the parser.
4276///
4277/// The operator-acceptance and operator-spelling family, split out of [`ExpressionSyntax`]
4278/// when it crossed its 16-field line. Each flag decides whether the parser *admits* the
4279/// operator, while the binding powers that order them live in [`BindingPowerTable`]; a
4280/// spelling that folds onto an existing operator carries a spelling tag so it round-trips.
4281/// A flag whose form needs a dialect-gated *lexeme* says "also gates the tokenizer" in its
4282/// first paragraph — a flag crossing the lexer/parser boundary must declare it there.
4283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4284pub struct OperatorSyntax {
4285    /// Accept the PostgreSQL explicit-operator infix form `a OPERATOR(schema.op) b`.
4286    pub operator_construct: bool,
4287    /// Accept PostgreSQL's containment operators — infix `@>` (contains) and `<@`
4288    /// (contained by). Also gates the tokenizer: the `@>`/`<@` lexemes are recognised
4289    /// only under a dialect that sets this (PostgreSQL), so elsewhere `@>` stays a stray
4290    /// `@` then `>`, and `<@` a `<` then a stray `@`. The `@>` munch requires the
4291    /// following `>`, so a bare `@` is always a stray byte here — the prefix `@`
4292    /// absolute-value operator is a scoped follow-up, since its bare-`@` lexeme contends
4293    /// with the T-SQL/MySQL `@name` sigils and needs a tracked conflict. The `<@` munch,
4294    /// by contrast, shadows an abutting `@name` sigil (`a<@x` meaning `a < @x`) whenever
4295    /// this is on together with a single-`@` form — the tracked
4296    /// [`LexicalConflict::ContainmentOperatorVersusAtName`]. The operators bind at
4297    /// PostgreSQL's "any other operator" precedence ([`BindingPowerTable::any_operator`]).
4298    ///
4299    /// [`BindingPowerTable::any_operator`]: crate::precedence::BindingPowerTable::any_operator
4300    pub containment_operators: bool,
4301    /// Accept PostgreSQL's JSON access operators — infix `->` (field/element as
4302    /// `json`) and `->>` (as `text`). Also gates the tokenizer: the `->`/`->>`
4303    /// lexemes are munched only under a dialect that sets this, so elsewhere `a->b`
4304    /// stays a `-` then a `>`. MySQL spells the same JSON accessors and can enable
4305    /// this independently of [`containment_operators`](Self::containment_operators),
4306    /// which is why the two are separate flags. Same "any other operator" precedence.
4307    pub json_arrow_operators: bool,
4308    /// Accept PostgreSQL's `jsonb` existence / path / search operators as one family: `?`
4309    /// (key exists), `?|` (any key exists), `?&` (all keys exist), `@?` (`jsonpath` returns
4310    /// any item), `@@` (`jsonpath` predicate match, also the `tsvector @@ tsquery` full-text
4311    /// match), `#>` (extract at path), `#>>` (extract at path as `text`), and `#-` (delete at
4312    /// path). Also gates the tokenizer: these lexemes are munched only under a dialect that
4313    /// sets this. One flag for the whole family (the family-level granularity of
4314    /// [`json_arrow_operators`](Self::json_arrow_operators) / [`containment_operators`](Self::containment_operators)):
4315    /// PostgreSQL enables the set as a unit and every other dialect leaves it off. All eight
4316    /// bind at the "any other operator" precedence ([`BindingPowerTable::any_operator`]),
4317    /// left-associative — engine-measured on pg_query (tighter than comparison, looser than
4318    /// additive). The operators fold onto the dedicated
4319    /// [`BinaryOperator`] `Json…` keys.
4320    ///
4321    /// Three lead bytes are shared triggers the tokenizer partitions by follow byte:
4322    /// - `?` is otherwise the anonymous placeholder ([`ParameterSyntax::anonymous_question`]);
4323    ///   the two never co-enable in a shipped preset (PostgreSQL has no `?` parameter), and a
4324    ///   feature set enabling both is the tracked
4325    ///   [`LexicalConflict::JsonbKeyExistsVersusAnonymousParameter`].
4326    /// - `@@` is otherwise the MySQL system-variable sigil
4327    ///   ([`SessionVariableSyntax::system_variables`]); the tracked
4328    ///   [`LexicalConflict::JsonbSearchOperatorVersusSystemVariable`]. `@?` is disjoint from
4329    ///   every other `@` claimant by its second byte, so it adds no conflict.
4330    /// - `#>`/`#>>`/`#-` ride the `#` byte, which reaches the operator scanner only under
4331    ///   PostgreSQL's `#` bitwise-XOR ([`hash_bitwise_xor`](FeatureSet::hash_bitwise_xor), on together with this in the
4332    ///   PostgreSQL preset); they are munched ahead of the bare `#` and stay disjoint from
4333    ///   DuckDB's `#n` positional column (`#`+digit) by follow byte.
4334    ///
4335    /// The sibling regex/geometric/network operator surface builds on the same `?`/`@`/`#`
4336    /// lexeme foundation under its own flag(s): bare prefix `@` (absolute value) stays a
4337    /// stray byte here (the `@?`/`@@` munch requires a follow byte), left for that work.
4338    ///
4339    /// [`BindingPowerTable::any_operator`]: crate::precedence::BindingPowerTable::any_operator
4340    pub jsonb_operators: bool,
4341    /// Accept SQLite's `==` equality spelling as a synonym for `=`. Also gates the
4342    /// tokenizer: the doubled-`=` lexeme is munched to the equality operator only
4343    /// under a dialect that sets this, so elsewhere `a == b` stays `a` `=` `=` `b`
4344    /// and surfaces as a clean parse error. The two spellings fold onto the one
4345    /// canonical [`BinaryOperator::Eq`] operator; the
4346    /// [`EqualsSpelling`](crate::ast::EqualsSpelling) tag records
4347    /// which the source used so `==`/`=` round-trip exactly, the same pattern as
4348    /// `MOD`/`RLIKE`/`REGEXP`.
4349    pub double_equals: bool,
4350    /// Accept DuckDB's `//` integer-division spelling. Also gates the tokenizer: the
4351    /// doubled-`/` lexeme is munched to the integer-division operator only under a dialect
4352    /// that sets this, so elsewhere `a // b` stays `a` `/` `/` `b` and surfaces as a clean
4353    /// parse error. No shipped preset lexes `//` as a line comment, so the doubled munch
4354    /// never shadows a comment mode. The symbol folds onto the one canonical
4355    /// [`BinaryOperator::IntegerDivide`] operator; the [`IntegerDivideSpelling`]
4356    /// tag records the `//` spelling so it round-trips (this is load-bearing for validity,
4357    /// not only fidelity: DuckDB has no `DIV` keyword and MySQL no `//` operator, so the
4358    /// spelling cannot be normalized away). Distinct from MySQL's `DIV`, which rides
4359    /// [`KeywordOperators::MySql`].
4360    pub integer_divide_slash: bool,
4361    /// Accept DuckDB's `^@` "starts with" infix operator (`'hello' ^@ 'he'`). Also
4362    /// gates the tokenizer: `^@` is munched only when this is on; elsewhere `^` then
4363    /// `@` stay separate (and `@` is often a stray byte).
4364    pub starts_with_operator: bool,
4365    /// Accept SQLite's `IS` / `IS NOT` as a *general* null-safe equality over
4366    /// arbitrary operands (`1 IS 1`, `1 IS NOT 2`), not just `IS NULL` /
4367    /// `IS [NOT] DISTINCT FROM`. When on, an `IS [NOT] <expr>` whose right operand is
4368    /// neither `NULL` nor `DISTINCT FROM …` folds onto the existing null-safe
4369    /// [`IsNotDistinctFrom`](crate::ast::BinaryOperator::IsNotDistinctFrom) /
4370    /// [`IsDistinctFrom`](crate::ast::BinaryOperator::IsDistinctFrom) operators —
4371    /// SQLite's `IS` is exactly `IS NOT DISTINCT FROM`. When off (ANSI/PostgreSQL/
4372    /// MySQL), `IS` requires `NULL` or `DISTINCT FROM`, so `1 IS 1` is a clean parse
4373    /// error.
4374    pub is_general_equality: bool,
4375    /// Accept the SQL:2016 truth-value tests `<expr> IS [NOT] {TRUE | FALSE | UNKNOWN}`
4376    /// (F571), parsed to the postfix [`Expr::IsTruth`](crate::ast::Expr::IsTruth) predicate.
4377    /// On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient, all of which accept the three-valued
4378    /// `UNKNOWN` (engine-measured on pg_query, MySQL 8, DuckDB). Off for SQLite, whose `IS`
4379    /// is a general null-safe equality ([`is_general_equality`](Self::is_general_equality)):
4380    /// there `IS TRUE`/`IS FALSE` fold onto the boolean literal and `IS UNKNOWN` reads as
4381    /// equality against an identifier `unknown`, so SQLite has no truth-value predicate and
4382    /// this stays off (turning it on would over-accept `IS UNKNOWN`, which SQLite rejects
4383    /// unless `unknown` is a bound column). Checked ahead of the general-equality reading so
4384    /// that a dialect enabling both keeps the standard truth predicate.
4385    pub truth_value_tests: bool,
4386    /// Accept MySQL's `<=>` null-safe equality operator (`a <=> b` ≡
4387    /// `a IS NOT DISTINCT FROM b`). Also gates the tokenizer: the `<=>` lexeme is munched
4388    /// (ahead of `<=`) only under a dialect that sets this, so elsewhere `a <=> b` stays
4389    /// `a` `<=` `>` `b` and surfaces as a clean parse error. It folds onto the canonical
4390    /// [`BinaryOperator::IsNotDistinctFrom`] operator; the
4391    /// [`IsNotDistinctFromSpelling`](crate::ast::IsNotDistinctFromSpelling) tag records the
4392    /// `<=>` spelling so it round-trips (MySQL rejects the keyword `IS NOT DISTINCT FROM`,
4393    /// so the spelling cannot be normalized away). Binds at comparison precedence, riding
4394    /// the shared comparison row like the other comparison operators.
4395    pub null_safe_equals: bool,
4396    /// Read an infix `->` whose left operand is a lambda-parameter list — a bare
4397    /// unqualified name, a parenthesized name list `(x, y)`, or the equivalent
4398    /// `ROW(x, y)` — as a DuckDB single-arrow lambda
4399    /// ([`Expr::Lambda`](crate::ast::Expr::Lambda)) instead of the JSON-arrow binary
4400    /// operator; any other left operand keeps the
4401    /// [`JsonGet`](crate::ast::BinaryOperator::JsonGet) reading.
4402    ///
4403    /// A grammar-position gate over a token another flag lexes: the `->` lexeme is
4404    /// munched only under [`json_arrow_operators`](Self::json_arrow_operators), so
4405    /// this flag is inert without it (the dependency is
4406    /// [`FeatureDependencyViolation::LambdaExpressionsWithoutJsonArrowOperators`]; no lexical
4407    /// trigger of its own, hence no [`LexicalConflict`] entry — the lexical registry covers
4408    /// shared *tokenizer* triggers, this one covers grammar dependencies). The node split mirrors the
4409    /// engine exactly (probed on DuckDB 1.5.4): DuckDB parses *every* `->` as a
4410    /// `LAMBDA` tree node — even `1 -> 2` or `t.a -> 'k'` — and defers the
4411    /// lambda-vs-JSON decision to bind time, where a lambda-consuming argument
4412    /// requires exactly the parameter shape above ("Parameters must be unqualified
4413    /// comma-separated names like x or (x, y)") and any other `->` is re-read as JSON
4414    /// extraction. Applying that bind-time shape test at parse time is a pure
4415    /// node-label choice: lambda `->` and JSON `->` share one token, one binding
4416    /// power, and one associativity, so acceptance is unchanged either way and the
4417    /// text round-trips identically. Position-independent, like the engine: a lambda
4418    /// parses anywhere an expression does (`SELECT x -> x + 1` parses in DuckDB; only
4419    /// the *binder* rejects an unconsumed lambda). Multi-parameter lists ride the
4420    /// implicit-row parse, so they additionally need
4421    /// [`ExpressionSyntax::row_constructor`] (on in every preset that sets this). On
4422    /// for DuckDB only.
4423    pub lambda_expressions: bool,
4424    /// Accept the shared bitwise operators — binary `|` (OR), `&` (AND), `<<`/`>>`
4425    /// (shift), and prefix `~` (complement) — over integers. On in PostgreSQL, MySQL,
4426    /// SQLite, and DuckDB (the family is cross-dialect); off in ANSI. A parser-acceptance
4427    /// gate over lexemes that always tokenize (`|`/`&`/`~` are operator-class bytes and
4428    /// `<<`/`>>` are maximal-munched unconditionally), so when off the operator ends the
4429    /// expression and the trailing operand surfaces as a clean parse error. Each operator's
4430    /// binding power lives in [`BindingPowerTable`], where the ranks diverge per dialect
4431    /// (MySQL splits `|` < `&` < `<<`/`>>`; PostgreSQL/SQLite/DuckDB share one rank).
4432    /// Bitwise *XOR* is a separate pair of knobs — [`FeatureSet::caret_operator`] for the
4433    /// `^` spelling and [`FeatureSet::hash_bitwise_xor`] for `#` — because XOR's spelling,
4434    /// lexing, and precedence all diverge (`#` vs `^`).
4435    pub bitwise_operators: bool,
4436    /// Accept the quantified subquery comparison `<expr> <cmp> {ANY | ALL | SOME}
4437    /// (<subquery>)` (SQL-92 F291), as in `a = ANY (SELECT …)` / `a > ALL (SELECT …)`.
4438    /// On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient. SQLite has no quantified comparison
4439    /// (it spells the same intent with `IN`/`EXISTS`), so it is off; the `ANY`/`ALL`/
4440    /// `SOME` keyword is then not read as a quantifier and surfaces as a clean parse
4441    /// error. Gates only the subquery-quantifier reading; `ALL`/`DISTINCT` as an
4442    /// aggregate-argument quantifier is the separate always-parsed set-quantifier.
4443    pub quantified_comparisons: bool,
4444    /// Accept the *list*-operand form of a quantified comparison — `<expr> <cmp>
4445    /// {ANY | ALL | SOME} (<value>)` where the parenthesized operand is a scalar
4446    /// list/array value rather than a subquery, as in DuckDB `ax = ANY (b)` /
4447    /// `x = ANY ([1, 2, 3])` and PostgreSQL `x = ANY (ARRAY[…])`. On for
4448    /// PostgreSQL/DuckDB/Lenient (which model it as PostgreSQL's `ScalarArrayOpExpr`,
4449    /// parsed to [`Expr::QuantifiedList`](crate::ast::Expr::QuantifiedList)); off for
4450    /// ANSI/MySQL, which admit only the subquery quantifier, and vacuously off for
4451    /// SQLite, which has no quantified comparison at all. Rides on top of
4452    /// [`quantified_comparisons`](Self::quantified_comparisons): when that gate leaves
4453    /// `ANY`/`ALL`/`SOME` unread this flag is unreachable, so it is only meaningfully
4454    /// set alongside it (the dependency is
4455    /// [`FeatureDependencyViolation::QuantifiedComparisonListsWithoutQuantifiedComparisons`]).
4456    /// When off, a non-subquery operand surfaces as the same clean
4457    /// "a subquery" parse error the standard form has always produced.
4458    pub quantified_comparison_lists: bool,
4459    /// Extend the quantifier `{ANY | ALL | SOME} (…)` past the six comparison
4460    /// operators to *any* infix operator — `<expr> <op> {ANY | ALL | SOME} (…)`
4461    /// where `<op>` is arithmetic (`+ - * / % ^`), string concatenation (`||`),
4462    /// bitwise (`& | # << >>`), or a comparison. PostgreSQL's grammar admits every
4463    /// operator in `MathOp`/`Op` here, only the boolean keywords `AND`/`OR` are
4464    /// excluded (engine-probed); the standard and the other dialects restrict the
4465    /// quantifier to the comparison operators. On for PostgreSQL/Lenient; off for
4466    /// ANSI/MySQL/DuckDB/SQLite. Rides on
4467    /// [`quantified_comparisons`](Self::quantified_comparisons) (and
4468    /// [`quantified_comparison_lists`](Self::quantified_comparison_lists) for the
4469    /// array-operand form): when the quantifier is unread this flag is unreachable (the
4470    /// dependency is [`FeatureDependencyViolation::QuantifiedArbitraryOperatorWithoutQuantifiedComparisons`]).
4471    /// When off, a non-comparison operator before `ANY`/`ALL`/`SOME` folds as an
4472    /// ordinary binary operator and the quantifier keyword then rejects as usual.
4473    pub quantified_arbitrary_operator: bool,
4474    /// Accept the general symbolic-operator surface — ANY operator drawn from the `Op`
4475    /// character class (`~ ! @ # ^ & | ? + - * / % < > =`), in both infix and prefix
4476    /// position, over a user-extensible operator set. A dialect-neutral capability any preset
4477    /// can enable; PostgreSQL (its `pg_operator`) is the current enabler, and the model
4478    /// follows its grammar. This is the ONE model for the whole
4479    /// tail (regex `~`/`!~`/`~*`/`!~*`; geometric/network/text-search `&&`/`&<`/`&>`/`<->`/
4480    /// `<<|`/`|>>`/`^@`/`##`/`<^`/`<%`/`@-@`; negator spellings `*<>`/`*>=`; the prefix
4481    /// `@`/`@@`/`|/`/`||/`/`!!`; and a fully user-defined `@#@`) rather than an enumerated
4482    /// per-lexeme set: the grammar admits every one as the same `Op`/`qual_Op`
4483    /// production, so a bare `a ~ b` is exactly `a OPERATOR(~) b`
4484    /// ([`Expr::NamedOperator`](crate::ast::Expr::NamedOperator) with the
4485    /// [`Bare`](crate::ast::NamedOperatorSpelling::Bare) spelling), and a prefix `@ x` is
4486    /// [`Expr::PrefixOperator`](crate::ast::Expr::PrefixOperator). Known operators still
4487    /// fold onto their dedicated [`BinaryOperator`] keys; only
4488    /// the remainder becomes a named operator.
4489    ///
4490    /// **Also gates the tokenizer.** Under this flag the operator scanner switches to
4491    /// PostgreSQL's maximal-munch lexer rule: a run of `Op` characters is one operator,
4492    /// truncated at an embedded `--`/`/*` comment start and with a trailing `+`/`-` stripped
4493    /// unless the run holds one of `~ ! @ # ^ & | ? %` (engine-measured: `a +- b` is
4494    /// `a + (- b)` but `a @- b` is one `@-` operator; `a <-- b` is `a <` then a `--`
4495    /// comment). The run that matches no built-in operator becomes an
4496    /// `Operator::Custom` token (the parser crate's tokenizer) carrying its span. Off leaves
4497    /// the fixed-form lexer untouched, so every other dialect's operator lexing is
4498    /// unchanged. The bare operators bind at the "any other operator" rank
4499    /// ([`BindingPowerTable::any_operator`](crate::precedence::BindingPowerTable::any_operator)),
4500    /// left-associative. On for PostgreSQL and DuckDB.
4501    ///
4502    /// The `Op` character class is not identical across enablers: DuckDB drops `#` and `?`
4503    /// (its positional-column `#1` and anonymous-parameter `?` sigils), so its operator runs
4504    /// stop at those bytes where PostgreSQL's do not — the lexer's `is_operator_char` reads
4505    /// the [`ExpressionSyntax::positional_column`] / [`ParameterSyntax::anonymous_question`]
4506    /// flags rather than hard-coding one charset. DuckDB *postfix* symbolic operators (`1 !`,
4507    /// removed from PostgreSQL in 14) are a separate axis this flag does not carry.
4508    ///
4509    /// The bare `@` operator shares its lead byte with the T-SQL `@name` / MySQL
4510    /// `@var` / `@@sysvar` sigils; the sigil arms win where a dialect enables them (the
4511    /// tracked [`LexicalConflict::CustomOperatorVersusAtName`] /
4512    /// [`LexicalConflict::CustomOperatorVersusSystemVariable`]), and no shipped preset
4513    /// enables both, so under PostgreSQL a bare `@` is always the operator.
4514    pub custom_operators: bool,
4515    /// Accept PostgreSQL/SQLite's one-word postfix null-test synonyms `<expr> ISNULL` and
4516    /// `<expr> NOTNULL` (for `IS NULL` / `IS NOT NULL`), folded onto
4517    /// [`Expr::IsNull`](crate::ast::Expr::IsNull) with a
4518    /// [`NullTestSpelling::Postfix`](crate::ast::NullTestSpelling) tag so they round-trip.
4519    /// A pure grammar gate at comparison precedence (the same non-associative rank as `IS
4520    /// NULL`): when off, the trailing `ISNULL`/`NOTNULL` keyword is left unconsumed and
4521    /// surfaces as a clean parse error (MySQL, which has no such synonym — `ISNULL(x)` is a
4522    /// *function* there, unaffected by this postfix gate). On for
4523    /// PostgreSQL/DuckDB/SQLite/Lenient.
4524    pub null_test_postfix: bool,
4525    /// Read a trailing symbolic operator with no following operand as a postfix operator
4526    /// application ([`Expr::PostfixOperator`](crate::ast::Expr::PostfixOperator)): `10!`
4527    /// (factorial), `1 ~`, `1 <->`, `1 &`. DuckDB keeps the generalized postfix reading
4528    /// PostgreSQL removed in version 14 (`a_expr Op %prec POSTFIXOP`), so it is the only
4529    /// enabler; PostgreSQL 14+ and every other preset reject the trailing operator.
4530    ///
4531    /// A pure parser-position gate, MECE against [`custom_operators`](Self::custom_operators):
4532    /// that flag owns the tokenizer's maximal-munch lexer and the *infix*/*prefix* general
4533    /// operator surface, while this flag owns only the *postfix* reduction of an already-lexed
4534    /// `Op`-class token. The infix reading still wins whenever an operand follows the operator
4535    /// (`1 ! + 2` is the infix `1 ! (+2)`); the postfix reduction fires only in the
4536    /// operand-absent position (`1 ! < 2`, `10!`, `1 ! FROM t`), engine-measured on DuckDB
4537    /// 1.5.4 via `duckdb_extract_statements` (parse-accept) — the unknown postfix operators
4538    /// then bind-reject (`Scalar Function !~__postfix does not exist`), an under-acceptance our
4539    /// parse-only parser closes by accepting the parse. The postfix binds at the "any other
4540    /// operator" left rank (looser than the arithmetic operators: `2 * 3 !` is `(2 * 3)!`).
4541    /// The postfix-eligible tokens are the general symbolic operators — the `Custom` residue,
4542    /// the lone `~`/`!`/`&&`, and the dedicated `& | << >> || <@ @> ^@`; the JSON arrows
4543    /// `->`/`->>` are excluded (DuckDB rejects them postfix). On for DuckDB, and for the
4544    /// permissive Lenient union (a pure additive parser position with no contended trigger,
4545    /// though only the always-lexed `Op` tokens reach it there — `custom_operators` is off
4546    /// under Lenient for the `@`-sigil conflict).
4547    pub postfix_operators: bool,
4548}
4549
4550/// Dialect-owned function-*call* syntax accepted by the parser.
4551///
4552/// The call-tail and special-call-form family, split out of [`ExpressionSyntax`] when it
4553/// crossed its 16-field line; the aggregate/window call forms and the `StringFunc` keyword
4554/// special forms later split out again into [`AggregateCallSyntax`] and [`StringFuncForms`]
4555/// at their own 16-field lines. Each flag decides whether the parser *admits* the form at
4556/// its call-grammar position, and when off the introducing keyword/arrow is left unconsumed
4557/// and surfaces as a clean parse error (`named_argument` additionally gates the `=>`/`:=`
4558/// tokenizer lexemes).
4559#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4560pub struct CallSyntax {
4561    /// Accept PostgreSQL named function arguments `f(name => value)` and the
4562    /// deprecated `f(name := value)`. Also gates the tokenizer: the `=>` / `:=`
4563    /// arrow lexemes are munched only under a dialect that sets this.
4564    pub named_argument: bool,
4565    /// Accept MySQL's `UTC_DATE` / `UTC_TIME` / `UTC_TIMESTAMP` niladic date/time
4566    /// functions — the UTC-clock analogues of the `CURRENT_*` special value functions,
4567    /// sharing the same nullary (plus optional precision on the time forms) grammar
4568    /// production. A parser-acceptance gate only: the keywords always tokenize (they are
4569    /// non-reserved outside MySQL), so when off they stay ordinary column/function names.
4570    /// Expression position only — MySQL has no PostgreSQL-style `func_table` promotion.
4571    pub utc_special_functions: bool,
4572    /// Read `COLUMNS(<selector>)` as DuckDB's star-expression column selector
4573    /// ([`Expr::Columns`](crate::ast::Expr::Columns)) rather than an ordinary call to a
4574    /// function named `columns`. A grammar-position gate keyed on the (non-reserved)
4575    /// `COLUMNS` keyword immediately followed by `(`: DuckDB has no user function of
4576    /// that spelling, so the form is unambiguous once the flag is on (the tokenizer is
4577    /// untouched — the disambiguation is a parser lookahead, mirroring `ARRAY[`/`ROW(`).
4578    /// A bare `columns` with no `(` stays an identifier in every dialect. Separate from
4579    /// [`SelectSyntax::wildcard_modifiers`](SelectSyntax::wildcard_modifiers) because
4580    /// the surfaces sit in different grammar positions — `COLUMNS(…)` is an expression
4581    /// (it nests in `sum(COLUMNS(*))`, `COLUMNS(*)::JSON`), the `*` modifiers are a
4582    /// projection-item tail — and are read by different parser code (expression vs
4583    /// select-item). When off, `COLUMNS(x)` parses as a plain function call, matching
4584    /// every non-DuckDB dialect. On for DuckDB / Lenient, off elsewhere.
4585    pub columns_expression: bool,
4586    /// Accept the `EXTRACT(<field> FROM <source>)` datetime-field extraction special
4587    /// form (SQL-92 F052; PostgreSQL). On for ANSI/PostgreSQL/MySQL/DuckDB/Lenient.
4588    /// SQLite has no `EXTRACT` (its date functions are `strftime`/`date`/…), so it is
4589    /// off; `EXTRACT` then reads as an ordinary identifier/function name and the `FROM`
4590    /// inside its parentheses surfaces as a clean parse error. Gates only the
4591    /// `FROM`-separated form — an ordinary `extract(a, b)` call is unaffected.
4592    pub extract_from_syntax: bool,
4593    /// Read `TRY_CAST(<expr> AS <type>)` as DuckDB's null-on-failure cast — the same
4594    /// [`Expr::Cast`](crate::ast::Expr::Cast) shape as `CAST`, distinguished by the
4595    /// canonical `try` flag (DuckDB's own serialized tree carries `try_cast: true`), not
4596    /// a spelling tag: null-on-failure is different *semantics*, not a different spelling
4597    /// of `CAST`. A grammar-position gate keyed on the (DuckDB-only) `TRY_CAST`
4598    /// keyword immediately followed by `(`; when off, `TRY_CAST` reads as an ordinary
4599    /// function name and the `AS` inside its parentheses surfaces as a clean parse error,
4600    /// matching every non-DuckDB dialect (verified: PostgreSQL syntax-errors at `AS`). On
4601    /// for DuckDb/Lenient.
4602    pub try_cast: bool,
4603    /// Restrict the `CAST(<expr> AS <target>)` target to MySQL's narrow `cast_type`
4604    /// grammar rather than the full column-type vocabulary. Off for
4605    /// ANSI/PostgreSQL/SQLite/DuckDB/Lenient, whose casts admit any type name. On for
4606    /// MySQL, whose `CAST`/`CONVERT` target is a closed set —
4607    /// `SIGNED`/`UNSIGNED [INTEGER|INT]`, `CHAR`/`NCHAR`/`CHARACTER`/`NATIONAL CHAR`,
4608    /// `BINARY`, `DATE`, `DATETIME`, `TIME`, `DECIMAL`/`DEC`, `DOUBLE`/`DOUBLE PRECISION`,
4609    /// `FLOAT`, `REAL`, `JSON`, `YEAR`, and the spatial types (`POINT`, `LINESTRING`,
4610    /// `POLYGON`, `MULTIPOINT`, `MULTILINESTRING`, `MULTIPOLYGON`, `GEOMETRYCOLLECTION`, and
4611    /// the `GEOMCOLLECTION` alias) — so the common column types `INT`/`INTEGER`/
4612    /// `SMALLINT`/`BIGINT`/`TINYINT`, `VARCHAR`/`TEXT`, `TIMESTAMP`, `NUMERIC`, `BOOLEAN`,
4613    /// `VARBINARY`/`BLOB`/`BIT`, bare `GEOMETRY`, and any user-defined name are the syntax
4614    /// error MySQL reports in cast position (while remaining valid as column types). When on,
4615    /// the parser parses the target type as usual, then rejects it unless it is one of the
4616    /// MySQL cast targets (the whole set engine-measured on mysql:8 —
4617    /// `mysql-faithful-cast-type-production`).
4618    ///
4619    /// `YEAR` and the spatial cast targets parse as user-defined names, so the faithful
4620    /// production layers a name allowlist over the shape check (the parser's
4621    /// `is_mysql_cast_target`); the inert trailing `INTEGER`/`INT` of `SIGNED`/`UNSIGNED` is
4622    /// folded onto the standalone numeric modifier. The one engine-measured residual is the
4623    /// `CHAR` charset annotation
4624    /// (`CAST(x AS CHAR CHARACTER SET utf8mb4)`, and the `ASCII`/`UNICODE`/trailing-`BINARY`
4625    /// shorthands): MySQL accepts these, but type-name charset/collation is a general MySQL
4626    /// feature the shared type grammar does not model at all (columns take the same
4627    /// annotation), so it is over-rejected here — a separate type-grammar feature, not a
4628    /// cast-target-membership gap, and invisible to every corpus.
4629    pub restricted_cast_targets: bool,
4630    /// Accept a string literal as the `EXTRACT('<field>' FROM <source>)` field, as DuckDB
4631    /// admits (`extract('year' FROM x)`), storing the quoted field as an
4632    /// [`Ident`](crate::ast::Ident) with [`QuoteStyle::Single`](crate::ast::QuoteStyle) so
4633    /// it round-trips. On for Postgres/DuckDb/Lenient. When off the field is a bare
4634    /// identifier only, so a leading string surfaces as a clean parse error (the standard
4635    /// `EXTRACT` field is an identifier). PostgreSQL admits the same quoted field (`Sconst`
4636    /// in its `extract_arg`) — engine-verified against pg_query, including the reject
4637    /// boundary (a non-string non-identifier field rejects on both) — so the flag ships on
4638    /// under the `Postgres` preset too.
4639    pub extract_string_field: bool,
4640    /// Accept DuckDB's dot-method call chaining on a value — a postfix `.<method>(<args>)`
4641    /// on a non-name receiver (`list(forecast).list_transform(x -> x + 10)`) that desugars
4642    /// to the ordinary function call `<method>(<receiver>, <args>)`. On for DuckDb/Lenient.
4643    /// A plain `name.method(args)` is already the schema-qualified call the object-name
4644    /// grammar reads, so the postfix fires only on a receiver that is not a bare name (a
4645    /// function-call result, a parenthesized expression, …); there is no ambiguity with a
4646    /// qualified call. When off, a `.method(` after a value surfaces as a clean parse error.
4647    pub method_chaining: bool,
4648    /// Reject an empty argument list on PostgreSQL's SQL/JSON constructor keywords — the
4649    /// closed set `PG_SQLJSON_EMPTY_REJECTING_CONSTRUCTORS` in the parser crate (`JSON`,
4650    /// `JSON_SCALAR`, `JSON_SERIALIZE`). PostgreSQL parses these through dedicated `gram.y`
4651    /// productions that require the context-item / value argument, so `JSON()` /
4652    /// `JSON_SCALAR()` / `JSON_SERIALIZE()` is a syntax error, whereas we would otherwise
4653    /// admit them as ordinary niladic calls. Keyed on a single *unquoted* name, so a quoted
4654    /// `"json"()` stays an ordinary call PostgreSQL accepts (rejected only at name
4655    /// resolution). On for PostgreSQL only. The set is deliberately narrow and extension-
4656    /// shaped: the JSON_VALUE/JSON_QUERY grammar ([[pg-sqljson-expression-functions]])
4657    /// carries the same arity floor via [`sqljson_expression_functions`](CallSyntax::sqljson_expression_functions).
4658    pub sqljson_constructors_require_argument: bool,
4659    /// Parse the SQL:2016/2023 SQL/JSON expression functions as dedicated special forms:
4660    /// the `JSON_VALUE`/`JSON_QUERY`/`JSON_EXISTS` query functions with their
4661    /// `PASSING`/`RETURNING`/`FORMAT JSON`/wrapper/quotes/`ON EMPTY`/`ON ERROR` clause
4662    /// tails, the `JSON_OBJECT`/`JSON_ARRAY` constructors and their `JSON_OBJECTAGG`/
4663    /// `JSON_ARRAYAGG` aggregates (with `[ABSENT|NULL] ON NULL` and `[WITH|WITHOUT]
4664    /// UNIQUE [KEYS]`), the bare `JSON`/`JSON_SCALAR`/`JSON_SERIALIZE` constructors, and
4665    /// the `IS [NOT] JSON [VALUE|ARRAY|OBJECT|SCALAR] [WITH|WITHOUT UNIQUE [KEYS]]`
4666    /// predicate. On for PostgreSQL/Lenient (engine-verified against `pg_query`); the
4667    /// clause grammar is PostgreSQL's raw-parse surface (per-function legality such as
4668    /// `JSON_VALUE` rejecting a wrapper is enforced, while the shared behaviour set that
4669    /// PostgreSQL only narrows during parse *analysis* is admitted uniformly). Off for
4670    /// MySQL/DuckDB/SQLite/ANSI: MySQL has its own JSON functions with a *different*
4671    /// grammar, DuckDB/SQLite have no SQL/JSON standard special forms, and those
4672    /// dialects keep the keywords as ordinary function/column names. When off, the
4673    /// keyword heads fall through to the ordinary call/name path exactly as before.
4674    /// Composes with [`sqljson_constructors_require_argument`](CallSyntax::sqljson_constructors_require_argument):
4675    /// the empty `JSON()`/`JSON_SCALAR()`/`JSON_SERIALIZE()` reject is enforced by these
4676    /// special forms requiring their argument (PostgreSQL), while a dialect with the
4677    /// arity floor *off* (Lenient) falls the empty form back to an ordinary niladic call.
4678    pub sqljson_expression_functions: bool,
4679    /// Parse the SQL:2006 SQL/XML expression functions as dedicated special forms: the
4680    /// `xmlelement`/`xmlforest`/`xmlconcat`/`xmlparse`/`xmlpi`/`xmlroot`/`xmlserialize`/
4681    /// `xmlexists` constructors — with their keyword-clause grammar inside the parens
4682    /// (`NAME <label>`, `xmlattributes(…)`, `{DOCUMENT|CONTENT}`, `{PRESERVE|STRIP}
4683    /// WHITESPACE`, `VERSION {…|NO VALUE}`, `STANDALONE {YES|NO|NO VALUE}`, `AS <type>
4684    /// [[NO] INDENT]`, `PASSING [BY {REF|VALUE}] …`) — and the `IS [NOT] DOCUMENT`
4685    /// predicate. On for PostgreSQL/Lenient (engine-verified against `pg_query`); the
4686    /// clause grammar is PostgreSQL's raw-parse surface. Off for MySQL/DuckDB/SQLite/ANSI:
4687    /// none of those dialects have the SQL/XML standard special forms, and they keep the
4688    /// `xml*` keywords as ordinary function/column names. When off, the keyword heads fall
4689    /// through to the ordinary call/name path exactly as before. The `xmlagg` aggregate is
4690    /// *not* gated here — it is an ordinary keyword-free aggregate name that already parses
4691    /// through the ordinary aggregate call path in every dialect.
4692    pub xml_expression_functions: bool,
4693    /// Accept the call-site `VARIADIC` argument marker that spreads an array over a
4694    /// variadic parameter (`f(a, VARIADIC arr)`, `f(VARIADIC name => arr)`), riding the
4695    /// shared [`FunctionArg`](crate::ast::FunctionArg)'s
4696    /// [`variadic`](crate::ast::FunctionArg::variadic) flag. On for PostgreSQL/DuckDb/
4697    /// Lenient (both engines parse-accept it with identical rules — engine-probed on
4698    /// pg_query PG-17 and DuckDB 1.5.4). A parse-layer gate: the parser admits the
4699    /// `VARIADIC` prefix only on the *last* argument of the list and rejects it alongside
4700    /// an `ALL`/`DISTINCT` quantifier, mirroring both engines' `gram.y` productions
4701    /// (`func_application: … ',' VARIADIC func_arg_expr`), which carry no quantifier and
4702    /// place `VARIADIC` last. When off (ANSI/MySQL/SQLite), the `VARIADIC` keyword is left
4703    /// unconsumed and surfaces as a clean parse error. The argument-type check (the spread
4704    /// value must be an array) is a binding concern neither parser enforces.
4705    pub variadic_argument: bool,
4706    /// Accept PostgreSQL's `merge_action()` — the zero-argument special function that
4707    /// reports which `MERGE` branch produced a row (`'INSERT'`/`'UPDATE'`/`'DELETE'`),
4708    /// valid only in a `MERGE ... RETURNING` list. PostgreSQL gives it a dedicated
4709    /// `func_expr_common_subexpr` production (`MERGE_ACTION '(' ')'`), so at raw parse it
4710    /// is accepted *anywhere* an expression is (the MERGE-RETURNING-only restriction is a
4711    /// parse-*analysis* check, engine-verified against `pg_query`: `SELECT merge_action()`
4712    /// raw-parse-accepts) but takes strictly empty parens — `merge_action(1)` and
4713    /// `merge_action() OVER ()` are both syntax errors (probed). The keyword is reserved
4714    /// against ordinary calls (`POSTGRES_NON_GENERIC_FUNCTION_KEYWORDS`), so when off it
4715    /// stays the "no call form" reject it already was; when on, the strictly niladic form
4716    /// parses to the canonical [`Expr::Function`](crate::ast::Expr::Function) shape (name
4717    /// `merge_action`, no arguments). On for PostgreSQL/Lenient; off elsewhere (no other
4718    /// shipped dialect has the form). A bare `merge_action` with no `(` is untouched.
4719    pub merge_action_function: bool,
4720    /// Accept MySQL's `CONVERT` special-form function — both the comma-form cast
4721    /// `CONVERT(<expr>, <type>)` and the transcoding `CONVERT(<expr> USING <charset>)`
4722    /// form (grammar's one `CONVERT '(' … ')'` production, two shapes). Only `CONVERT`
4723    /// immediately followed by `(` opens it; when off, `CONVERT` keeps its ordinary
4724    /// function-name reading (PostgreSQL's plain `convert(bytea, name, name)` call is
4725    /// unaffected — engine-verified against `pg_query`, which parses `CONVERT('x', 'a', 'b')`
4726    /// as an ordinary call and rejects the `USING` form). The comma form folds onto the
4727    /// [`Expr::Cast`](crate::ast::Expr::Cast) node as [`CastSyntax::Convert`](crate::ast::CastSyntax)
4728    /// and shares [`restricted_cast_targets`](CallSyntax::restricted_cast_targets)' `cast_type`
4729    /// gate (so `CONVERT(1, INT)` rejects wherever `CAST(1 AS INT)` does); the USING form
4730    /// is a [`StringFunc::ConvertUsing`](crate::ast::StringFunc::ConvertUsing) whose charset
4731    /// operand is a MySQL `charset_name` (`ident_or_text` or the `BINARY` transcoding name).
4732    /// On for MySQL/Lenient; off elsewhere (no other shipped dialect has the special form).
4733    pub convert_function: bool,
4734}
4735
4736/// Dialect-owned keyword string/scalar special-form syntax accepted by the parser.
4737///
4738/// The keyword special forms that parse to the [`StringFunc`](crate::ast::StringFunc)
4739/// AST family — the SQL-standard string special forms and the sibling scalar keyword forms
4740/// sharing that node. Split out of [`CallSyntax`] at its 16-field line on the grammar
4741/// boundary "the flag's sole AST product is a `StringFunc` variant" — so the dual
4742/// cast/transcode `CONVERT` stays with the cast core. Each flag is a grammar gate: when off
4743/// the keyword head falls through to the ordinary call path or the trailing keyword
4744/// surfaces as a clean parse error.
4745#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4746pub struct StringFuncForms {
4747    /// Accept the `SUBSTRING(<expr> FROM <start> [FOR <count>])` keyword special form
4748    /// (SQL-92 E021-06). On for ANSI/PostgreSQL/DuckDB/MySQL/Lenient (each engine
4749    /// probed accepting); off for SQLite, which has no keyword string forms at all
4750    /// (probed: `near "FROM": syntax error`) — there the head falls through to the
4751    /// ordinary call path and the inner `FROM` is a clean parse error. The comma
4752    /// plain-call spelling `substring(x, 1, 2)` is untouched by this flag: it keeps
4753    /// parsing as an ordinary [`FunctionCall`](crate::ast::FunctionCall) everywhere
4754    /// (every probed engine accepts it). MySQL additionally requires the `(` adjacent
4755    /// to the head for the keyword form — the same `IGNORE_SPACE`-off demotion the
4756    /// aggregates and `EXTRACT` follow, composed via
4757    /// [`aggregate_args_require_adjacent_paren`](AggregateCallSyntax::aggregate_args_require_adjacent_paren)
4758    /// (probed: spaced `SUBSTRING ('a' FROM 2)` is 1064 while spaced
4759    /// `SUBSTRING ('a', 2)` parse-accepts through the demoted generic path).
4760    pub substring_from_for: bool,
4761    /// Accept the `FOR`-leading `SUBSTRING` spellings: the bare
4762    /// `SUBSTRING(<expr> FOR <count>)` and the reversed
4763    /// `SUBSTRING(<expr> FOR <count> FROM <start>)` order. On for
4764    /// PostgreSQL/DuckDB/Lenient (both engines probed accepting both orders); off for
4765    /// MySQL (probed 1064 on both — its grammar is strictly `FROM`-first), ANSI
4766    /// (SQL-92's `<character substring function>` is `FROM`-first only), and SQLite.
4767    pub substring_leading_for: bool,
4768    /// Accept PostgreSQL's `SUBSTRING(<expr> SIMILAR <pattern> ESCAPE <escape>)`
4769    /// regex form (SQL:1999's regular-expression substring function). On for
4770    /// PostgreSQL/Lenient; off for DuckDB (probed: parser error — its PG-fork grammar
4771    /// dropped the production), MySQL, SQLite, and ANSI (an optional-feature form only
4772    /// PostgreSQL ships). The `ESCAPE` operand is mandatory, matching pg_query
4773    /// (`SUBSTRING(x SIMILAR p)` rejects, probed).
4774    pub substring_similar: bool,
4775    /// Reject a `substring(…)`/`substr(…)` plain (comma) call whose argument count is
4776    /// not 2 or 3 — MySQL's dedicated grammar admits exactly `(str, pos)`,
4777    /// `(str, pos, len)`, and the `FROM`/`FOR` keyword forms, so `SUBSTRING('a')`,
4778    /// `SUBSTRING()`, and `SUBSTRING('a', 2, 3, 4)` are `ER_PARSE_ERROR` (1064) on
4779    /// mysql:8.4 (all probed) while PostgreSQL parse-accepts any arity
4780    /// (`SUBSTRING()` probed accepted — arity is a catalog lookup there, not
4781    /// grammar). On for MySQL only. Only the *adjacent-paren* call is checked: a
4782    /// spaced `SUBSTRING ('a')` is MySQL's demoted stored-function path, which
4783    /// parse-accepts any arity (probed 1046, a binding-class reject).
4784    pub substring_plain_call_requires_2_or_3_args: bool,
4785    /// Accept the `SUBSTR` head for the same `FROM`/`FOR` keyword forms
4786    /// (`SUBSTR(str FROM 2 FOR 3)`). On for MySQL/Lenient: MySQL's `SUBSTR` is a
4787    /// full synonym of `SUBSTRING` including the keyword grammar (probed accepted),
4788    /// while PostgreSQL and DuckDB have no `SUBSTR` keyword at all — their `substr`
4789    /// is an ordinary catalog function, so the keyword form parse-rejects (both
4790    /// probed) and the flag stays off. `substr` is not a keyword in any dialect's
4791    /// inventory, so the head is matched textually on an unquoted call only — a
4792    /// quoted `"substr"(…)` stays an ordinary call, and every plain `substr(a, b)`
4793    /// call is untouched.
4794    pub substr_from_for: bool,
4795    /// Accept the `POSITION(<substr> IN <string>)` keyword special form (SQL-92
4796    /// E021-11). On for ANSI/PostgreSQL/DuckDB/MySQL/Lenient; off for SQLite (no
4797    /// keyword form; its `position(a, b)` stays an ordinary call that fails only at
4798    /// binding). There is NO plain-call fallback where the flag is on:
4799    /// `position('b', 'abc')` is a parse error on PostgreSQL, DuckDB, *and* MySQL
4800    /// (all probed), so a comma after the first operand surfaces as a clean parse
4801    /// error rather than re-reading as a generic call. The operands are the
4802    /// restricted `b_expr` (PostgreSQL's `position_list`, DuckDB inheriting it —
4803    /// `POSITION('a' = 'b' IN 'c')` parse-accepts on both while
4804    /// `POSITION(1 IN 2 OR 3)` rejects, both probed); MySQL's asymmetric grammar is
4805    /// [`position_asymmetric_operands`](StringFuncForms::position_asymmetric_operands).
4806    pub position_in: bool,
4807    /// Use MySQL's asymmetric `POSITION` operand grammar — `bit_expr IN expr` — in
4808    /// place of the standard symmetric `b_expr IN b_expr`. The needle tightens to
4809    /// MySQL's `bit_expr` (arithmetic/bit operators only — no comparisons:
4810    /// `POSITION('a' = 'b' IN 'c')` is 1064, probed) and the haystack widens to a
4811    /// full expression (`POSITION(1 IN 2 OR 3)` accepts, probed). On for MySQL only.
4812    pub position_asymmetric_operands: bool,
4813    /// Accept the `OVERLAY(<target> PLACING <replacement> FROM <start> [FOR <count>])`
4814    /// keyword special form (SQL:1999 T312). On for ANSI/PostgreSQL/DuckDB/Lenient;
4815    /// off for MySQL (no `OVERLAY` at all — the keyword form is 1064 and a plain
4816    /// `overlay(…)` call parses as a stored-function reference, probed) and SQLite.
4817    /// `FROM <start>` is mandatory: `OVERLAY(x PLACING y)` and
4818    /// `OVERLAY(x PLACING y FOR 1)` parse-reject on PostgreSQL and DuckDB (probed).
4819    pub overlay_placing: bool,
4820    /// Require the `PLACING` form after `OVERLAY(` — i.e. drop the plain-call
4821    /// fallback. DuckDB's grammar has *only* the `PLACING` production:
4822    /// `overlay('abc', 'X', 2, 1)`, `overlay('abc')`, and `overlay()` are all parser
4823    /// errors there (probed), while PostgreSQL keeps its `func_arg_list_opt`
4824    /// alternative so the same spellings parse-accept (probed; arity is a catalog
4825    /// concern there). On for DuckDB and ANSI (the standard defines no plain
4826    /// `overlay` call); off for PostgreSQL/Lenient, where a non-`PLACING` argument
4827    /// list falls back to the ordinary call path.
4828    pub overlay_requires_placing: bool,
4829    /// Accept the restricted `TRIM([{BOTH | LEADING | TRAILING}] [<chars>] FROM
4830    /// <source>)` keyword special form (SQL-92 E021-09): a side and/or a
4831    /// trim-character expression, then `FROM` and exactly one source. On for
4832    /// ANSI/PostgreSQL/DuckDB/MySQL/Lenient; off for SQLite (probed: syntax error —
4833    /// its two-argument `trim(x, y)` plain call is the only spelling). The bare
4834    /// single-argument `TRIM(x)` stays an ordinary call everywhere, and `TRIM()`
4835    /// rejects wherever the flag is on (PostgreSQL/DuckDB/MySQL all probed rejecting
4836    /// the empty form). MySQL requires at least one of side/chars before `FROM`
4837    /// (`TRIM(FROM 'x')` is 1064, probed) and holds the keyword form to the adjacent
4838    /// `(` like `SUBSTRING` (spaced `TRIM (LEADING …)` is 1064 while spaced
4839    /// `TRIM ('abc')` parse-accepts through the demoted generic path, both probed);
4840    /// the looser PostgreSQL tails are [`trim_list_syntax`](StringFuncForms::trim_list_syntax).
4841    pub trim_from: bool,
4842    /// Accept PostgreSQL's loose `trim_list` tails on the `TRIM` special form: the
4843    /// bare `TRIM(FROM <list>)` (no side, no chars), a side without `FROM`
4844    /// (`TRIM(TRAILING ' foo ')`, `TRIM(LEADING 'x', 'y')`), a multi-expression
4845    /// source list (`TRIM('a' FROM 'b', 'c')`, `TRIM(BOTH FROM 'a', 'b')`), and the
4846    /// comma plain-call spelling `trim('a', 'b')` (which PostgreSQL parses through
4847    /// the same production and we keep as an ordinary call). On for
4848    /// PostgreSQL/DuckDB/Lenient (every listed form probed parse-accepting on both
4849    /// engines — DuckDB's rejects here are binder arity, not grammar); off for MySQL
4850    /// (each probed 1064), ANSI (the standard's trim operand takes one source), and
4851    /// SQLite. Where this is off but [`trim_from`](StringFuncForms::trim_from) is on, a comma
4852    /// after the first `TRIM` operand is a clean parse error — matching MySQL, whose
4853    /// `trim('a', 'b')` is 1064 (probed).
4854    pub trim_list_syntax: bool,
4855    /// Accept PostgreSQL's `COLLATION FOR (<expr>)` common-subexpr — the special form that
4856    /// reports the collation name derived for its operand. PostgreSQL gives it a dedicated
4857    /// `COLLATION FOR '(' a_expr ')'` production (lowered to a
4858    /// `pg_catalog.pg_collation_for(<expr>)` call), so only `COLLATION` immediately trailed
4859    /// by `FOR (` opens it; the parentheses and single `a_expr` operand are mandatory —
4860    /// `COLLATION FOR 'x'`, `COLLATION FOR ()`, and a two-argument list all reject
4861    /// (engine-verified against `pg_query`). When off, `COLLATION` keeps its ordinary
4862    /// `type_func_name` reading (a plain `collation(x)` call is unaffected either way). On
4863    /// for PostgreSQL/Lenient; off elsewhere (no other shipped dialect has the form —
4864    /// DuckDB overrides PostgreSQL's `true` back to `false`, its `COLLATION FOR` surface
4865    /// unprobed). The parsed form keeps its keyword shape as
4866    /// [`StringFunc::CollationFor`](crate::ast::StringFunc::CollationFor) so it round-trips
4867    /// as written rather than as the lowered call.
4868    pub collation_for_expression: bool,
4869    /// Accept the `CEIL`/`CEILING` rounding-field keyword form: `CEIL(<expr> TO <field>)`
4870    /// (and the `CEILING` spelling). No probed oracle grammar admits the `TO` tail —
4871    /// engine-verified against `pg_query` (`syntax error at or near "TO"`), DuckDB, and
4872    /// mysql:8.4.10 (all reject) — so this is sqlparser-rs-parity surface only, not a
4873    /// real-engine grammar; on for Lenient, off for every shipped engine preset. Only
4874    /// `CEIL`/`CEILING` immediately followed by `(` opens the speculative read (mirroring
4875    /// [`substring_from_for`](StringFuncForms::substring_from_for)'s shape): the first operand parses
4876    /// as an ordinary expression, and only a following `TO` commits to the special form —
4877    /// a first operand with no `TO` tail rewinds to the ordinary call path, so the comma
4878    /// scale spelling `CEIL(<expr>, <scale>)` is untouched and keeps parsing as a plain
4879    /// [`FunctionCall`](crate::ast::FunctionCall) in every dialect regardless of this flag.
4880    /// When off, `CEIL(x TO DAY)` is the same clean parse error it is today (an
4881    /// unexpected `TO` where a `,` or `)` is expected). The field (`DAY`, `HOUR`, …) is
4882    /// stored as a written [`Ident`](crate::ast::Ident), validated (if at all) by the
4883    /// consuming engine at analysis time, not parse. The parsed form is
4884    /// [`StringFunc::CeilTo`](crate::ast::StringFunc::CeilTo).
4885    pub ceil_to_field: bool,
4886    /// Accept the `FLOOR` rounding-field keyword form: `FLOOR(<expr> TO <field>)`. No
4887    /// probed oracle grammar admits the `TO` tail — engine-verified against `pg_query`
4888    /// (`syntax error at or near "TO"`), DuckDB, and mysql:8.4.10 (all reject) — so this
4889    /// is sqlparser-rs-parity surface only, not a real-engine grammar; on for Lenient,
4890    /// off for every shipped engine preset. Unlike `CEIL`/`CEILING`, `FLOOR` has no
4891    /// synonym spelling to track. Only `FLOOR` immediately followed by `(` opens the
4892    /// speculative read (mirroring [`ceil_to_field`](StringFuncForms::ceil_to_field)'s shape): the
4893    /// first operand parses as an ordinary expression, and only a following `TO` commits
4894    /// to the special form — a first operand with no `TO` tail rewinds to the ordinary
4895    /// call path, so the comma scale spelling `FLOOR(<expr>, <scale>)` is untouched and
4896    /// keeps parsing as a plain [`FunctionCall`](crate::ast::FunctionCall) in every
4897    /// dialect regardless of this flag. When off, `FLOOR(x TO DAY)` is the same clean
4898    /// parse error it is today (an unexpected `TO` where a `,` or `)` is expected). The
4899    /// field (`DAY`, `HOUR`, …) is stored as a written [`Ident`](crate::ast::Ident),
4900    /// validated (if at all) by the consuming engine at analysis time, not parse. The
4901    /// parsed form is [`StringFunc::FloorTo`](crate::ast::StringFunc::FloorTo).
4902    pub floor_to_field: bool,
4903    /// Accept MySQL's full-text `MATCH (<col>, …) AGAINST (<expr> [<modifier>])` special-form
4904    /// expression (grammar's `MATCH ident_list_arg AGAINST '(' bit_expr fulltext_options ')'`).
4905    /// Only `MATCH` immediately followed by `(` opens it; the column list is comma-separated
4906    /// column references (a general expression, literal, function call, or empty list all
4907    /// parse-reject), the `AGAINST` operand is a `bit_expr` (so a trailing `IN`/`WITH` opens the
4908    /// modifier rather than an `IN` predicate), and the optional modifier is exactly one of
4909    /// `IN NATURAL LANGUAGE MODE`, `IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION`,
4910    /// `IN BOOLEAN MODE`, or `WITH QUERY EXPANSION` (all engine-verified on mysql:8.4.10; the
4911    /// non-reserved `AGAINST`/`QUERY`/`EXPANSION` words are matched contextually, MySQL's
4912    /// reserved-only inventory design). The parsed form is
4913    /// [`StringFunc::MatchAgainst`](crate::ast::StringFunc::MatchAgainst). Distinct from SQLite's
4914    /// infix `<expr> MATCH <expr>` operator (a binding-power table entry, not this gate). On for
4915    /// MySQL/Lenient; off elsewhere (no other shipped dialect has the special form).
4916    pub match_against: bool,
4917}
4918
4919/// Dialect-owned aggregate/window function-call syntax accepted by the parser.
4920///
4921/// The call-grammar forms specific to aggregate and window function calls — the
4922/// in-parenthesis and post-`)` tails, the argument-shape and arity restrictions, and
4923/// the `OVER`-eligibility gate. Split out of [`CallSyntax`] at its 16-field line as
4924/// the aggregate/window axis, distinct from the scalar special-form and general
4925/// call-tail cores. Each flag is a grammar gate: when off the tail keyword is left
4926/// unconsumed and surfaces as a clean parse error, or the restriction does not fire.
4927#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4928pub struct AggregateCallSyntax {
4929    /// Accept the MySQL `GROUP_CONCAT(<args> [ORDER BY …] SEPARATOR <string>)` delimiter
4930    /// tail — the trailing `SEPARATOR '<sep>'` inside an aggregate call's parentheses,
4931    /// after any in-parenthesis `ORDER BY`. It rides the shared
4932    /// [`FunctionCall`](crate::ast::FunctionCall) shape's new
4933    /// [`separator`](crate::ast::FunctionCall::separator) field, gated by
4934    /// grammar position like the always-parsed in-parenthesis `ORDER BY`. When off
4935    /// (ANSI/PostgreSQL, which write the delimiter as an ordinary `string_agg` argument),
4936    /// the `SEPARATOR` keyword is left unconsumed and the unmatched `)` surfaces as a
4937    /// clean parse error.
4938    pub group_concat_separator: bool,
4939    /// Accept the `WITHIN GROUP (ORDER BY …)` ordered-set-aggregate tail (SQL:2008
4940    /// T612/T614), as in `percentile_cont(0.5) WITHIN GROUP (ORDER BY x)`. On for
4941    /// ANSI/PostgreSQL/DuckDB/Lenient. Neither SQLite nor MySQL has ordered-set
4942    /// aggregates (both engine-measured-rejected), so it is off for both; the `WITHIN`
4943    /// keyword is then left unconsumed and surfaces as a clean parse error. Only the
4944    /// `WITHIN GROUP` pair opens the clause, so a bare `within` stays a usable alias
4945    /// regardless.
4946    pub within_group: bool,
4947    /// Accept the `FILTER (WHERE <predicate>)` aggregate-filter tail (SQL:2003 T612), as
4948    /// in `sum(x) FILTER (WHERE x > 1)`. On for ANSI/PostgreSQL/SQLite (3.30+)/DuckDB/
4949    /// Lenient. MySQL has no aggregate `FILTER` clause (engine-measured-rejected on
4950    /// mysql:8), so it is off there; the `FILTER` keyword is then left unconsumed and
4951    /// surfaces as a clean parse error. Only `FILTER` immediately followed by `(` opens the
4952    /// clause, so a bare `filter` after a call stays a usable alias.
4953    pub aggregate_filter: bool,
4954    /// Accept an aggregate `FILTER (…)` tail whose predicate is *not* preceded by the
4955    /// SQL-standard `WHERE` keyword, as in DuckDB's `sum(x) FILTER (x > 1)` (probed on
4956    /// 1.5.4). On for DuckDb/Lenient; the presence of the keyword round-trips via
4957    /// [`FunctionCall::filter_where`](crate::ast::FunctionCall::filter_where). Off for
4958    /// ANSI/PostgreSQL/SQLite, which require `FILTER (WHERE …)` — a keyword-less body then
4959    /// surfaces as a clean "expected `WHERE`" parse error. Independent of
4960    /// [`aggregate_filter`](Self::aggregate_filter): this only widens the *body* of a
4961    /// clause that gate already admits, so it is inert when the filter clause itself is off
4962    /// (MySQL).
4963    pub filter_optional_where: bool,
4964    /// Require a built-in aggregate's argument parentheses to be *adjacent* to its name
4965    /// for the aggregate-only argument forms — a leading `*`, a `DISTINCT`/`ALL` quantifier,
4966    /// the in-parenthesis `ORDER BY`, or the `SEPARATOR` tail. On for MySQL, off everywhere
4967    /// else. MySQL's default (`IGNORE_SPACE` off) tokenizer treats a space before the `(`
4968    /// as demoting a built-in aggregate to an ordinary/stored-function reference, where that
4969    /// aggregate-only argument grammar is illegal: `COUNT ( * )` / `MAX ( ALL 1 )` /
4970    /// `COUNT ( DISTINCT 1 )` are engine-measured `ER_PARSE_ERROR` (1064) on mysql:8, while
4971    /// the adjacent `COUNT(*)` accepts. A *normal*-argument spaced call is unaffected —
4972    /// `count (1)` still parses (it fails only at name resolution, a binding not a syntax
4973    /// error), so this narrowly rejects the aggregate-only forms rather than blanket-forbidding
4974    /// a space (which would over-reject the valid general-call form). When off, the name/paren
4975    /// gap is irrelevant and every dialect admits the aggregate forms with or without the space.
4976    /// Only the default `IGNORE_SPACE`-off mode is modelled; the runtime `sql_mode` toggle to
4977    /// `IGNORE_SPACE` on (which would accept the spaced aggregate forms) is out of scope.
4978    pub aggregate_args_require_adjacent_paren: bool,
4979    /// Accept the `IGNORE NULLS` / `RESPECT NULLS` null-treatment written *inside* a
4980    /// window/aggregate call's parentheses (DuckDB's `last(s IGNORE NULLS) OVER (…)`),
4981    /// riding the shared [`FunctionCall`](crate::ast::FunctionCall)'s
4982    /// [`null_treatment`](crate::ast::FunctionCall::null_treatment) field. On for
4983    /// DuckDb/Lenient. DuckDB spells it inside the parentheses (the SQL:2016 post-`)`
4984    /// position engine-rejects on 1.5.4), so it is parsed at the in-parenthesis tail after
4985    /// any `ORDER BY`. When off, `IGNORE`/`RESPECT` is left unconsumed and the unmatched `)`
4986    /// surfaces as a clean parse error. PostgreSQL has no null-treatment clause, so this
4987    /// stays a DuckDB extension rather than a shared PG form.
4988    pub null_treatment: bool,
4989    /// Reject an empty argument list `f()` on a MySQL built-in *aggregate* function —
4990    /// the closed set `MYSQL_AGGREGATE_FUNCTIONS` in the parser crate (`COUNT`, `SUM`,
4991    /// `AVG`, `MIN`, `MAX`, the `BIT_*`/`STD*`/`VAR*`/`VARIANCE` family, `GROUP_CONCAT`,
4992    /// `JSON_ARRAYAGG`/`JSON_OBJECTAGG`). MySQL's dedicated aggregate grammar requires at
4993    /// least one argument (or the `COUNT(*)` wildcard), so `COUNT()` is `ER_PARSE_ERROR`
4994    /// (1064) on mysql:8, while a niladic *non*-aggregate built-in (`NOW()`, `UUID()`,
4995    /// `PI()`) and an empty user-function call are accepted (the latter fails only at
4996    /// name resolution — a binding, not a syntax error — and a `CONCAT()`/`ABS()` empty
4997    /// call is a `wrong-parameter-count` semantic reject, also not a syntax error). On for
4998    /// MySQL, off elsewhere. When off, an empty aggregate call parses as an ordinary empty
4999    /// function call (every non-MySQL dialect admits it). Only a single *unquoted* name in
5000    /// the set matches — a backtick-quoted `` `count`() `` is a general call MySQL rejects
5001    /// at binding, not a syntax error, so it stays accepted — and the `COUNT(*)` wildcard
5002    /// and any argumented/quantified call are unaffected.
5003    pub aggregate_calls_reject_empty_arguments: bool,
5004    /// Restrict the `OVER (…)` window clause to MySQL's *windowable* functions — the
5005    /// built-in aggregates (`MYSQL_AGGREGATE_FUNCTIONS`) ∪ the dedicated window functions
5006    /// (`MYSQL_WINDOW_FUNCTIONS`: `ROW_NUMBER`, `RANK`, `DENSE_RANK`, `PERCENT_RANK`,
5007    /// `CUME_DIST`, `NTILE`, `LEAD`, `LAG`, `FIRST_VALUE`, `LAST_VALUE`, `NTH_VALUE`), both
5008    /// in the parser crate. MySQL grammatically admits `OVER` only on this set:
5009    /// `OVER` on an ordinary scalar built-in or a user function (`ABS(x) OVER ()`,
5010    /// `PERCENTILE_CONT(x, 0.5) OVER ()`, `ANY_VALUE(x) OVER ()`) is `ER_PARSE_ERROR`
5011    /// (1064) on mysql:8, while `SUM(x) OVER ()` / `ROW_NUMBER() OVER ()` /
5012    /// `GROUP_CONCAT(x) OVER ()` parse (they fail only at binding or a not-supported-yet
5013    /// semantic reject). On for MySQL, off elsewhere.
5014    ///
5015    /// The vocabulary must stay *complete*: an omission would over-*reject* a valid
5016    /// windowed call (the worse failure — no coverage-gap pin catches an over-rejection),
5017    /// so it is the engine-verified full MySQL 8.0 aggregate + window function list. A
5018    /// qualified name (`db.f(x) OVER ()`, engine-rejected too) is not a single-part member
5019    /// and is likewise rejected. When off, `OVER` attaches to any call, matching every
5020    /// non-MySQL dialect.
5021    ///
5022    /// This flag also gates the *converse* half of MySQL's dedicated window-function
5023    /// grammar — the requirements the pure window functions (`MYSQL_WINDOW_FUNCTIONS`,
5024    /// admitted as call heads by carving them out of `MYSQL_RESERVED_FUNCTION_NAME`) carry
5025    /// once admitted. Unlike the aggregates (whose `OVER` is optional), each window
5026    /// function *requires* an `OVER` clause, takes a *fixed* positional argument arity
5027    /// (`ROW_NUMBER`/`RANK`/`DENSE_RANK`/`PERCENT_RANK`/`CUME_DIST` exactly 0, `NTILE`/
5028    /// `FIRST_VALUE`/`LAST_VALUE` exactly 1, `LEAD`/`LAG` 1–3, `NTH_VALUE` exactly 2), and
5029    /// rejects the aggregate-only argument forms (`*`, a `DISTINCT`/`ALL` quantifier, an
5030    /// in-parenthesis `ORDER BY`, a `SEPARATOR`) — each violation an `ER_PARSE_ERROR`
5031    /// (1064) on mysql:8 (`ROW_NUMBER()` without `OVER`, `ROW_NUMBER(1) OVER ()`,
5032    /// `NTILE() OVER ()`, `RANK(DISTINCT a) OVER ()`). These are one indivisible dialect
5033    /// grammar, so they share the flag rather than a second knob that would have to
5034    /// co-vary with it. The parser enforces them in `parse_function_call`.
5035    pub over_requires_windowable_function: bool,
5036    /// Accept MySQL's window-function post-`)` tail — the SQL:2016
5037    /// `[FROM {FIRST | LAST}] [{RESPECT | IGNORE} NULLS]` clauses written *between* a
5038    /// null-treatment window function's argument `)` and its `OVER` clause, riding the
5039    /// shared [`FunctionCall`](crate::ast::FunctionCall)'s
5040    /// [`window_tail`](crate::ast::FunctionCall::window_tail) field. On for MySQL, off
5041    /// everywhere else. Engine-verified on mysql:8, the accepted surface is narrow: the
5042    /// null treatment is admitted only on `LEAD`/`LAG`/`FIRST_VALUE`/`LAST_VALUE`/
5043    /// `NTH_VALUE` and only as `RESPECT NULLS` (`IGNORE NULLS` grammar-admits but
5044    /// feature-rejects, `ER_NOT_SUPPORTED_YET` 1235); `FROM {FIRST | LAST}` is admitted
5045    /// only on `NTH_VALUE` and only as `FROM FIRST` (`FROM LAST` likewise 1235); the two
5046    /// clauses appear in that fixed order (the reverse is `ER_PARSE_ERROR`, 1064), and
5047    /// both sit strictly after the `)` (the in-paren spelling MySQL rejects, unlike
5048    /// DuckDB's [`null_treatment`](AggregateCallSyntax::null_treatment)). Keyed on a single
5049    /// *unquoted* window-function name — a quoted `` `nth_value` `` or qualified
5050    /// `db.nth_value` takes the general-call path (rejected there), matching the engine.
5051    /// When off, the tail keywords are left unconsumed and the trailing text surfaces as
5052    /// a clean parse error. The parser enforces the per-function admission in
5053    /// `parse_window_function_tail`.
5054    pub window_function_tail: bool,
5055    /// Accept a bare in-parenthesis `ORDER BY` as the sole content of a call's argument
5056    /// list — no positional argument preceding it (`rank(ORDER BY x) OVER w`,
5057    /// `cume_dist(ORDER BY x DESC) OVER w`). DuckDB lets a window/rank function carry its
5058    /// ordering inside the call parentheses instead of the `OVER (ORDER BY …)` clause; the
5059    /// [`order_by`](crate::ast::FunctionCall::order_by) list is the same field the
5060    /// arguments-then-`ORDER BY` form (`array_agg(x ORDER BY y)`) already fills, so the
5061    /// call shape is unchanged — only the empty *positional* list is new. On for
5062    /// DuckDb/Lenient, off elsewhere: standard SQL / PostgreSQL / MySQL require at least
5063    /// one argument before an aggregate `ORDER BY` (engine-verified), so when off the
5064    /// leading `ORDER` keyword falls into the argument-expression grammar where the
5065    /// reserved word surfaces as a clean parse error.
5066    ///
5067    /// A parse-level gate only. The per-function validity of the standalone form is a
5068    /// binding concern DuckDB enforces after parsing — `sum`/`array_agg`/`string_agg` and
5069    /// a bare `rank(ORDER BY x)` with no `OVER` are DuckDB *binder*/*catalog* rejects (the
5070    /// standalone form parses), so a parse-only parser correctly accepts them here; the one
5071    /// exception is DuckDB's parser-level "ORDER BY is not supported for the window function
5072    /// `dense_rank`", a fine-grained per-function restriction this gate deliberately does not
5073    /// model (see `duckdb-order-by-in-agg-args-trailing-comma`).
5074    pub standalone_argument_order_by: bool,
5075}
5076
5077/// Dialect-owned predicate-form syntax accepted by the parser.
5078///
5079/// The dialect-gated predicate forms beyond the always-available comparison / `IS` / `IN`
5080/// baseline. They are predicates (non-chaining, comparison precedence), distinct from the
5081/// [`ExpressionSyntax`] postfix/constructor forms, and — unlike those — not all PostgreSQL
5082/// extensions: some are SQL Core surface on in every shipped dialect (`LIKE`, SQL-92 /
5083/// SQL:2016 **Core E021-08**), while the rest are gated per dialect. Each is a pure grammar
5084/// gate: when off, the keyword is left unconsumed and the trailing operand surfaces as a
5085/// clean parse error — the same reject mechanism the other syntax gates use.
5086#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5087pub struct PredicateSyntax {
5088    /// Accept `<expr> [NOT] LIKE <pattern> [ESCAPE <c>]` (SQL-92 core E021-08). On in
5089    /// every dialect — this is the standard pattern-match predicate.
5090    pub like: bool,
5091    /// Accept `<expr> [NOT] ILIKE <pattern> [ESCAPE <c>]` case-insensitive matching
5092    /// (PostgreSQL).
5093    pub ilike: bool,
5094    /// Accept `<expr> [NOT] SIMILAR TO <pattern> [ESCAPE <c>]` regex matching
5095    /// (SQL:1999 F841; PostgreSQL).
5096    pub similar_to: bool,
5097    /// Accept the SQL-standard `(s1, e1) OVERLAPS (s2, e2)` period predicate (SQL:2016
5098    /// F251) — the `row OVERLAPS row` form yielding a boolean, both operands
5099    /// exactly-two-element rows (a bare parenthesized pair or `ROW(...)`). A pure grammar
5100    /// gate: when off, the `OVERLAPS` keyword is left unconsumed and surfaces as a clean
5101    /// parse error. The parser enforces the two-element-row operand shape on both sides
5102    /// (a scalar, a single-element grouping `(a)`, or a three-element row is rejected,
5103    /// matching PostgreSQL's grammar-level wrong-arity error), so a value stored here only
5104    /// opens the predicate — validity of the operands is still checked. PostgreSQL-only
5105    /// among the shipped presets (DuckDB, MySQL, and SQLite all reject the form,
5106    /// engine-probed); the Lenient union carries it too.
5107    pub overlaps_period_predicate: bool,
5108    /// Accept DuckDB's unparenthesized `<expr> [NOT] IN <value>` list-membership operator
5109    /// ([`Expr::InExpr`](crate::ast::Expr::InExpr)) — `z IN y`, distinct from the standard
5110    /// parenthesized `IN (list)` / `IN (subquery)`. DuckDB-only: the right operand is a
5111    /// restricted `c_expr` that may not begin with a constant or unary sign (`IN 4` / `IN
5112    /// -5` are DuckDB parser errors), so the parser gates on the leading token. Off in
5113    /// every non-DuckDB preset (PostgreSQL and the standard require the parentheses).
5114    pub unparenthesized_in_list: bool,
5115    /// Accept a pattern-match predicate quantified over an array operand: `<expr>
5116    /// [NOT] LIKE|ILIKE {ANY | ALL | SOME} (<array>)`
5117    /// ([`Expr::QuantifiedLike`](crate::ast::Expr::QuantifiedLike)) — PostgreSQL's
5118    /// `ScalarArrayOpExpr` over the `~~`/`~~*` operator. `SIMILAR TO` has no
5119    /// quantified form (PostgreSQL rejects it, engine-probed), so only `LIKE`/`ILIKE`
5120    /// open it. A pure grammar gate: when off, the `ANY`/`ALL`/`SOME` head after a
5121    /// pattern operator is left unconsumed and surfaces as the usual reject at the
5122    /// reserved quantifier keyword. PostgreSQL-only among the shipped presets; the
5123    /// Lenient union carries it too.
5124    pub pattern_match_quantifier: bool,
5125    /// Accept the SQL-standard `SYMMETRIC`/`ASYMMETRIC` modifier on the range predicate:
5126    /// `<expr> [NOT] BETWEEN {SYMMETRIC | ASYMMETRIC} <low> AND <high>` (SQL:2016 T461).
5127    /// `SYMMETRIC` is load-bearing — it permits `low > high` by testing against the ordered
5128    /// pair — and is kept on [`Expr::Between`](crate::ast::Expr::Between)'s `symmetric` flag;
5129    /// the default `ASYMMETRIC` is a noise word dropped on parse. A pure grammar gate: when
5130    /// off, the modifier keyword after `BETWEEN` is left unconsumed and surfaces as a clean
5131    /// parse error. `SYMMETRIC` is an *optional* standard feature (T461), so it stays off in
5132    /// the strict ANSI baseline (and thus in MySQL/SQLite/ClickHouse/Snowflake, which reuse
5133    /// `PredicateSyntax::ANSI` and reject the modifier); on for PostgreSQL (engine-probed on
5134    /// pg_query) and the Lenient union.
5135    pub between_symmetric: bool,
5136    /// Accept the SQL-standard Unicode-normalization test `<expr> IS [NOT]
5137    /// [NFC|NFD|NFKC|NFKD] NORMALIZED` (SQL:2016 T061), parsed to the postfix
5138    /// [`Expr::IsNormalized`](crate::ast::Expr::IsNormalized) predicate. A pure grammar gate:
5139    /// when off, the `NORMALIZED` continuation after `IS [NOT]` is left unconsumed and the
5140    /// null/truth reading rejects it. An optional standard feature, so off in the strict ANSI
5141    /// baseline (and thus in MySQL/SQLite/ClickHouse/Snowflake, which reuse
5142    /// `PredicateSyntax::ANSI` and reject it); on for PostgreSQL (engine-probed on pg_query)
5143    /// and the Lenient union.
5144    pub is_normalized: bool,
5145    /// Accept an empty parenthesized `IN` list — `<expr> [NOT] IN ()` — with no elements,
5146    /// parsed to an [`Expr::InList`](crate::ast::Expr::InList) whose `list` is empty. SQLite
5147    /// evaluates `x IN ()` to false and `x NOT IN ()` to true (engine-measured via rusqlite
5148    /// 3.53.2, where both `prepare`-accept). A pure grammar gate: when off, the closing `)` in
5149    /// list position is left unconsumed and the required-first-element reject stands — the
5150    /// standard `IN` predicate demands at least one element, so ANSI/PostgreSQL/MySQL/DuckDB
5151    /// syntax-reject the empty list. On for SQLite and the Lenient union, off elsewhere.
5152    pub empty_in_list: bool,
5153    /// Accept the two-word postfix null test `<expr> NOT NULL` (a synonym for `IS NOT NULL`),
5154    /// folded onto [`Expr::IsNull`](crate::ast::Expr::IsNull) with `negated: true` and a
5155    /// [`NullTestSpelling::PostfixNotNull`](crate::ast::NullTestSpelling) tag so it round-trips.
5156    /// A `NOT`-led predicate spelling, parsed at comparison precedence alongside the other
5157    /// `NOT`-led predicates (`NOT IN`/`NOT LIKE`/`NOT BETWEEN`); when off, the `NOT NULL` run is
5158    /// left unconsumed and the `NOT` surfaces as the ordinary prefix operator (or a clean parse
5159    /// error), so the two-word form is rejected.
5160    ///
5161    /// Distinct from the one-word [`OperatorSyntax::null_test_postfix`](OperatorSyntax) gate
5162    /// because the surfaces diverge (engine-measured): SQLite and DuckDB accept both spellings,
5163    /// but PostgreSQL — despite accepting the one-word `ISNULL`/`NOTNULL` — rejects the two-word
5164    /// `NOT NULL` postfix. On for SQLite and the Lenient union; off elsewhere (including
5165    /// PostgreSQL; DuckDB's acceptance is tracked separately).
5166    pub null_test_two_word_postfix: bool,
5167}
5168
5169/// A frozen SQL-standard edition, used to anchor feature availability to a release
5170/// without enumerating features (ZetaSQL's `LanguageVersion` checkpoints).
5171///
5172/// These are the feature-ID-era editions: the `Ennn`/`Fnnn`/`Tnnn` feature taxonomy
5173/// was introduced in SQL:1999, so it is the earliest anchor. Declaration order is
5174/// chronological, so `Ord` reads as "released no later than".
5175#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
5176pub enum StandardVersion {
5177    /// SQL:1999 — first edition with the named-feature taxonomy.
5178    Sql1999,
5179    /// SQL:2003 — adds windowed table functions (OLAP), MERGE, sequences.
5180    Sql2003,
5181    /// SQL:2008.
5182    Sql2008,
5183    /// SQL:2011.
5184    Sql2011,
5185    /// SQL:2016 — the edition "generic"/ANSI is anchored on.
5186    Sql2016,
5187}
5188
5189/// Small, typed dialect data consumed by parser and renderer code.
5190///
5191/// # Preset permissiveness spectrum
5192///
5193/// The shipped presets run from the strict standard to a parse-anything union; a
5194/// caller picks by how much non-standard SQL it must accept. The `squonk` crate's
5195/// `BuiltinDialect::ALL` is the authoritative selectable list — this doc groups those
5196/// presets by how they are held honest:
5197///
5198/// - [`ANSI`](Self::ANSI) — the strict SQL:2016 standard baseline: the
5199///   principled neutral default, accepting only standard surface.
5200/// - `POSTGRES` / `MYSQL` / `SQLITE` / `DUCKDB` — one real dialect's surface, strict in
5201///   its own idiom (each gated behind its cargo feature). These, with `ANSI`, are the
5202///   oracle-compared presets: each is held to its real engine by a differential
5203///   accept/reject oracle, so over-acceptance is a measured, gated zero.
5204/// - `LENIENT` — the permissive "parse anything" catch-all: the documented maximal
5205///   union tooling reaches for on SQL of unknown origin (gated behind the `lenient`
5206///   feature).
5207/// - The conservative, no-oracle presets — `BIGQUERY`, `HIVE`, `CLICKHOUSE`, `DATABRICKS`,
5208///   `MSSQL`, `SNOWFLAKE`, `REDSHIFT` (each behind its cargo feature) — derive from `ANSI`
5209///   and enable only the
5210///   surface that already has a modelled, tested parser gate and documentary evidence
5211///   (their `ON`s are evidence-cited, not oracle-proven). This workspace ships no oracle
5212///   for these engines, so their over-acceptance cannot be measured; conservatism is the
5213///   honesty bar, and they are deliberately excluded from the oracle conformance sets —
5214///   unsupported syntax is a clean reject routed to a follow-up ticket, never a silent
5215///   over-accept. Each preset's module doc spells out exactly what it adds over `ANSI`.
5216///
5217/// There is deliberately **no** permissive "generic" preset (a vibe-union is
5218/// banned): `ANSI` *is* the generic/standard baseline, and `LENIENT` is the
5219/// honest, spelled-out permissive end. This differs from `datafusion-sqlparser-rs`,
5220/// whose `GenericDialect` is a permissive catch-all — the equivalent here is
5221/// `LENIENT`, not the standard baseline. The runtime name `"generic"` aliases `ANSI`;
5222/// see `BuiltinDialect::from_name` in the `squonk` crate for the migration mapping.
5223///
5224/// # Shared byte-trigger ownership: `#`
5225///
5226/// A single-meaning byte folds into one meaning-enum (the `^` axis is
5227/// [`caret_operator`](Self::caret_operator): exactly one of exponent / bitwise-XOR / none
5228/// per dialect). The `#` byte is **not** such a byte, and this is why its claimants stay
5229/// separate flags on their own sub-structs rather than a single `HashMeaning` enum: `#` is
5230/// a shared *lead* byte whose readings **coexist** in one dialect, partitioned by scan
5231/// phase and follow byte, not mutually exclusive. PostgreSQL enables bare-`#` XOR **and**
5232/// the `#>`/`#>>`/`#-` jsonb operators at once; a single-valued enum could not represent
5233/// both. Its five claimants, in resolution order:
5234///
5235/// 1. `#` line comment ([`CommentSyntax::line_comment_hash`], MySQL) — consumed as trivia
5236///    before tokenizing, so when on it shadows every reading below.
5237/// 2. `#`+identifier-start word (a [`byte_classes`](Self::byte_classes) table marking `#`
5238///    [`CLASS_IDENTIFIER_START`](lex_class::CLASS_IDENTIFIER_START), T-SQL `#temp`) — the
5239///    identifier scan precedes the operator/positional arms, so it wins on an identifier
5240///    follow byte.
5241/// 3. `#`+digit positional column ([`ExpressionSyntax::positional_column`], DuckDB `#1`).
5242/// 4. `#`+`>`/`-` jsonb path operators ([`OperatorSyntax::jsonb_operators`], PostgreSQL) —
5243///    maximal-munched ahead of a bare `#`, so disjoint from the readings below by follow
5244///    byte; reaches the operator scanner only when `#` is routed there by claimant 5.
5245/// 5. bare `#` bitwise-XOR operator ([`hash_bitwise_xor`](Self::hash_bitwise_xor),
5246///    PostgreSQL) — the fallthrough when no earlier partition claims the byte.
5247///
5248/// Claimants 3/4/5 partition by follow byte and coexist freely; the *genuine* collisions
5249/// are only where a claimant is silently shadowed — the trivia phase (1 vs 2/3/5) and the
5250/// scan-order overlap (positional 3 vs bare-XOR 5). Those are the tracked pairwise
5251/// [`LexicalConflict`]s ([`HashCommentVersusHashIdentifier`](LexicalConflict::HashCommentVersusHashIdentifier),
5252/// [`HashXorOperatorVersusHashComment`](LexicalConflict::HashXorOperatorVersusHashComment),
5253/// [`HashXorOperatorVersusPositionalColumn`](LexicalConflict::HashXorOperatorVersusPositionalColumn),
5254/// [`HashCommentVersusPositionalColumn`](LexicalConflict::HashCommentVersusPositionalColumn)),
5255/// asserted absent on every shipped preset. That registry — one flag per behaviour, plus a
5256/// conflict entry per silently-shadowing pair — is the correct model for a coexisting-lead
5257/// byte; a `HashMeaning` meaning-enum would falsely impose mutual exclusion.
5258#[derive(Clone, Debug, PartialEq, Eq)]
5259pub struct FeatureSet {
5260    /// Identity fold for unquoted identifiers; exact text remains interned.
5261    pub identifier_casing: Casing,
5262    /// Identifier quote styles the dialect accepts (one or more; symmetric or
5263    /// asymmetric). The tokenizer matches an opening delimiter against this set.
5264    pub identifier_quotes: &'static [IdentifierQuote],
5265    /// Dialect default when `NULLS FIRST`/`NULLS LAST` is omitted.
5266    pub default_null_ordering: NullOrdering,
5267    /// Keywords rejected as an unquoted column/table name or `ColId` alias
5268    /// (PostgreSQL `type_func_name ∪ reserved`). Per-position reservation
5269    /// (prod-keyword-position-reserved-sets): a keyword's admissibility depends on
5270    /// the grammatical position, so the single reserved set is replaced by four.
5271    pub reserved_column_name: KeywordSet,
5272    /// Keywords rejected as a function name (PostgreSQL `reserved`).
5273    pub reserved_function_name: KeywordSet,
5274    /// Keywords rejected as a (user-defined) type name (PostgreSQL
5275    /// `col_name ∪ reserved`).
5276    pub reserved_type_name: KeywordSet,
5277    /// Keywords rejected as a bare column alias — one written without `AS`
5278    /// (PostgreSQL `AS_LABEL`). `AS`-introduced aliases (`ColLabel`) admit every
5279    /// keyword, so they need no reject set.
5280    pub reserved_bare_alias: KeywordSet,
5281    /// Keywords rejected in `ColLabel` position — an `AS`-introduced alias
5282    /// (`SELECT 1 AS <label>`) and a dotted-name continuation part (`schema.<part>`,
5283    /// `x.<part>`). PostgreSQL admits *every* keyword there (`SELECT a AS select` is
5284    /// valid), so its set is [`KeywordSet::EMPTY`]; the same holds for every
5285    /// PostgreSQL-family preset. SQLite draws no `ColId`/`ColLabel` split — a reserved
5286    /// word is rejected in every name position uniformly — so it reuses its single
5287    /// reserved set here, making `SELECT 1 AS delete` / `SELECT x.update` /
5288    /// `FROM schema.case` the parse errors SQLite reports (engine-measured via rusqlite).
5289    pub reserved_as_label: KeywordSet,
5290    /// Whether a relation (table / index / view) name admits a *catalog* qualifier —
5291    /// the third dotted part, `catalog.schema.table`. On for ANSI/PostgreSQL/MySQL/
5292    /// DuckDB/Lenient, which cap a relation at three parts (a fourth is rejected). Off
5293    /// for SQLite, whose relation names are `schema.table` at most (a database *is* the
5294    /// schema namespace), so a three-part `a.b.c` in table/index position is the syntax
5295    /// error SQLite reports (engine-measured via rusqlite). Column references reach one
5296    /// part deeper through composite-field selection — a different grammar position —
5297    /// and are never bounded by this flag.
5298    pub catalog_qualified_names: bool,
5299    /// Byte classes used by tokenizer dispatch.
5300    pub byte_classes: ByteClasses,
5301    /// Binary and prefix operator binding powers used by parser and renderer.
5302    pub binding_powers: BindingPowerTable,
5303    /// Set-operation binding powers used by parser and renderer.
5304    pub set_operation_powers: SetOperationBindingPowerTable,
5305    /// String literal syntax extensions accepted by the tokenizer/parser.
5306    pub string_literals: StringLiteralSyntax,
5307    /// Numeric literal syntax extensions accepted by the tokenizer.
5308    pub numeric_literals: NumericLiteralSyntax,
5309    /// Prepared-statement parameter placeholder forms accepted by the tokenizer.
5310    pub parameters: ParameterSyntax,
5311    /// MySQL session-variable forms (`@name` user variables, `@@[scope.]name` system
5312    /// variables) accepted by the tokenizer.
5313    pub session_variables: SessionVariableSyntax,
5314    /// Unquoted-identifier character policy (the dialect-variable `$`) accepted by the
5315    /// tokenizer.
5316    pub identifier_syntax: IdentifierSyntax,
5317    /// Table-expression syntax forms accepted by the parser.
5318    pub table_expressions: TableExpressionSyntax,
5319    /// Join-operator and recursive-query relation-composition syntax, gathered from
5320    /// [`table_expressions`](Self::table_expressions).
5321    pub join_syntax: JoinSyntax,
5322    /// Table-factor `FROM`-item forms beyond a plain named table, gathered from
5323    /// [`table_expressions`](Self::table_expressions).
5324    pub table_factor_syntax: TableFactorSyntax,
5325    /// Expression postfix and constructor forms (typecast, subscript, COLLATE, AT TIME
5326    /// ZONE, array/row constructors, field selection, typed literals) accepted by the
5327    /// parser. The operator spellings and call-tail forms are separate dimensions —
5328    /// see [`operator_syntax`](Self::operator_syntax) and [`call_syntax`](Self::call_syntax).
5329    pub expression_syntax: ExpressionSyntax,
5330    /// Infix/prefix operator forms (`OPERATOR(…)`, `@>`/`<@`, `->`/`->>`, `==`, general
5331    /// `IS`, `<=>`, the DuckDB lambda, the bitwise family, quantified comparisons)
5332    /// accepted by the parser.
5333    pub operator_syntax: OperatorSyntax,
5334    /// Function-call forms (named arguments, the `SEPARATOR`/`WITHIN GROUP` tails, the
5335    /// `UTC_*`/`COLUMNS(…)`/`EXTRACT(… FROM …)` special calls) accepted by the parser.
5336    pub call_syntax: CallSyntax,
5337    /// Keyword string/scalar special-form syntax — the flags whose sole AST product
5338    /// is a [`StringFunc`](crate::ast::StringFunc), gathered from
5339    /// [`call_syntax`](Self::call_syntax).
5340    pub string_func_forms: StringFuncForms,
5341    /// Aggregate/window function-call syntax — the in-parenthesis and post-`)` tails,
5342    /// the argument-shape/arity restrictions, and the `OVER`-eligibility gate,
5343    /// gathered from [`call_syntax`](Self::call_syntax).
5344    pub aggregate_call_syntax: AggregateCallSyntax,
5345    /// Pattern-match predicate forms (`LIKE`/`ILIKE`/`SIMILAR TO`) accepted by the
5346    /// parser. `LIKE` is SQL core (on everywhere); `ILIKE`/`SIMILAR TO` are gated.
5347    pub predicate_syntax: PredicateSyntax,
5348    /// What the `||` operator token means: string concatenation or logical OR.
5349    pub pipe_operator: PipeOperator,
5350    /// What the `&&` operator token means: logical AND, or not an operator.
5351    pub double_ampersand: DoubleAmpersand,
5352    /// Which dialect's keyword infix operators are recognized (MySQL's
5353    /// `DIV`/`MOD`/`XOR`/`RLIKE`/`REGEXP`, SQLite's `GLOB`/`MATCH`/`REGEXP`, or none) —
5354    /// each variant names one dialect's exact set; see [`KeywordOperators`].
5355    pub keyword_operators: KeywordOperators,
5356    /// What the `^` operator token means: arithmetic exponentiation, bitwise XOR, or no
5357    /// infix meaning. The `^` byte always lexes; this is the sole owner of its infix
5358    /// reading — the former split across an `exponent_operator` bool and a `Caret`-XOR
5359    /// spelling is folded here, so the "both power and XOR" state is unrepresentable. See
5360    /// [`CaretOperator`].
5361    pub caret_operator: CaretOperator,
5362    /// Whether a bare `#` lexes and parses as the bitwise-XOR operator
5363    /// ([`BinaryOperator::BitwiseXor`] under the [`Hash`](BitwiseXorSpelling::Hash)
5364    /// spelling, PostgreSQL). A different byte from [`caret_operator`](Self::caret_operator)'s
5365    /// `^`: PostgreSQL spells XOR `#`, MySQL spells it `^`, and neither accepts the other's
5366    /// spelling. Also gates the tokenizer — `#` reaches the operator scanner only under this
5367    /// flag — so it is one of the `#`-trigger claimants tracked in [`LexicalConflict`]
5368    /// (against a `#` line comment and DuckDB's `#n` positional column). Off for every
5369    /// shipped dialect but PostgreSQL.
5370    pub hash_bitwise_xor: bool,
5371    /// Dialect comment syntax extensions accepted by the tokenizer.
5372    pub comment_syntax: CommentSyntax,
5373    /// Mutation-statement (`INSERT`/`UPDATE`/`DELETE`) syntax forms accepted by the
5374    /// parser (`RETURNING`, `ON CONFLICT`).
5375    pub mutation_syntax: MutationSyntax,
5376    /// Whole-statement DDL dispatch gates (non-`TABLE` `CREATE`/`DROP` object dispatch),
5377    /// split from the retired `SchemaChangeSyntax`.
5378    pub statement_ddl_gates: StatementDdlGates,
5379    /// `CREATE TABLE` table-level clause syntax (storage/CTAS/partitioning/inheritance/
5380    /// persistence clauses), split from the retired `SchemaChangeSyntax`.
5381    pub create_table_clause_syntax: CreateTableClauseSyntax,
5382    /// `CREATE TABLE` column-definition syntax (generated/identity columns, SQLite column
5383    /// attributes, `DEFAULT`/`COLLATE`/`STORAGE` clauses), split from `SchemaChangeSyntax`.
5384    pub column_definition_syntax: ColumnDefinitionSyntax,
5385    /// Table/column-constraint syntax (deferrable/named/bare constraints, `EXCLUDE`, the
5386    /// `NO INHERIT`/`NOT VALID` markers, index parameters), split from `SchemaChangeSyntax`.
5387    pub constraint_syntax: ConstraintSyntax,
5388    /// `CREATE INDEX`/`ALTER TABLE`/`DROP` syntax (index clauses, the extended `ALTER`
5389    /// surface, drop behaviour, routine arg types), split from `SchemaChangeSyntax`.
5390    pub index_alter_syntax: IndexAlterSyntax,
5391    /// `IF [NOT] EXISTS` existence guards on DDL statements (`DROP`/`ALTER IF EXISTS`,
5392    /// `CREATE VIEW`/`CREATE DATABASE IF NOT EXISTS`), gathered from the schema-change
5393    /// surface into one per-site table.
5394    pub existence_guards: ExistenceGuards,
5395    /// SELECT-clause syntax forms accepted by the parser (`DISTINCT ON`, the
5396    /// offset / fetch-first row-limiting spelling).
5397    pub select_syntax: SelectSyntax,
5398    /// Query-tail syntax — the row-limiting/locking family and the trailing
5399    /// ClickHouse/pipe clauses parsed after the SELECT body, gathered from
5400    /// [`select_syntax`](Self::select_syntax).
5401    pub query_tail_syntax: QueryTailSyntax,
5402    /// `GROUP BY`/`ORDER BY` grouping-and-ordering syntax (grouping sets, clause
5403    /// modes, and quantifiers), gathered from [`select_syntax`](Self::select_syntax).
5404    pub grouping_syntax: GroupingSyntax,
5405    /// Utility-statement syntax forms the parser dispatches: the
5406    /// PostgreSQL `COPY`/`COMMENT ON` and SQLite `PRAGMA`/`ATTACH`/`DETACH`
5407    /// statement gates.
5408    pub utility_syntax: UtilitySyntax,
5409    /// SHOW/DESCRIBE introspection-statement syntax (the session `SHOW` reader, the typed
5410    /// `SHOW <object>` listings, and `DESCRIBE`/`SUMMARIZE`), gathered from
5411    /// [`utility_syntax`](Self::utility_syntax).
5412    pub show_syntax: ShowSyntax,
5413    /// Physical-maintenance-statement syntax (`VACUUM`/`REINDEX`/`ANALYZE`/`CHECKPOINT`),
5414    /// gathered from [`utility_syntax`](Self::utility_syntax).
5415    pub maintenance_syntax: MaintenanceSyntax,
5416    /// Access-control-statement syntax (`GRANT`/`REVOKE` and the extended object grammar),
5417    /// gathered from [`utility_syntax`](Self::utility_syntax).
5418    pub access_control_syntax: AccessControlSyntax,
5419    /// Type-name vocabulary the parser recognizes beyond the shared standard set
5420    /// (the MySQL `TINYINT`/`ENUM`/`UNSIGNED`/… surface).
5421    pub type_name_syntax: TypeNameSyntax,
5422    /// Which dialect's canonical surface spelling a [`RenderSpelling::TargetDialect`]
5423    /// render emits for constructs whose spelling diverges across dialects (today the
5424    /// type names). Read by the renderer in place of recognizing a preset by identity,
5425    /// so PostgreSQL-vs-ANSI output spelling is pure dialect data, not a cfg-gated
5426    /// `FeatureSet::POSTGRES` comparison.
5427    ///
5428    /// [`RenderSpelling::TargetDialect`]: crate::render::RenderSpelling::TargetDialect
5429    pub target_spelling: TargetSpelling,
5430}
5431
5432impl FeatureSet {
5433    /// The ANSI/standard config baseline as of SQL edition `version`.
5434    ///
5435    /// The M1 dialect-data surface (identifier quoting, casing, concatenation, set
5436    /// operations, …) is invariant across feature-ID-era editions — every standard
5437    /// feature we implement is SQL:1999 Core — so this returns [`FeatureSet::ANSI`]
5438    /// for every edition today. It is the stable anchor consumers pin against; the
5439    /// exhaustive `match` forces a decision here once edition-varying dialect data
5440    /// is added. For the set of *standard features* available as of an edition
5441    /// (which does vary), query [`standard_features_as_of`].
5442    ///
5443    /// Per-dialect *release* pinning (e.g. PostgreSQL 14 vs 16) is a distinct axis
5444    /// that needs multi-version dialect data; it arrives with the later milestones.
5445    pub const fn as_of(version: StandardVersion) -> Self {
5446        match version {
5447            StandardVersion::Sql1999
5448            | StandardVersion::Sql2003
5449            | StandardVersion::Sql2008
5450            | StandardVersion::Sql2011
5451            | StandardVersion::Sql2016 => Self::ANSI,
5452        }
5453    }
5454
5455    /// Return all lexical classes for `byte` under this dialect.
5456    pub const fn byte_class(&self, byte: u8) -> u8 {
5457        self.byte_classes.byte_class(byte)
5458    }
5459
5460    /// Return true if `byte` has any lexical class in `mask` under this dialect.
5461    pub const fn has_byte_class(&self, byte: u8, mask: u8) -> bool {
5462        self.byte_classes.has_class(byte, mask)
5463    }
5464
5465    /// Return the binary binding power for `op` under this dialect.
5466    pub const fn binding_power(&self, op: &BinaryOperator) -> BindingPower {
5467        self.binding_powers.binary(op)
5468    }
5469
5470    /// Return the prefix binding power for `op` under this dialect.
5471    pub const fn prefix_binding_power(&self, op: &UnaryOperator) -> u8 {
5472        self.binding_powers.prefix(op)
5473    }
5474
5475    /// Return the set-operation binding power for `op` under this dialect.
5476    pub const fn set_operation_binding_power(&self, op: &SetOperator) -> BindingPower {
5477        self.set_operation_powers.set_operation(op)
5478    }
5479
5480    /// Fold an unquoted identifier according to this dialect's identity rules.
5481    pub fn fold_unquoted_identifier<'a>(&self, identifier: &'a str) -> Cow<'a, str> {
5482        self.identifier_casing.fold_identifier(identifier)
5483    }
5484
5485    /// Whether omitted `NULLS FIRST`/`NULLS LAST` sorts nulls first.
5486    pub const fn default_nulls_first(&self) -> bool {
5487        self.default_null_ordering.nulls_first()
5488    }
5489}
5490
5491fn fold_identifier_upper(identifier: &str) -> Cow<'_, str> {
5492    if identifier.bytes().any(|byte| byte.is_ascii_lowercase()) {
5493        Cow::Owned(identifier.to_ascii_uppercase())
5494    } else {
5495        Cow::Borrowed(identifier)
5496    }
5497}
5498
5499fn fold_identifier_lower(identifier: &str) -> Cow<'_, str> {
5500    if identifier.bytes().any(|byte| byte.is_ascii_uppercase()) {
5501        Cow::Owned(identifier.to_ascii_lowercase())
5502    } else {
5503        Cow::Borrowed(identifier)
5504    }
5505}
5506
5507// `FeatureDelta`, `FeatureSet::with`, and the
5508// `Feature` / `FeatureMetadata` registry below are the per-field boilerplate that
5509// drifted each time a `FeatureSet` dimension was added (the delta mirror, the
5510// builder setters, and the enum/`id`/`ALL`/metadata registry). The thin include
5511// below pulls in that generated module: it is derived from the `FeatureSet` struct
5512// plus an annotation table (ADR-0013 drift gate; ADR-0011 self-describing dialect
5513// data) and re-exported here, so the public dialect API is unchanged. To add a
5514// dimension: add the struct field above and its `FEATURE_FIELDS` annotation in
5515// `crates/squonk-sourcegen/src/feature_set.rs`, then run
5516// `cargo run -p squonk-sourcegen`.
5517mod feature_set_generated;
5518pub use feature_set_generated::{FEATURE_METADATA, FEATURES, Feature, FeatureDelta};
5519
5520mod conflict;
5521mod head_contention;
5522mod standard_catalog;
5523mod support_tier;
5524
5525pub use conflict::{FeatureDependencyViolation, GrammarConflict, LexicalConflict};
5526pub use head_contention::{HeadResolution, MULTI_CLAIMANT_STATEMENT_HEADS, MultiClaimantHead};
5527pub use standard_catalog::{
5528    Conformance, FeatureMetadata, Maturity, STANDARD_FEATURE_CATALOG, StandardFeature,
5529    max_feature_metadata, standard_feature, standard_features_as_of, unsupported_standard_features,
5530};
5531pub use support_tier::{SupportEvidence, SupportTier};
5532
5533#[cfg(test)]
5534mod tests {
5535    use super::*;
5536    use lex_class::{
5537        CLASS_DIGIT, CLASS_IDENTIFIER_CONTINUE, CLASS_IDENTIFIER_START, CLASS_OPERATOR,
5538        CLASS_PUNCTUATION, CLASS_WHITESPACE, CLASS_WHITESPACE_BOUNDARY, CLASS_WHITESPACE_CONTINUE,
5539        byte_class, has_class,
5540    };
5541
5542    #[test]
5543    fn ansi_and_postgres_differ_in_identifier_casing() {
5544        assert_eq!(FeatureSet::ANSI.identifier_casing, Casing::Upper);
5545        assert_eq!(FeatureSet::POSTGRES.identifier_casing, Casing::Lower);
5546        assert_ne!(
5547            FeatureSet::ANSI.identifier_casing,
5548            FeatureSet::POSTGRES.identifier_casing
5549        );
5550    }
5551
5552    #[test]
5553    fn identifier_casing_folds_unquoted_identifier_identity() {
5554        assert_eq!(FeatureSet::ANSI.fold_unquoted_identifier("MiXeD"), "MIXED");
5555        assert_eq!(
5556            FeatureSet::POSTGRES.fold_unquoted_identifier("MiXeD"),
5557            "mixed",
5558        );
5559        assert_eq!(
5560            FeatureSet::ANSI
5561                .with(FeatureDelta::EMPTY.identifier_casing(Casing::Preserve))
5562                .fold_unquoted_identifier("MiXeD"),
5563            "MiXeD",
5564        );
5565        assert_eq!(
5566            FeatureSet::ANSI.fold_unquoted_identifier("CAFE"),
5567            "CAFE",
5568            "already-folded ASCII identifiers stay borrowed-equivalent",
5569        );
5570    }
5571
5572    #[test]
5573    fn default_null_ordering_exposes_sort_behavior() {
5574        assert!(!FeatureSet::ANSI.default_nulls_first());
5575        assert!(
5576            FeatureSet::ANSI
5577                .with(FeatureDelta::EMPTY.default_null_ordering(NullOrdering::NullsFirst))
5578                .default_nulls_first()
5579        );
5580    }
5581
5582    #[test]
5583    fn delta_customizes_from_base_preset() {
5584        let custom = FeatureSet::ANSI.with(
5585            FeatureDelta::EMPTY
5586                .identifier_casing(Casing::Preserve)
5587                .default_null_ordering(NullOrdering::NullsFirst),
5588        );
5589
5590        assert_eq!(custom.identifier_casing, Casing::Preserve);
5591        assert_eq!(custom.default_null_ordering, NullOrdering::NullsFirst);
5592        assert_eq!(custom.identifier_quotes, FeatureSet::ANSI.identifier_quotes);
5593        assert_eq!(
5594            custom.reserved_column_name,
5595            FeatureSet::ANSI.reserved_column_name
5596        );
5597        assert_eq!(
5598            custom.reserved_bare_alias,
5599            FeatureSet::ANSI.reserved_bare_alias
5600        );
5601        assert_eq!(custom.byte_classes, FeatureSet::ANSI.byte_classes);
5602        assert_eq!(custom.binding_powers, FeatureSet::ANSI.binding_powers);
5603        assert_eq!(
5604            custom.set_operation_powers,
5605            FeatureSet::ANSI.set_operation_powers
5606        );
5607        assert_eq!(custom.string_literals, FeatureSet::ANSI.string_literals);
5608        assert_eq!(custom.numeric_literals, FeatureSet::ANSI.numeric_literals);
5609        assert_eq!(custom.parameters, FeatureSet::ANSI.parameters);
5610        assert_eq!(custom.table_expressions, FeatureSet::ANSI.table_expressions);
5611        assert_eq!(custom.double_ampersand, FeatureSet::ANSI.double_ampersand);
5612        assert_eq!(custom.comment_syntax, FeatureSet::ANSI.comment_syntax);
5613        assert_eq!(custom.pipe_operator, FeatureSet::ANSI.pipe_operator);
5614    }
5615
5616    #[test]
5617    fn identifier_quote_styles_expose_open_and_close() {
5618        assert_eq!(
5619            FeatureSet::ANSI.identifier_quotes,
5620            STANDARD_IDENTIFIER_QUOTES
5621        );
5622        assert_eq!(
5623            FeatureSet::POSTGRES.identifier_quotes,
5624            STANDARD_IDENTIFIER_QUOTES
5625        );
5626
5627        let double = IdentifierQuote::Symmetric('"');
5628        assert_eq!((double.open(), double.close()), ('"', '"'));
5629
5630        let bracket = IdentifierQuote::Asymmetric {
5631            open: '[',
5632            close: ']',
5633        };
5634        assert_eq!((bracket.open(), bracket.close()), ('[', ']'));
5635    }
5636
5637    #[test]
5638    fn string_literal_syntax_is_explicit_dialect_data() {
5639        assert_eq!(FeatureSet::ANSI.string_literals, StringLiteralSyntax::ANSI);
5640        assert_eq!(
5641            FeatureSet::POSTGRES.string_literals,
5642            StringLiteralSyntax::POSTGRES,
5643        );
5644        assert_ne!(
5645            FeatureSet::ANSI.string_literals,
5646            FeatureSet::POSTGRES.string_literals,
5647        );
5648    }
5649
5650    #[test]
5651    fn numeric_literal_syntax_is_explicit_dialect_data() {
5652        // The radix/separator forms are off in both baselines (PostgreSQL gates
5653        // them by release), so the dimension is exercised via an explicit delta.
5654        assert_eq!(
5655            FeatureSet::ANSI.numeric_literals,
5656            NumericLiteralSyntax::ANSI
5657        );
5658        let hex =
5659            FeatureSet::ANSI.with(FeatureDelta::EMPTY.numeric_literals(NumericLiteralSyntax {
5660                hex_integers: true,
5661                ..NumericLiteralSyntax::ANSI
5662            }));
5663        assert!(hex.numeric_literals.hex_integers);
5664        assert_ne!(hex.numeric_literals, FeatureSet::ANSI.numeric_literals);
5665    }
5666
5667    #[test]
5668    fn parameter_syntax_is_explicit_dialect_data() {
5669        // ANSI recognises no placeholder forms; PostgreSQL enables positional `$n`
5670        // but not `?`. Bind to a local so the field checks are not const-folded into
5671        // a clippy `assertions_on_constants` lint.
5672        assert_eq!(FeatureSet::ANSI.parameters, ParameterSyntax::ANSI);
5673        assert_eq!(FeatureSet::POSTGRES.parameters, ParameterSyntax::POSTGRES);
5674        let postgres = FeatureSet::POSTGRES.parameters;
5675        assert!(postgres.positional_dollar);
5676        assert!(!postgres.anonymous_question);
5677        assert_ne!(FeatureSet::ANSI.parameters, FeatureSet::POSTGRES.parameters);
5678
5679        let anon = FeatureSet::ANSI.with(FeatureDelta::EMPTY.parameters(ParameterSyntax {
5680            anonymous_question: true,
5681            ..ParameterSyntax::ANSI
5682        }));
5683        assert!(anon.parameters.anonymous_question);
5684        assert_ne!(anon.parameters, FeatureSet::ANSI.parameters);
5685    }
5686
5687    #[test]
5688    fn identifier_syntax_is_explicit_dialect_data() {
5689        // The `$`-in-identifier policy is the dialect-variable part of the identifier
5690        // rule: ANSI forbids `$`, PostgreSQL accepts it mid-identifier. Bind to locals
5691        // so the field checks are not const-folded into a clippy
5692        // `assertions_on_constants` lint.
5693        assert_eq!(FeatureSet::ANSI.identifier_syntax, IdentifierSyntax::ANSI);
5694        assert_eq!(
5695            FeatureSet::POSTGRES.identifier_syntax,
5696            IdentifierSyntax::POSTGRES,
5697        );
5698        let ansi = FeatureSet::ANSI.identifier_syntax;
5699        let postgres = FeatureSet::POSTGRES.identifier_syntax;
5700        assert!(!ansi.dollar_in_identifiers);
5701        assert!(postgres.dollar_in_identifiers);
5702        assert_ne!(ansi, postgres);
5703
5704        // The SQLite string-literal identifier misfeature is SQLite/Lenient-only; every
5705        // other shipped preset syntax-rejects a string in a name position, so it stays off.
5706        // Bound to locals so the check is a runtime `assert!`, not the `assertions_on_constants`
5707        // lint's const-value form.
5708        let sqlite = FeatureSet::SQLITE.identifier_syntax;
5709        let lenient = FeatureSet::LENIENT.identifier_syntax;
5710        let mysql = FeatureSet::MYSQL.identifier_syntax;
5711        assert!(sqlite.string_literal_identifiers);
5712        assert!(lenient.string_literal_identifiers);
5713        assert!(!ansi.string_literal_identifiers);
5714        assert!(!postgres.string_literal_identifiers);
5715        assert!(!mysql.string_literal_identifiers);
5716
5717        // The policy toggles independently of the base preset.
5718        let dollars =
5719            FeatureSet::ANSI.with(FeatureDelta::EMPTY.identifier_syntax(IdentifierSyntax {
5720                dollar_in_identifiers: true,
5721                ..IdentifierSyntax::ANSI
5722            }));
5723        assert!(dollars.identifier_syntax.dollar_in_identifiers);
5724        assert_ne!(
5725            dollars.identifier_syntax,
5726            FeatureSet::ANSI.identifier_syntax
5727        );
5728    }
5729
5730    #[test]
5731    fn table_expression_syntax_is_explicit_dialect_data() {
5732        assert_eq!(
5733            FeatureSet::ANSI.table_expressions,
5734            TableExpressionSyntax::ANSI
5735        );
5736        assert_eq!(
5737            FeatureSet::POSTGRES.table_expressions,
5738            TableExpressionSyntax::POSTGRES
5739        );
5740        assert_ne!(
5741            FeatureSet::ANSI.table_expressions,
5742            FeatureSet::POSTGRES.table_expressions
5743        );
5744    }
5745
5746    #[test]
5747    fn pipe_operator_is_explicit_dialect_data() {
5748        assert_eq!(FeatureSet::ANSI.pipe_operator, PipeOperator::StringConcat);
5749        assert_eq!(
5750            FeatureSet::POSTGRES.pipe_operator,
5751            PipeOperator::StringConcat
5752        );
5753
5754        let mysql_like =
5755            FeatureSet::ANSI.with(FeatureDelta::EMPTY.pipe_operator(PipeOperator::LogicalOr));
5756        assert_eq!(mysql_like.pipe_operator, PipeOperator::LogicalOr);
5757
5758        // `||` maps to one canonical operator; `OR`'s looser binding power follows.
5759        assert_eq!(
5760            PipeOperator::StringConcat.binary_operator(),
5761            BinaryOperator::StringConcat,
5762        );
5763        assert_eq!(
5764            PipeOperator::LogicalOr.binary_operator(),
5765            BinaryOperator::Or
5766        );
5767    }
5768
5769    #[test]
5770    fn double_ampersand_is_explicit_dialect_data() {
5771        // ANSI/PostgreSQL do not treat `&&` as a scalar operator.
5772        assert_eq!(
5773            FeatureSet::ANSI.double_ampersand,
5774            DoubleAmpersand::Unsupported
5775        );
5776        assert_eq!(DoubleAmpersand::Unsupported.binary_operator(), None);
5777
5778        // A MySQL-like dialect maps `&&` to the canonical `AND` operator, so its
5779        // `AND` binding power follows automatically (no precedence override).
5780        let mysql_like = FeatureSet::ANSI
5781            .with(FeatureDelta::EMPTY.double_ampersand(DoubleAmpersand::LogicalAnd));
5782        assert_eq!(mysql_like.double_ampersand, DoubleAmpersand::LogicalAnd);
5783        assert_eq!(
5784            DoubleAmpersand::LogicalAnd.binary_operator(),
5785            Some(BinaryOperator::And),
5786        );
5787    }
5788
5789    #[test]
5790    fn comment_syntax_is_explicit_dialect_data() {
5791        assert_eq!(FeatureSet::ANSI.comment_syntax, CommentSyntax::ANSI);
5792
5793        let mysql_like = FeatureSet::ANSI.with(FeatureDelta::EMPTY.comment_syntax(CommentSyntax {
5794            line_comment_hash: true,
5795            ..CommentSyntax::ANSI
5796        }));
5797        assert!(mysql_like.comment_syntax.line_comment_hash);
5798        assert_ne!(mysql_like.comment_syntax, FeatureSet::ANSI.comment_syntax);
5799
5800        // The MySQL comment shape is data, not tokenizer hard-coding: block comments
5801        // stop nesting and `/*!…*/` becomes conditional inclusion, while the ANSI
5802        // baseline keeps the permissive nesting and no versioned form.
5803        let ansi = FeatureSet::ANSI.comment_syntax;
5804        let mysql = FeatureSet::MYSQL.comment_syntax;
5805        assert!(ansi.nested_block_comments);
5806        assert_eq!(ansi.versioned_comments, None);
5807        assert!(!mysql.nested_block_comments);
5808        assert_eq!(
5809            mysql.versioned_comments,
5810            Some(CommentSyntax::MYSQL_8_VERSION_BOUND)
5811        );
5812    }
5813
5814    #[test]
5815    fn select_syntax_is_explicit_dialect_data() {
5816        // `DISTINCT ON` is PostgreSQL-only; the fetch-first row-limiting spelling is
5817        // standard and on in both presets. Bind to locals so the field checks are not
5818        // const-folded into a clippy `assertions_on_constants` lint.
5819        assert_eq!(FeatureSet::ANSI.select_syntax, SelectSyntax::ANSI);
5820        assert_eq!(FeatureSet::POSTGRES.select_syntax, SelectSyntax::POSTGRES);
5821        let ansi = FeatureSet::ANSI.select_syntax;
5822        let postgres = FeatureSet::POSTGRES.select_syntax;
5823        let ansi_qt = FeatureSet::ANSI.query_tail_syntax;
5824        assert!(!ansi.distinct_on);
5825        assert!(postgres.distinct_on);
5826        assert!(ansi_qt.fetch_first);
5827        assert_ne!(ansi, postgres);
5828
5829        // The fetch-first gate can be turned off independently of the base preset.
5830        let no_fetch =
5831            FeatureSet::ANSI.with(FeatureDelta::EMPTY.query_tail_syntax(QueryTailSyntax {
5832                fetch_first: false,
5833                ..QueryTailSyntax::ANSI
5834            }));
5835        assert!(!no_fetch.query_tail_syntax.fetch_first);
5836        assert_ne!(
5837            no_fetch.query_tail_syntax,
5838            FeatureSet::ANSI.query_tail_syntax
5839        );
5840    }
5841
5842    #[test]
5843    fn utility_syntax_is_explicit_dialect_data() {
5844        // `COPY` and `COMMENT ON` are PostgreSQL-only: the ANSI baseline leaves both off,
5845        // PostgreSQL turns both on, and MySQL reuses the ANSI off value. Bind to locals so
5846        // the field checks are not const-folded into a clippy `assertions_on_constants`
5847        // lint.
5848        assert_eq!(FeatureSet::ANSI.utility_syntax, UtilitySyntax::ANSI);
5849        assert_eq!(FeatureSet::POSTGRES.utility_syntax, UtilitySyntax::POSTGRES);
5850        let ansi = FeatureSet::ANSI.utility_syntax;
5851        let postgres = FeatureSet::POSTGRES.utility_syntax;
5852        assert!(!ansi.copy);
5853        assert!(postgres.copy);
5854        assert!(!ansi.comment_on);
5855        assert!(postgres.comment_on);
5856        assert_ne!(ansi, postgres);
5857
5858        // The gates toggle independently of the base preset.
5859        let copy_on = FeatureSet::ANSI.with(FeatureDelta::EMPTY.utility_syntax(UtilitySyntax {
5860            copy: true,
5861            ..UtilitySyntax::ANSI
5862        }));
5863        assert!(copy_on.utility_syntax.copy);
5864        assert_ne!(copy_on.utility_syntax, FeatureSet::ANSI.utility_syntax);
5865
5866        // The SQLite statement gates ride the same struct: SQLITE dispatches
5867        // `PRAGMA`/`ATTACH` and the `VACUUM`/`REINDEX`/`ANALYZE` maintenance trio while
5868        // keeping the PostgreSQL statements off.
5869        let sqlite = FeatureSet::SQLITE.utility_syntax;
5870        let sqlite_maint = FeatureSet::SQLITE.maintenance_syntax;
5871        let ansi_maint = FeatureSet::ANSI.maintenance_syntax;
5872        let postgres_maint = FeatureSet::POSTGRES.maintenance_syntax;
5873        assert_eq!(sqlite, UtilitySyntax::SQLITE);
5874        assert!(sqlite.pragma);
5875        assert!(sqlite.attach);
5876        assert!(sqlite_maint.vacuum);
5877        assert!(sqlite_maint.reindex);
5878        assert!(sqlite_maint.analyze);
5879        assert!(!sqlite.copy);
5880        assert!(!ansi.pragma);
5881        assert!(!ansi.attach);
5882        assert!(!ansi_maint.vacuum);
5883        assert!(!ansi_maint.reindex);
5884        assert!(!ansi_maint.analyze);
5885        assert!(!postgres.pragma);
5886        assert!(!postgres.attach);
5887        // PostgreSQL has its own `VACUUM`/`ANALYZE`/`REINDEX`, but only SQLite's forms
5888        // are modelled, so the gates stay off under the PostgreSQL preset.
5889        assert!(!postgres_maint.vacuum);
5890        assert!(!postgres_maint.reindex);
5891        assert!(!postgres_maint.analyze);
5892
5893        // `create_trigger` is the whole-statement DDL gate: on for SQLite, off for the
5894        // standard/PostgreSQL baselines whose trigger body form is not modelled. Bound
5895        // to locals like the checks above, for the same const-folding reason.
5896        let sqlite_schema_change = FeatureSet::SQLITE.statement_ddl_gates;
5897        let ansi_schema_change = FeatureSet::ANSI.statement_ddl_gates;
5898        let postgres_schema_change = FeatureSet::POSTGRES.statement_ddl_gates;
5899        assert!(sqlite_schema_change.create_trigger);
5900        assert!(!ansi_schema_change.create_trigger);
5901        assert!(!postgres_schema_change.create_trigger);
5902
5903        // `create_macro` is the parallel whole-statement DDL gate for DuckDB's macro DDL:
5904        // on for DuckDB, off for every non-DuckDB baseline (PostgreSQL's `CREATE FUNCTION`
5905        // is the string-body routine, not a live-body macro). Bound to a local like the
5906        // checks above, both for const-folding and to keep clippy off the constant assert.
5907        let duckdb_schema_change = FeatureSet::DUCKDB.statement_ddl_gates;
5908        assert!(duckdb_schema_change.create_macro);
5909        assert!(!ansi_schema_change.create_macro);
5910        assert!(!postgres_schema_change.create_macro);
5911        assert!(!sqlite_schema_change.create_macro);
5912    }
5913
5914    #[test]
5915    fn mysql_preset_composes_existing_knobs_as_data() {
5916        // The third dialect is a re-composition of foundation knobs, not new fields:
5917        // every distinctive MySQL lexical choice is a value the preset selects.
5918        let mysql = FeatureSet::MYSQL;
5919
5920        // Backtick-only identifier quoting; `"` is a string, not an identifier.
5921        assert_eq!(mysql.identifier_quotes, MYSQL_IDENTIFIER_QUOTES);
5922        assert!(mysql.string_literals.double_quoted_strings);
5923        assert!(mysql.string_literals.backslash_escapes);
5924        // Operator-meaning divergences: `||` is OR and `&&` is AND in MySQL.
5925        assert_eq!(mysql.pipe_operator, PipeOperator::LogicalOr);
5926        assert_eq!(mysql.double_ampersand, DoubleAmpersand::LogicalAnd);
5927        // `#` line comments, `0x`/`0b` numbers, `?` placeholders, `$` in identifiers.
5928        assert!(mysql.comment_syntax.line_comment_hash);
5929        assert!(mysql.numeric_literals.hex_integers && mysql.numeric_literals.binary_integers);
5930        assert!(mysql.parameters.anonymous_question);
5931        assert!(mysql.identifier_syntax.dollar_in_identifiers);
5932        // The one new grammar gate: the `LIMIT a, b` comma form.
5933        assert!(mysql.query_tail_syntax.limit_offset_comma);
5934
5935        // It is genuinely distinct from both shipped presets.
5936        assert_ne!(mysql, FeatureSet::ANSI);
5937        assert_ne!(mysql, FeatureSet::POSTGRES);
5938
5939        // Adding the preset did not perturb the existing ones (no new field default
5940        // leaked a behaviour change): ANSI/PostgreSQL still reject the comma form.
5941        // Bind to locals so the const field reads are not flagged by clippy's
5942        // `assertions_on_constants`.
5943        let ansi_qt = FeatureSet::ANSI.query_tail_syntax;
5944        let postgres_qt = FeatureSet::POSTGRES.query_tail_syntax;
5945        assert!(!ansi_qt.limit_offset_comma);
5946        assert!(!postgres_qt.limit_offset_comma);
5947    }
5948
5949    #[test]
5950    fn limit_by_clause_off_for_oracle_compared_presets() {
5951        // ClickHouse `LIMIT n BY …` has no differential oracle, so the no-oracle
5952        // acceptance addition belongs to the ClickHouse preset and Lenient (the `apply_join`
5953        // precedent): on for Lenient, off for every oracle-compared preset. Bind to a
5954        // local so the const read is not flagged by clippy's `assertions_on_constants`.
5955        let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
5956        assert!(lenient_qt.limit_by_clause);
5957        for preset in [
5958            FeatureSet::ANSI,
5959            FeatureSet::POSTGRES,
5960            FeatureSet::MYSQL,
5961            FeatureSet::SQLITE,
5962            FeatureSet::DUCKDB,
5963        ] {
5964            assert!(!preset.query_tail_syntax.limit_by_clause);
5965        }
5966    }
5967
5968    #[test]
5969    fn format_clause_off_for_oracle_compared_presets() {
5970        // ClickHouse `FORMAT <name>` has no differential oracle, so the no-oracle
5971        // acceptance addition belongs to the ClickHouse preset and Lenient (the `settings_clause`
5972        // precedent): on for Lenient, off for every oracle-compared preset. Bind to a
5973        // local so the const read is not flagged by clippy's `assertions_on_constants`.
5974        let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
5975        assert!(lenient_qt.format_clause);
5976        for preset in [
5977            FeatureSet::ANSI,
5978            FeatureSet::POSTGRES,
5979            FeatureSet::MYSQL,
5980            FeatureSet::SQLITE,
5981            FeatureSet::DUCKDB,
5982        ] {
5983            assert!(!preset.query_tail_syntax.format_clause);
5984        }
5985    }
5986
5987    #[test]
5988    fn for_xml_json_clause_on_for_mssql_and_lenient_off_elsewhere() {
5989        // MSSQL `FOR XML`/`FOR JSON` has no differential oracle, so the no-oracle
5990        // acceptance addition belongs to the MSSQL preset and Lenient (the permissive
5991        // union): on for both, off for every oracle-compared preset. Bind to a local so
5992        // the const read is not flagged by clippy's `assertions_on_constants`.
5993        let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
5994        assert!(lenient_qt.for_xml_json_clause);
5995        #[cfg(feature = "mssql")]
5996        {
5997            let mssql_qt = FeatureSet::MSSQL.query_tail_syntax;
5998            assert!(mssql_qt.for_xml_json_clause);
5999            // MSSQL spells result-shaping with `FOR`, but has no query-tail locking
6000            // clause — the two `FOR`-led clauses never coexist under this preset.
6001            assert!(!mssql_qt.locking_clauses);
6002        }
6003        for preset in [
6004            FeatureSet::ANSI,
6005            FeatureSet::POSTGRES,
6006            FeatureSet::MYSQL,
6007            FeatureSet::SQLITE,
6008            FeatureSet::DUCKDB,
6009        ] {
6010            assert!(!preset.query_tail_syntax.for_xml_json_clause);
6011        }
6012    }
6013
6014    #[test]
6015    fn lateral_view_clause_on_for_hive_databricks_and_lenient_off_elsewhere() {
6016        // Hive/Spark `LATERAL VIEW` has no differential oracle, so the no-oracle
6017        // acceptance addition belongs to the Hive and Databricks presets and Lenient
6018        // (the permissive union): on for all three, off for every oracle-compared
6019        // preset. Bind to a local so the const read is not flagged by clippy's
6020        // `assertions_on_constants`.
6021        let lenient_select = FeatureSet::LENIENT.select_syntax;
6022        assert!(lenient_select.lateral_view_clause);
6023        // Lenient also enables the LATERAL derived-table factor — the one preset with
6024        // both `LATERAL`-led gates on, which the position/follow-token partition keeps
6025        // conflict-free (see the flag doc).
6026        let lenient_factors = FeatureSet::LENIENT.table_factor_syntax;
6027        assert!(lenient_factors.lateral);
6028        #[cfg(feature = "hive")]
6029        {
6030            let hive = FeatureSet::HIVE;
6031            assert!(hive.select_syntax.lateral_view_clause);
6032            // Hive has no LATERAL derived-table factor gate on, so under its preset
6033            // `LATERAL` leads only the view clause.
6034            assert!(!hive.table_factor_syntax.lateral);
6035        }
6036        #[cfg(feature = "databricks")]
6037        {
6038            let dbx = FeatureSet::DATABRICKS;
6039            assert!(dbx.select_syntax.lateral_view_clause);
6040            assert!(!dbx.table_factor_syntax.lateral);
6041        }
6042        for preset in [
6043            FeatureSet::ANSI,
6044            FeatureSet::POSTGRES,
6045            FeatureSet::MYSQL,
6046            FeatureSet::SQLITE,
6047            FeatureSet::DUCKDB,
6048        ] {
6049            assert!(!preset.select_syntax.lateral_view_clause);
6050        }
6051    }
6052
6053    #[test]
6054    fn connect_by_clause_on_for_snowflake_and_lenient_off_elsewhere() {
6055        // The Oracle-style `START WITH`/`CONNECT BY` hierarchical query clause has no
6056        // differential oracle, so the no-oracle acceptance addition belongs to the
6057        // Snowflake preset (whose public docs are the citable grammar) and Lenient (the
6058        // permissive union): on for both, off for every oracle-compared preset. Bind to a
6059        // local so the const read is not flagged by clippy's `assertions_on_constants`.
6060        let lenient_select = FeatureSet::LENIENT.select_syntax;
6061        assert!(lenient_select.connect_by_clause);
6062        #[cfg(feature = "snowflake")]
6063        {
6064            let snowflake = FeatureSet::SNOWFLAKE;
6065            assert!(snowflake.select_syntax.connect_by_clause);
6066        }
6067        // Databricks/Spark documents recursive CTEs instead of `CONNECT BY` and does not
6068        // support the Oracle `CONNECT BY … START WITH` syntax, so it stays off — unlike
6069        // the LATERAL VIEW clause, which Databricks *does* inherit from Spark.
6070        #[cfg(feature = "databricks")]
6071        {
6072            let dbx = FeatureSet::DATABRICKS;
6073            assert!(!dbx.select_syntax.connect_by_clause);
6074        }
6075        for preset in [
6076            FeatureSet::ANSI,
6077            FeatureSet::POSTGRES,
6078            FeatureSet::MYSQL,
6079            FeatureSet::SQLITE,
6080            FeatureSet::DUCKDB,
6081        ] {
6082            assert!(!preset.select_syntax.connect_by_clause);
6083        }
6084    }
6085
6086    #[test]
6087    fn nullable_type_off_for_oracle_compared_presets() {
6088        // ClickHouse `Nullable(T)` has no differential oracle, so the no-oracle
6089        // acceptance addition belongs to the ClickHouse preset and Lenient (the `composite_types`
6090        // precedent): on for Lenient, off for every oracle-compared preset. Bind to a local
6091        // so the const read is not flagged by clippy's `assertions_on_constants`.
6092        let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6093        assert!(lenient_types.nullable_type);
6094        for preset in [
6095            FeatureSet::ANSI,
6096            FeatureSet::POSTGRES,
6097            FeatureSet::MYSQL,
6098            FeatureSet::SQLITE,
6099            FeatureSet::DUCKDB,
6100        ] {
6101            assert!(!preset.type_name_syntax.nullable_type);
6102        }
6103    }
6104
6105    #[test]
6106    fn low_cardinality_type_off_for_oracle_compared_presets() {
6107        // ClickHouse `LowCardinality(T)` has no differential oracle, so the no-oracle
6108        // acceptance addition belongs to the ClickHouse preset and Lenient (the `nullable_type`
6109        // precedent): on for Lenient, off for every oracle-compared preset. Bind to a local
6110        // so the const read is not flagged by clippy's `assertions_on_constants`.
6111        let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6112        assert!(lenient_types.low_cardinality_type);
6113        for preset in [
6114            FeatureSet::ANSI,
6115            FeatureSet::POSTGRES,
6116            FeatureSet::MYSQL,
6117            FeatureSet::SQLITE,
6118            FeatureSet::DUCKDB,
6119        ] {
6120            assert!(!preset.type_name_syntax.low_cardinality_type);
6121        }
6122    }
6123
6124    #[test]
6125    fn fixed_string_type_off_for_oracle_compared_presets() {
6126        // ClickHouse `FixedString(N)` has no differential oracle, so the no-oracle
6127        // acceptance addition belongs to the ClickHouse preset and Lenient (the `nullable_type` /
6128        // `low_cardinality_type` precedent): on for Lenient, off for every oracle-compared
6129        // preset. Bind to a local so the const read is not flagged by clippy's
6130        // `assertions_on_constants`.
6131        let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6132        assert!(lenient_types.fixed_string_type);
6133        for preset in [
6134            FeatureSet::ANSI,
6135            FeatureSet::POSTGRES,
6136            FeatureSet::MYSQL,
6137            FeatureSet::SQLITE,
6138            FeatureSet::DUCKDB,
6139        ] {
6140            assert!(!preset.type_name_syntax.fixed_string_type);
6141        }
6142    }
6143
6144    #[test]
6145    fn datetime64_type_off_for_oracle_compared_presets() {
6146        // ClickHouse `DateTime64(P[, 'tz'])` has no differential oracle, so the
6147        // no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
6148        // `fixed_string_type` precedent): on for Lenient, off for every oracle-compared
6149        // preset. Bind to a local so the const read is not flagged by clippy's
6150        // `assertions_on_constants`.
6151        let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6152        assert!(lenient_types.datetime64_type);
6153        for preset in [
6154            FeatureSet::ANSI,
6155            FeatureSet::POSTGRES,
6156            FeatureSet::MYSQL,
6157            FeatureSet::SQLITE,
6158            FeatureSet::DUCKDB,
6159        ] {
6160            assert!(!preset.type_name_syntax.datetime64_type);
6161        }
6162    }
6163
6164    #[test]
6165    fn nested_type_off_for_oracle_compared_presets() {
6166        // ClickHouse `Nested(name Type, ...)` has no differential oracle, so the
6167        // no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
6168        // `datetime64_type` precedent): on for Lenient, off for every oracle-compared
6169        // preset. Bind to a local so the const read is not flagged by clippy's
6170        // `assertions_on_constants`.
6171        let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6172        assert!(lenient_types.nested_type);
6173        for preset in [
6174            FeatureSet::ANSI,
6175            FeatureSet::POSTGRES,
6176            FeatureSet::MYSQL,
6177            FeatureSet::SQLITE,
6178            FeatureSet::DUCKDB,
6179        ] {
6180            assert!(!preset.type_name_syntax.nested_type);
6181        }
6182    }
6183
6184    #[test]
6185    fn bit_width_integer_names_off_for_oracle_compared_presets() {
6186        // ClickHouse's `Int8`…`Int256`/`UInt*` fixed-bit-width integer names have no
6187        // differential oracle, so the no-oracle acceptance addition belongs to the ClickHouse
6188        // preset and Lenient (the `datetime64_type` precedent): on for Lenient, off for every
6189        // oracle-compared preset. Bind to a local so the const read is not flagged by clippy's
6190        // `assertions_on_constants`.
6191        let lenient_types = FeatureSet::LENIENT.type_name_syntax;
6192        assert!(lenient_types.bit_width_integer_names);
6193        for preset in [
6194            FeatureSet::ANSI,
6195            FeatureSet::POSTGRES,
6196            FeatureSet::MYSQL,
6197            FeatureSet::SQLITE,
6198            FeatureSet::DUCKDB,
6199        ] {
6200            assert!(!preset.type_name_syntax.bit_width_integer_names);
6201        }
6202    }
6203
6204    #[test]
6205    fn settings_clause_off_for_oracle_compared_presets() {
6206        // ClickHouse `SETTINGS name = value, …` has no differential oracle, so the
6207        // no-oracle acceptance addition belongs to the ClickHouse preset and Lenient (the
6208        // `limit_by_clause` precedent): on for Lenient, off for every oracle-compared
6209        // preset. Bind to a local so the const read is not flagged by clippy's
6210        // `assertions_on_constants`.
6211        let lenient_qt = FeatureSet::LENIENT.query_tail_syntax;
6212        assert!(lenient_qt.settings_clause);
6213        for preset in [
6214            FeatureSet::ANSI,
6215            FeatureSet::POSTGRES,
6216            FeatureSet::MYSQL,
6217            FeatureSet::SQLITE,
6218            FeatureSet::DUCKDB,
6219        ] {
6220            assert!(!preset.query_tail_syntax.settings_clause);
6221        }
6222    }
6223
6224    #[test]
6225    fn feature_set_as_of_is_a_deterministic_total_anchor() {
6226        // Invariant across feature-ID-era editions at M1 (every implemented standard
6227        // feature is SQL:1999 Core), but a stable, total pin point.
6228        for version in [
6229            StandardVersion::Sql1999,
6230            StandardVersion::Sql2003,
6231            StandardVersion::Sql2008,
6232            StandardVersion::Sql2011,
6233            StandardVersion::Sql2016,
6234        ] {
6235            assert_eq!(FeatureSet::as_of(version), FeatureSet::ANSI);
6236        }
6237    }
6238
6239    #[test]
6240    fn byte_classes_cover_m1_lexer_needs() {
6241        assert!(has_class(b' ', CLASS_WHITESPACE));
6242        assert!(has_class(b'\n', CLASS_WHITESPACE));
6243
6244        assert!(has_class(b'a', CLASS_IDENTIFIER_START));
6245        assert!(has_class(b'Z', CLASS_IDENTIFIER_START));
6246        assert!(has_class(b'_', CLASS_IDENTIFIER_START));
6247        assert!(has_class(b'a', CLASS_IDENTIFIER_CONTINUE));
6248
6249        assert!(has_class(b'7', CLASS_DIGIT));
6250        assert!(has_class(b'7', CLASS_IDENTIFIER_CONTINUE));
6251        assert!(!has_class(b'7', CLASS_IDENTIFIER_START));
6252
6253        assert!(has_class(b'+', CLASS_OPERATOR));
6254        assert!(has_class(b'|', CLASS_OPERATOR));
6255        assert!(has_class(b'(', CLASS_PUNCTUATION));
6256        assert!(has_class(b'.', CLASS_PUNCTUATION));
6257
6258        assert!(has_class(0x80, CLASS_IDENTIFIER_CONTINUE));
6259        assert_eq!(byte_class(0), 0);
6260    }
6261
6262    #[test]
6263    fn vertical_tab_is_dialect_specific_whitespace() {
6264        // The vertical tab (`0x0b`) is the one member of the flex `space` set `[ \t\n\r\f\v]`
6265        // that Rust's `is_ascii_whitespace` — and hence `STANDARD_BYTE_CLASSES` — omits, and
6266        // every engine treats it differently. Engine-measured:
6267        //
6268        // - PostgreSQL / MySQL fold it as *ordinary* whitespace everywhere (a lone `0x0b`
6269        //   parses as an empty statement, `SELECT\x0b1` as `SELECT 1`) — full `CLASS_WHITESPACE`.
6270        assert!(FeatureSet::POSTGRES.has_byte_class(0x0b, CLASS_WHITESPACE));
6271        assert!(FeatureSet::MYSQL.has_byte_class(0x0b, CLASS_WHITESPACE));
6272        // - SQLite folds it only as a whitespace-run *continuation*: it rides an open run
6273        //   (`"\x20\x0b"` accepts) but cannot start one (lone `"\x0b"` rejects) — so it carries
6274        //   `CLASS_WHITESPACE_CONTINUE`, never `CLASS_WHITESPACE`.
6275        assert!(FeatureSet::SQLITE.has_byte_class(0x0b, CLASS_WHITESPACE_CONTINUE));
6276        assert!(!FeatureSet::SQLITE.has_byte_class(0x0b, CLASS_WHITESPACE));
6277        // - DuckDB folds it as statement-boundary *trim*: whitespace at a `;`-segment's edges
6278        //   (lone `"\x0b"`, `"SELECT 1\x0b"` accept) but a hard error interior to a statement
6279        //   (`"SELECT\x0b1"` rejects) — so it carries both `CLASS_WHITESPACE` and the
6280        //   `CLASS_WHITESPACE_BOUNDARY` marker the tokenizer's interior guard reads.
6281        assert!(FeatureSet::DUCKDB.has_byte_class(0x0b, CLASS_WHITESPACE));
6282        assert!(FeatureSet::DUCKDB.has_byte_class(0x0b, CLASS_WHITESPACE_BOUNDARY));
6283        assert!(FeatureSet::DUCKDB.byte_classes.has_boundary_whitespace());
6284        // - ANSI (and every other preset) keeps it strict: not whitespace in any form.
6285        for preset in [FeatureSet::ANSI, FeatureSet::POSTGRES, FeatureSet::MYSQL] {
6286            assert!(!preset.has_byte_class(0x0b, CLASS_WHITESPACE_BOUNDARY));
6287        }
6288        assert!(!FeatureSet::ANSI.has_byte_class(
6289            0x0b,
6290            CLASS_WHITESPACE | CLASS_WHITESPACE_CONTINUE | CLASS_WHITESPACE_BOUNDARY,
6291        ));
6292        // Only DuckDB flags the boundary class, so only its tokenizer pays the guard.
6293        for preset in [
6294            FeatureSet::ANSI,
6295            FeatureSet::POSTGRES,
6296            FeatureSet::MYSQL,
6297            FeatureSet::SQLITE,
6298        ] {
6299            assert!(!preset.byte_classes.has_boundary_whitespace());
6300        }
6301        // The form feed (`0x0c`) is shared whitespace: every probed engine folds it, and it
6302        // already rides `STANDARD_BYTE_CLASSES` via `is_ascii_whitespace`.
6303        for preset in [
6304            FeatureSet::ANSI,
6305            FeatureSet::POSTGRES,
6306            FeatureSet::MYSQL,
6307            FeatureSet::SQLITE,
6308            FeatureSet::DUCKDB,
6309        ] {
6310            assert!(preset.has_byte_class(0x0c, CLASS_WHITESPACE));
6311        }
6312    }
6313
6314    #[test]
6315    fn dialect_data_owns_byte_classes_and_binding_powers() {
6316        let byte_classes = FeatureSet::ANSI
6317            .byte_classes
6318            .with_class(b'@', CLASS_IDENTIFIER_START | CLASS_IDENTIFIER_CONTINUE);
6319        let binding_powers = FeatureSet::ANSI.binding_powers.with_binary(
6320            &BinaryOperator::StringConcat,
6321            BindingPower {
6322                left: 70,
6323                right: 71,
6324                assoc: crate::precedence::Assoc::Left,
6325            },
6326        );
6327        let custom = FeatureSet::ANSI.with(
6328            FeatureDelta::EMPTY
6329                .byte_classes(byte_classes)
6330                .binding_powers(binding_powers),
6331        );
6332
6333        assert!(custom.has_byte_class(b'@', CLASS_IDENTIFIER_START));
6334        assert_eq!(custom.binding_power(&BinaryOperator::StringConcat).left, 70);
6335        assert_eq!(
6336            custom.binding_power(&BinaryOperator::Plus),
6337            FeatureSet::ANSI.binding_power(&BinaryOperator::Plus),
6338        );
6339    }
6340
6341    #[test]
6342    fn dialect_data_owns_set_operation_binding_powers() {
6343        let custom_set_powers = FeatureSet::ANSI.set_operation_powers.with_set_operator(
6344            &SetOperator::Union,
6345            BindingPower {
6346                left: 30,
6347                right: 31,
6348                assoc: crate::precedence::Assoc::Left,
6349            },
6350        );
6351        let custom =
6352            FeatureSet::ANSI.with(FeatureDelta::EMPTY.set_operation_powers(custom_set_powers));
6353
6354        assert_eq!(
6355            custom.set_operation_binding_power(&SetOperator::Union).left,
6356            30,
6357        );
6358        assert_eq!(
6359            custom
6360                .set_operation_binding_power(&SetOperator::Except)
6361                .left,
6362            30,
6363        );
6364        assert_eq!(
6365            custom.set_operation_binding_power(&SetOperator::Intersect),
6366            FeatureSet::ANSI.set_operation_binding_power(&SetOperator::Intersect),
6367        );
6368    }
6369}