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}