Skip to main content

squonk_ast/dialect/
duckdb.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The DuckDB dialect preset (PostgreSQL-derived).
5//!
6//! DuckDB is PostgreSQL-dialect-compatible by design, so this preset is
7//! [`FeatureSet::POSTGRES`] with DuckDB deltas layered on top. This is the deliberate
8//! exception to the "never reference a gated sibling" rule: the `duckdb` cargo feature
9//! implies `postgres`, so sharing the PostgreSQL sub-presets records the real derivation
10//! without duplicating values.
11//!
12//! Comments below are kept where a value is not an obvious inheritance/addition, such as
13//! a parse-time tightening, keyword reservation needed to disambiguate grammar, or the
14//! `->` precedence difference measured against the DuckDB oracle.
15
16use super::{
17    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
18    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
19    DUCKDB_BYTE_CLASSES, DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet,
20    GroupingSyntax, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, Keyword, KeywordOperators,
21    KeywordSet, MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax,
22    OperatorSyntax, ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax,
23    RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME,
24    STANDARD_IDENTIFIER_QUOTES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
25    StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
26    TypeNameSyntax, UtilitySyntax,
27};
28use crate::ast::{BinaryOperator, EqualsSpelling};
29use crate::precedence::{
30    Assoc, BindingPower, BindingPowerTable, IS_PREDICATE_BELOW_COMPARISON, STANDARD_BINDING_POWERS,
31    STANDARD_SET_OPERATION_BINDING_POWERS,
32};
33
34/// `QUALIFY` (`duckdb_keywords()` class `reserved`, DuckDB 1.5.4), the fully-reserved
35/// half of DuckDB's reservation delta over the shared PostgreSQL-derived model.
36/// Unioned into all four per-position reject sets below because DuckDB's `reserved`
37/// class rejects the word as a column/table name, function name, type name, *and*
38/// bare alias (probed: `SELECT qualify FROM t`, `SELECT * FROM qualify`,
39/// `SELECT qualify(1)`, `CAST(1 AS qualify)`, `SELECT 1 qualify`, and
40/// `FROM t qualify` all syntax-error; `SELECT 1 AS qualify` labels). The same
41/// hand-composition pattern the SQLite/MySQL presets use for their reservation deltas.
42pub const DUCKDB_QUALIFY_RESERVATION: KeywordSet = KeywordSet::from_keywords(&[Keyword::Qualify]);
43
44/// `PIVOT` and `UNPIVOT` (`duckdb_keywords()` class `reserved`, DuckDB 1.5.4), the
45/// row/column rotation operators. Like `QUALIFY`'s `reserved` class (and unlike the
46/// `type_function` join words), both are rejected in all four identifier positions —
47/// column/table name, function name, type name, and bare alias — while `AS pivot` still
48/// labels (probed: `SELECT pivot FROM t`, `SELECT * FROM pivot`, `SELECT pivot(1)`,
49/// `CAST(1 AS pivot)`, `SELECT 1 pivot` all syntax-error; `SELECT 1 AS pivot` parses —
50/// identically for `unpivot`). The bare-alias reservation is load-bearing for the
51/// grammar: it is what lets `FROM t PIVOT (…)` read the operator instead of a table
52/// alias named `pivot`. Unioned into all four reject sets below, exactly like
53/// [`DUCKDB_QUALIFY_RESERVATION`].
54pub const DUCKDB_PIVOT_RESERVATION: KeywordSet =
55    KeywordSet::from_keywords(&[Keyword::Pivot, Keyword::Unpivot]);
56
57/// The nonstandard-join keywords, `ASOF` and `POSITIONAL` (`duckdb_keywords()` class
58/// `type_function`, like `CROSS`, DuckDB 1.5.4). Unlike `QUALIFY`'s `reserved` class,
59/// this profile rejects the words only as a column/table name (`ColId`) and as a bare
60/// alias, while function/type positions and `AS` labels still admit them (probed:
61/// `SELECT asof FROM t`, `CREATE TABLE asof(…)`, `FROM t asof`, and `SELECT 1 asof`
62/// all syntax-error; `SELECT asof(1)`, `CAST(1 AS asof)`, and `SELECT 1 AS asof`
63/// parse — identically for `positional`). The `ColId` reservation is load-bearing for
64/// the grammar: it is what lets `FROM l ASOF JOIN r …` read the join instead of a
65/// table alias named `asof`.
66pub const DUCKDB_NONSTANDARD_JOIN_RESERVATION: KeywordSet =
67    KeywordSet::from_keywords(&[Keyword::Asof, Keyword::Positional]);
68
69/// The semi-/anti-join keywords, `SEMI` and `ANTI` (`duckdb_keywords()` class
70/// `type_function`, DuckDB 1.5.4). Their `type_function` category matches
71/// `ASOF`/`POSITIONAL`, but the DuckDB grammar reserves them one position *further*:
72/// they reject as a column/table name (`ColId`), a bare alias, *and a function name*,
73/// while only the type position and `AS` labels admit them (probed: `SELECT semi FROM
74/// t`, `CREATE TABLE semi(…)`, `FROM t semi`, `SELECT 1 semi`, and — unlike `asof(1)` —
75/// `SELECT semi(1)` all syntax-error; `CAST(1 AS semi)` and `SELECT 1 AS semi` parse,
76/// identically for `anti`). So this set unions into the `ColId`, function-name, and
77/// bare-alias rejects but *not* the type-name one. The `ColId`/bare-alias reservation
78/// is load-bearing for the grammar: it is what lets `FROM l SEMI JOIN r …` read the
79/// join instead of a table alias named `semi`.
80pub const DUCKDB_SEMI_ANTI_JOIN_RESERVATION: KeywordSet =
81    KeywordSet::from_keywords(&[Keyword::Semi, Keyword::Anti]);
82
83/// DuckDB `ColId` reject set: the shared model plus `QUALIFY`, the `PIVOT`/`UNPIVOT`
84/// operators, and the nonstandard-join / semi-anti-join keywords.
85pub const DUCKDB_RESERVED_COLUMN_NAME: KeywordSet = RESERVED_COLUMN_NAME
86    .union(DUCKDB_QUALIFY_RESERVATION)
87    .union(DUCKDB_PIVOT_RESERVATION)
88    .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
89    .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION);
90
91/// DuckDB function-name reject set: the shared model plus `QUALIFY` (DuckDB reads
92/// `SELECT qualify(1)` as an empty projection followed by the QUALIFY clause, never a
93/// call), `PIVOT`/`UNPIVOT` (their `reserved` class likewise rejects `pivot(1)`), and
94/// `SEMI`/`ANTI` (DuckDB's grammar rejects `semi(1)` despite the `type_function` class).
95/// The `ASOF`/`POSITIONAL` words are *not* here: their `type_function` class admits
96/// `asof(1)` / `positional(1)` as calls, matching the engine.
97pub const DUCKDB_RESERVED_FUNCTION_NAME: KeywordSet = RESERVED_FUNCTION_NAME
98    .union(DUCKDB_QUALIFY_RESERVATION)
99    .union(DUCKDB_PIVOT_RESERVATION)
100    .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION);
101
102/// DuckDB type-name reject set: the shared model plus `QUALIFY` and `PIVOT`/`UNPIVOT`
103/// (their `reserved` class rejects `CAST(1 AS pivot)`). The nonstandard-join and
104/// semi-anti-join words are *not* here: `CAST(1 AS asof)` and `CAST(1 AS semi)` parse in
105/// the engine (the type position admits any non-`reserved` word).
106pub const DUCKDB_RESERVED_TYPE_NAME: KeywordSet = RESERVED_TYPE_NAME
107    .union(DUCKDB_QUALIFY_RESERVATION)
108    .union(DUCKDB_PIVOT_RESERVATION);
109
110/// DuckDB bare-label reject set: the shared model plus `QUALIFY`, so a projection or
111/// FROM-relation bare alias cannot swallow the clause keyword (`SELECT a FROM t
112/// QUALIFY …` reads the clause); `PIVOT`/`UNPIVOT`, so a source's alias cannot swallow a
113/// trailing operator (`FROM t PIVOT (…)` reads the operator); and the nonstandard-join /
114/// semi-anti-join keywords, whose `type_function` class the engine likewise rejects as a
115/// bare projection label (`SELECT 1 asof` / `SELECT 1 semi` syntax-error while
116/// `SELECT 1 AS asof` / `SELECT 1 AS semi` parse).
117pub const DUCKDB_RESERVED_BARE_ALIAS: KeywordSet = RESERVED_BARE_ALIAS
118    .union(DUCKDB_QUALIFY_RESERVATION)
119    .union(DUCKDB_PIVOT_RESERVATION)
120    .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
121    .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION);
122
123impl NumericLiteralSyntax {
124    /// The `DUCKDB` preset for numeric literal syntax.
125    pub const DUCKDB: Self = Self {
126        hex_integers: true,
127        octal_integers: true,
128        binary_integers: true,
129        underscore_separators: true,
130        // DuckDB lexes numerics loosely (`123abc` re-reads as `123` aliased), so the
131        // leading-underscore radix opener stays off and `0x_1F` keeps its `0` + word split.
132        radix_leading_underscore: false,
133        money_literals: false,
134        reject_trailing_junk: false,
135    };
136}
137
138impl PredicateSyntax {
139    /// The `DUCKDB` preset for predicate syntax.
140    pub const DUCKDB: Self = Self {
141        unparenthesized_in_list: true,
142        // DuckDB rejects the SQL-standard `OVERLAPS` period predicate (engine-probed
143        // 1.5.4) — override the `..Self::POSTGRES` spread's `true`.
144        overlaps_period_predicate: false,
145        // The PostgreSQL `LIKE/ILIKE ANY|ALL (array)` pattern-match quantifier is not a
146        // DuckDB construct — override the `..Self::POSTGRES` spread's `true`.
147        pattern_match_quantifier: false,
148        between_symmetric: false,
149        is_normalized: false,
150        // DuckDB accepts the two-word `<expr> NOT NULL` postfix (engine-measured) — override
151        // the `..Self::POSTGRES` spread's `false`.
152        null_test_two_word_postfix: true,
153        ..Self::POSTGRES
154    };
155}
156
157/// The DuckDB binding-power table: the standard table with the `->` token re-ranked
158/// below every expression operator.
159///
160/// DuckDB lexes `->` as its own `LAMBDA_ARROW` grammar token (its `->>` stays an
161/// ordinary `Op`), ranked looser than everything — measured on 1.5.4 via
162/// `json_serialize_sql`: `x -> x % 2 = 0`, `x -> x OR y`, and
163/// `elem -> extract(…) BETWEEN 2000 AND 2022` each put the whole right side in the
164/// arrow's right operand, `NOT x -> y` and `a = x -> y` each take the full left
165/// expression as the arrow's left operand, and `x -> y -> z` groups left. `4`/`5`
166/// sits below `or` (10) with left-associativity, reproducing exactly that. The rank
167/// belongs to the *token*, not to the lambda reading: a non-parameter left operand
168/// still folds as the JSON accessor, at this same DuckDB rank (one table
169/// drives parser and renderer, per dialect).
170pub const DUCKDB_BINDING_POWERS: BindingPowerTable = BindingPowerTable {
171    // The `IS`-family predicates (`IS NULL`, `IS DISTINCT FROM`, `IS TRUE`, …) rank one tier
172    // below comparison, so `a <> b IS NULL` groups `(a <> b) IS NULL` and `a IS DISTINCT FROM
173    // b = c` groups `a IS DISTINCT FROM (b = c)` (measured on 1.5.4 via `json_serialize_sql`).
174    is_predicate_override: Some(IS_PREDICATE_BELOW_COMPARISON),
175    ..STANDARD_BINDING_POWERS
176        .with_binary(
177            &BinaryOperator::JsonGet,
178            BindingPower {
179                left: 4,
180                right: 5,
181                assoc: Assoc::Left,
182            },
183        )
184        // DuckDB lexes `==` as a generic `%left Op`, not the `%nonassoc '='` comparison: it
185        // binds tighter than the comparisons and looser than additive, left-associative, so
186        // `a = b == c` is `a = (b == c)` and `a == b == c` is `((a = b) = c)` (measured on
187        // 1.5.4). This is the `any_operator` rank (45/46), the same `%left Op OPERATOR` slot
188        // `||`/`@>` occupy; splitting it off keeps the `=`/`<`/`>` comparisons non-associative.
189        .with_binary(
190            &BinaryOperator::Eq(EqualsSpelling::Double),
191            BindingPower {
192                left: 45,
193                right: 46,
194                assoc: Assoc::Left,
195            },
196        )
197};
198
199impl SelectSyntax {
200    /// The `DUCKDB` preset for select syntax.
201    pub const DUCKDB: Self = Self {
202        // DuckDB rejects the empty target list where PostgreSQL's raw grammar accepts it.
203        empty_target_list: false,
204        qualify: true,
205        // DuckDB's `UNION [ALL] BY NAME` name-matched set operation (probed on 1.5.4):
206        // an additive grammar delta over the PostgreSQL base, UNION-only (the engine
207        // rejects `INTERSECT`/`EXCEPT BY NAME`). `duckdb-union-by-name`.
208        union_by_name: true,
209        // DuckDB's FROM-first SELECT (`FROM t SELECT x`, bare `FROM t`) — an additive
210        // grammar delta above the PostgreSQL base, which rejects a statement-position
211        // `FROM`. `FROM` is reserved under the shared model, so the leading-`FROM` primary
212        // can never shadow an identifier read (`duckdb-from-first-select`).
213        from_first: true,
214        // DuckDB's `*`/`t.*` wildcard modifiers `EXCLUDE`/`REPLACE`/`RENAME` (probed on
215        // 1.5.4) — an additive grammar delta over the PostgreSQL base, which has no
216        // wildcard tail. `duckdb-select-star-modifiers`.
217        wildcard_modifiers: true,
218        // DuckDB aliases a qualified wildcard (`t.* AS x`, engine-probed 1.5.4) — the plain
219        // alias axis PostgreSQL shares, distinct from the DuckDB-only modifier tail above.
220        qualified_wildcard_alias: true,
221        // DuckDB rejects a ragged VALUES constructor (rows of differing width) at *parse*
222        // — `Parser Error: VALUES lists must all be the same length`, in every VALUES
223        // position (standalone, derived, INSERT; measured on 1.5.4) — where PostgreSQL's
224        // raw grammar accepts it and defers the check to bind. A shape-level tightening
225        // *below* the PostgreSQL base (like `empty_target_list`), enforced by the parser
226        // comparing the parsed rows' arities. `duckdb-from-clause-parse-overaccept`.
227        values_rows_require_equal_arity: true,
228        // DuckDB admits a single-quoted string literal as a projection alias
229        // (`(a = b) AS '(a = b)'`; probed on 1.5.4) — the MySQL-precedent
230        // `alias_string_literals` gate, reusing its projection-alias round-trip machinery.
231        // `duckdb-operator-and-literal-gaps`.
232        alias_string_literals: true,
233        // DuckDB accepts ONLY the `AS 'x'` form — the *bare* `SELECT 1 'x'` rejects (probed on
234        // 1.5.4), unlike SQLite/MySQL — so the bare axis stays off here.
235        bare_alias_string_literals: false,
236        // DuckDB tolerates a single trailing comma in its list positions — the SELECT /
237        // VALUES / collection-literal / `IN` lists (engine-probed 1.5.4), but not function
238        // arguments, `ORDER BY`, or a bare row constructor. `duckdb-trailing-comma`.
239        trailing_comma: true,
240        // DuckDB's prefix colon alias (`SELECT j : 42`, `FROM b : a`; probed on 1.5.4) — an
241        // additive grammar delta over the PostgreSQL base, pure sugar for a trailing `AS`
242        // alias that folds onto the existing alias field. Conflict-free here: DuckDB has no
243        // top-level semi-structured `a:b` access, the one construct that would claim the
244        // same `<ident> :` head. `duckdb-colon-alias`.
245        prefix_colon_alias: true,
246        ..SelectSyntax::POSTGRES
247    };
248}
249
250impl QueryTailSyntax {
251    /// The `DUCKDB` preset for query tail syntax.
252    pub const DUCKDB: Self = Self {
253        // DuckDB has no `FOR UPDATE`/`FOR SHARE` row locking, so this diverges *below*
254        // the PostgreSQL base it otherwise spreads (like `empty_target_list`). The
255        // strength/stacking refinements ride the same base, so override them off too
256        // rather than inherit PostgreSQL's `true` for a dialect with no locking clause.
257        locking_clauses: false,
258        key_lock_strengths: false,
259        stacked_locking_clauses: false,
260        // DuckDB's `USING SAMPLE <entry>` query-level sample clause (probed on 1.5.4) —
261        // an additive grammar delta over the PostgreSQL base, which has no such clause.
262        // `duckdb-expression-and-clause-tails`.
263        using_sample: true,
264        // DuckDB's percentage `LIMIT` (`LIMIT 40 PERCENT`, `LIMIT 35%`; probed on 1.5.4) —
265        // an additive grammar delta over the PostgreSQL base, which has no percentage form.
266        // The marker folds only onto a numeric-literal count at a clause boundary, so
267        // ordinary modulo (`LIMIT 10 % 3`) and non-literal counts stay unaffected.
268        // `duckdb-limit-percent`.
269        limit_percent: true,
270        // PostgreSQL's raw-parse `WITH TIES` guards are not modelled for DuckDB (conservative
271        // — DuckDB's own `WITH TIES` validity is unprobed here); keep the PG-only behaviour.
272        with_ties_requires_order_by: false,
273        ..QueryTailSyntax::POSTGRES
274    };
275}
276
277impl GroupingSyntax {
278    /// The `DUCKDB` preset for grouping syntax.
279    pub const DUCKDB: Self = Self {
280        // DuckDB's `GROUP BY ALL` / `ORDER BY ALL` clause modes (probed on 1.5.4) —
281        // purely additive grammar deltas: `ALL` is reserved under the shared
282        // PostgreSQL-derived model, so neither branch can shadow an identifier read.
283        group_by_all: true,
284        group_by_set_quantifier: false,
285        order_by_all: true,
286        ..GroupingSyntax::POSTGRES
287    };
288}
289
290impl ExpressionSyntax {
291    /// The `DUCKDB` preset for expression syntax.
292    pub const DUCKDB: Self = Self {
293        collection_literals: true,
294        // The three-bound `[lower:upper:step]` slice with its `-` open-upper placeholder —
295        // a DuckDB extension over the two-bound slice the POSTGRES spread carries.
296        slice_step: true,
297        // The `#n` positional column reference — a DuckDB-only extension; `#` is a stray
298        // byte in every other preset, so this overrides the POSTGRES `false` spread.
299        positional_column: true,
300        lambda_keyword: true,
301        // DuckDB parses `(struct).field` (field_selection, from the POSTGRES spread) but
302        // has no `.*` value-expansion production — `(struct).*`, `ROW(t.*)`, `f(t.*)`,
303        // `t.*::type` all parse-reject (engine-probed 1.5.4). Override the POSTGRES `true`.
304        field_wildcard: false,
305        // DuckDB reaches nested `ARRAY[[1,2],[3,4]]` through `collection_literals` (a
306        // top-level `[…]` list is a value there, and levels may mix scalars and lists),
307        // so the PG multidim production stays off — override the POSTGRES `true` spread.
308        multidim_array_literals: false,
309        semi_structured_access: false,
310        // The relaxed interval spellings (`INTERVAL 3 DAYS`, `INTERVAL (x) DAY`) — a
311        // DuckDB extension over the standard quoted form the POSTGRES spread carries.
312        relaxed_interval_syntax: true,
313        ..ExpressionSyntax::POSTGRES
314    };
315}
316
317impl OperatorSyntax {
318    /// The `DUCKDB` preset for operator syntax.
319    pub const DUCKDB: Self = Self {
320        lambda_expressions: true,
321        double_equals: true,
322        integer_divide_slash: true,
323        starts_with_operator: true,
324        // Off, overriding the inherited PostgreSQL `true`: DuckDB spells `?` as the anonymous
325        // placeholder (`anonymous_question`), which contends with the `?`-led `jsonb`
326        // operators, and it has none of that PostgreSQL `jsonb` operator family. Forcing it
327        // off keeps `FeatureSet::DUCKDB` lexically consistent (the `const` assert below).
328        jsonb_operators: false,
329        // On, inheriting PostgreSQL's `true`. `^`-as-exponentiation (`caret_operator:
330        // Exponent`, on the preset below) turns on too: both were probed against DuckDB 1.5.4
331        // (`duckdb-operator-surface-sweep`, `duckdb-pg-operator-spelling-under-acceptance`).
332        //
333        // `^` is exponentiation with the *same* precedence row this preset already carries
334        // (the shared [`exponent`](crate::precedence::BindingPowerTable::exponent) rank, which
335        // `DUCKDB_BINDING_POWERS` inherits unchanged): probed `2^3*2 = 16` (`^` tighter than
336        // `*`), `2^3^2 = 64` (left-associative), `-2^2 = 4` (unary sign tighter than `^`) —
337        // identical to the PostgreSQL fit. So `CaretOperator::Exponent` is the honest model.
338        // (DuckDB also spells the same power as `**`, an unmodelled synonym with no corpus
339        // member — tracked on the sweep ticket, not this flag.)
340        //
341        // `custom_operators` turns on: DuckDB inherits PostgreSQL's generalized maximal-munch
342        // operator lexer and *parse*-accepts the same `Op`-class runs — `1 <<| 2`, `1 <-> 2`,
343        // `p &&&&&@ q`, regex `~`/`!~`/`~*` — via `duckdb_extract_statements`, then
344        // bind-rejects the ones with no backing function (`1 <<| 2` → Catalog error). A
345        // parse-accept that binds-fail is still under-acceptance when we reject at *parse*: our
346        // parser is parse-only and the DuckDB accept/reject oracle compares parse acceptance
347        // (`m2::duckdb_raw_bytes_divergence` reads `extract_statement_count`), so folding an
348        // unknown run onto [`Expr::NamedOperator`](crate::ast::Expr::NamedOperator) matches the
349        // engine's parse verdict — it does not claim DuckDB is user-extensible. DuckDB's real
350        // operators still fold onto their dedicated [`BinaryOperator`] keys ahead of the generic
351        // surface (`&&`/`^@`/`//`/`==` in `known_operator_token`), so their shape is unchanged.
352        //
353        // DuckDB's `Op` charset is PostgreSQL's *minus* `#` and `?`, which it repurposes as the
354        // positional-column sigil (`#1`) and the anonymous parameter placeholder — the lexer's
355        // `is_operator_char` drops them under `positional_column` / `anonymous_question`, so a
356        // run stops at either (`1 @#@ 2` is `@` then a stray `#` — reject on both DuckDB and
357        // here; `1 @?@ 2` is `@` then a `?` placeholder). Engine-measured across the full
358        // single/doubled/prefix/postfix/trailing-sign matrix on DuckDB 1.5.4
359        // (`duckdb-pg-operator-spelling-under-acceptance`). Backtick is an `Op`-class byte here
360        // (DuckDB does not quote identifiers with it), so `` `= `` lexes as an operator. DuckDB
361        // *postfix* symbolic operators (`1 !`, `1 ~`, `1 @` — PostgreSQL removed postfix in 14)
362        // are a distinct axis carried by `postfix_operators` below (the parser-side postfix
363        // reduction), not by this tokenizer-plus-infix/prefix flag.
364        custom_operators: true,
365        // On, overriding the inherited PostgreSQL `false`: DuckDB keeps the generalized postfix
366        // reading PostgreSQL removed in version 14 — `10!`, `1 ~`, `1 <->`, `1 &` all
367        // parse-accept (then bind-reject the ones with no backing `__postfix` function).
368        // Engine-measured on DuckDB 1.5.4 (`duckdb-postfix-operator-dimension`). See the flag
369        // doc for the MECE split against `custom_operators` and the eligible-token set.
370        postfix_operators: true,
371        // Inherited from DuckDB's PostgreSQL-fork grammar, which keeps the `a_expr ISNULL` /
372        // `a_expr NOTNULL` postfix synonyms (additive over PostgreSQL like the other shared
373        // operator knobs).
374        null_test_postfix: true,
375        // The PostgreSQL any-operator quantifier (`3 * ANY(list)`) is not a DuckDB
376        // construct — override the `..OperatorSyntax::POSTGRES` spread's `true`.
377        quantified_arbitrary_operator: false,
378        ..OperatorSyntax::POSTGRES
379    };
380}
381
382impl CallSyntax {
383    /// The `DUCKDB` preset for call syntax.
384    pub const DUCKDB: Self = Self {
385        columns_expression: true,
386        try_cast: true,
387        extract_string_field: true,
388        method_chaining: true,
389        variadic_argument: true,
390        // The PostgreSQL SQL/JSON empty-constructor reject is not modelled for DuckDB
391        // (conservative — DuckDB's `json()` surface is unprobed here); keep it a plain call.
392        sqljson_constructors_require_argument: false,
393        // DuckDB has no SQL:2016 SQL/JSON expression-function special forms (its JSON
394        // support is ordinary functions like `json_extract`), so the keyword heads stay
395        // plain call/name forms — override the inherited PostgreSQL `true`.
396        sqljson_expression_functions: false,
397        // DuckDB has no SQL/XML expression functions; override the inherited PostgreSQL
398        // `true` so the `xml*` keyword heads stay plain call/name forms.
399        xml_expression_functions: false,
400        // DuckDB has no `merge_action()` support function; override PostgreSQL's `true` so
401        // the reserved keyword head stays the "no call form" reject (conservative — DuckDB's
402        // MERGE surface is unprobed here).
403        merge_action_function: false,
404        ..CallSyntax::POSTGRES
405    };
406}
407
408impl StringFuncForms {
409    /// The `DUCKDB` preset for string func forms.
410    pub const DUCKDB: Self = Self {
411        // DuckDB's PG-fork string special forms diverge from PostgreSQL in exactly two
412        // knobs (both probed on 1.5.4): the SIMILAR/ESCAPE regex substring production
413        // was dropped (parser error), and OVERLAY kept *only* the PLACING form —
414        // `overlay('abc', 'X', 2, 1)` / `overlay('abc')` / `overlay()` are parser
415        // errors where PostgreSQL parse-accepts them as plain calls. Everything else
416        // (FROM/FOR + FOR-leading substring orders, b_expr POSITION operands, the
417        // loose trim_list tails) inherits PostgreSQL's `true` verbatim, each probed
418        // parse-accepting on the live engine.
419        substring_similar: false,
420        overlay_requires_placing: true,
421        // DuckDB's `COLLATION FOR (<expr>)` surface is unprobed; override PostgreSQL's
422        // `true` back to `false` (conservative — `COLLATION` stays an ordinary name head).
423        collation_for_expression: false,
424        ..StringFuncForms::POSTGRES
425    };
426}
427
428impl AggregateCallSyntax {
429    /// The `DUCKDB` preset for aggregate call syntax.
430    pub const DUCKDB: Self = Self {
431        null_treatment: true,
432        standalone_argument_order_by: true,
433        // DuckDB accepts `FILTER (<predicate>)` without the standard `WHERE` (probed on 1.5.4).
434        filter_optional_where: true,
435        ..AggregateCallSyntax::POSTGRES
436    };
437}
438
439impl TypeNameSyntax {
440    /// The `DUCKDB` preset for type name syntax.
441    pub const DUCKDB: Self = Self {
442        composite_types: true,
443        enum_type: true,
444        // Empty `DECIMAL()`/`DEC()`/`NUMERIC()` parens mean the default `(18,3)` — probed on
445        // 1.5.4, byte-identical to a bare `DECIMAL` (`duckdb-empty-type-parens`).
446        empty_type_parens: true,
447        // DuckDB requires an unsigned `DECIMAL` modifier — a negative scale is rejected
448        // (probed on 1.5.4), unlike PostgreSQL, so this PG-inherited flag is turned back off.
449        signed_type_modifier: false,
450        // DuckDB admits a string-literal type modifier on a user-defined type name —
451        // `GEOMETRY('OGC:CRS84')` and the general `type_name('constant', ...)` form (probed
452        // on 1.5.4). `duckdb-geometry-type-and-overlaps-operator`.
453        string_type_modifiers: true,
454        ..TypeNameSyntax::POSTGRES
455    };
456}
457
458impl TableExpressionSyntax {
459    /// The `DUCKDB` preset for table expression syntax.
460    pub const DUCKDB: Self = Self {
461        // DuckDB's string-literal table alias (`FROM integers AS 't'('k')` / `t('k')`;
462        // probed on 1.5.4). `duckdb-string-literal-table-alias`.
463        string_literal_aliases: true,
464        ..TableExpressionSyntax::POSTGRES
465    };
466}
467
468impl JoinSyntax {
469    /// The `DUCKDB` preset for join syntax.
470    pub const DUCKDB: Self = Self {
471        asof_join: true,
472        positional_join: true,
473        semi_anti_join: true,
474        // Spark/Hive-only; DuckDB parse-rejects the sided `LEFT/RIGHT SEMI/ANTI JOIN`
475        // spelling (engine-probed), accepting only its own side-less `SEMI`/`ANTI JOIN`.
476        sided_semi_anti_join: false,
477        // MSSQL-only; DuckDB parse-rejects `APPLY` in join position.
478        apply_join: false,
479        // DuckDB parse-rejects the SQL:2023 recursive-query SEARCH/CYCLE clauses
480        // (`syntax error at or near "SEARCH"`, probed on 1.5.4), so it overrides the
481        // PostgreSQL surface it otherwise inherits below — the `data_modifying_ctes` split.
482        recursive_search_cycle: false,
483        // DuckDB parse-rejects a top-level ORDER BY/LIMIT/OFFSET on a `UNION`-bodied
484        // recursive CTE (`Parser Error: ORDER BY in a recursive query is not allowed`;
485        // probed on 1.5.4), overriding the inherited PostgreSQL parse-accept.
486        // `duckdb-recursive-cte-term-restrictions-over-accept`.
487        recursive_union_rejects_order_limit: true,
488        // DuckDB's keyed-recursion `USING KEY (cols)` clause between the CTE column list and
489        // `AS` (stable since 1.3; probed accepting on 1.5.4), overriding the inherited
490        // PostgreSQL off. `duckdb-with-using-key`.
491        recursive_using_key: true,
492        ..JoinSyntax::POSTGRES
493    };
494}
495
496impl TableFactorSyntax {
497    /// The `DUCKDB` preset for table factor syntax.
498    pub const DUCKDB: Self = Self {
499        pivot: true,
500        unpivot: true,
501        // DuckDB's `DESCRIBE`/`SHOW`/`SUMMARIZE` utility as a parenthesized `FROM`
502        // table source (its `SHOW_REF` table_ref; probed on 1.5.4).
503        // `duckdb-statement-in-query-position`.
504        show_ref: true,
505        // DuckDB's bare `FROM VALUES (…) AS t` row-list table factor (no parentheses;
506        // probed on 1.5.4). `duckdb-from-values-table-factor`.
507        from_values: true,
508        // DuckDB parse-rejects `JSON_TABLE(… COLUMNS …)` (reads `json_table` as an ordinary
509        // name) and `XMLTABLE(` (probed on 1.5.4), so both override the inherited PostgreSQL
510        // surface off — the same `recursive_search_cycle` split above.
511        json_table: false,
512        xml_table: false,
513        ..TableFactorSyntax::POSTGRES
514    };
515}
516
517impl UtilitySyntax {
518    /// The `DUCKDB` preset for utility syntax.
519    pub const DUCKDB: Self = Self {
520        pragma: true,
521        use_statement: true,
522        // DuckDB's `USE <catalog> . <schema>` admits the dotted two-part name (MySQL's
523        // single-ident form is a subset); the deeper `USE a.b.c` is still parser-rejected.
524        use_qualified_name: true,
525        prepared_statements: true,
526        // Override the `..UtilitySyntax::POSTGRES` spread's `true`: DuckDB structurally
527        // rejects the PostgreSQL `PREPARE name(<type>, …)` typed parameter-type list
528        // ("Prepared statement argument types are not supported, use CAST"; probed on
529        // 1.5.4), so only the bare `PREPARE <name> AS <statement>` form is admitted.
530        prepare_typed_parameters: false,
531        call: true,
532        // DuckDB has `ATTACH`/`DETACH` (its own catalog databases), so the shared gate is
533        // on — closing the `DETACH <db>` coverage gap. It extends `DETACH DATABASE` with an
534        // `IF EXISTS` guard (`detach_if_exists`), and adds the `[FORCE] CHECKPOINT [db]`
535        // operands (`checkpoint_database`), the bare-identifier `LOAD tpch` argument
536        // (`load_bare_name`), and the `RESET`-scope prefix (`reset_scope`) — all DuckDB
537        // extensions over the inherited PostgreSQL `checkpoint`/`load_extension` base.
538        attach: true,
539        load_bare_name: true,
540        reset_scope: true,
541        detach_if_exists: true,
542        // Override the PostgreSQL base: DuckDB has no `DO` anonymous code block (`DO $$...$$`
543        // is a parser error, probed on 1.5.4), so the leading `DO` keyword stays
544        // undispatched and surfaces as an unknown statement.
545        do_statement: false,
546        // DuckDB's `EXPORT DATABASE`/`IMPORT DATABASE` catalogue round-trip — the pair is a
547        // DuckDB extension over the inherited PostgreSQL base (which has neither), gated as
548        // one unit.
549        export_import_database: true,
550        // DuckDB's `UPDATE EXTENSIONS [( <name>, ... )]` extension-refresh statement — a
551        // DuckDB extension over the inherited PostgreSQL base (which has no such statement).
552        update_extensions: true,
553        // Spread-inheritance invariant: DuckDB is PostgreSQL-derived, so this
554        // spread inherits PostgreSQL's utility values for every gate not overridden above —
555        // including live `true`s (`prepared_statements`, `load_extension`). Inheritance is
556        // deliberate and load-bearing: "inherits only falses" does NOT hold here, and a
557        // PostgreSQL base flip propagates silently. The statement-head family is pinned by
558        // absolute value in head_contention's `statement_head_gate_values_are_pinned_per_preset`.
559        ..UtilitySyntax::POSTGRES
560    };
561}
562
563impl ShowSyntax {
564    /// The `DUCKDB` preset for show syntax.
565    pub const DUCKDB: Self = Self {
566        // DuckDB's `{DESCRIBE | SUMMARIZE} <query> | <table>` introspection statement — the
567        // `SHOW_REF` utility (probed on 1.5.4), which it desugars to `SELECT * FROM
568        // (SHOW_REF …)` and shares with the parenthesized-`FROM` `show_ref` table factor.
569        describe_summarize: true,
570        // DuckDB's `SHOW [ALL] TABLES [FROM <schema>]` catalogue listing (engine-probed
571        // 1.5.4), which it desugars to `SELECT * FROM (SHOW_REF …)`; modelled as the typed
572        // statement, distinct from the generic session `SHOW <var>` inherited from PG.
573        show_tables: true,
574        ..ShowSyntax::POSTGRES
575    };
576}
577
578impl MaintenanceSyntax {
579    /// The `DUCKDB` preset for maintenance syntax.
580    pub const DUCKDB: Self = Self {
581        checkpoint_database: true,
582        // DuckDB's `VACUUM [ANALYZE] [<table> [(<cols>)]]` and `ANALYZE [<table>
583        // [(<cols>)]]` statistics/compaction statements (both `PGVacuumStmt` in libpg_query;
584        // engine-probed 1.5.4). The leading `VACUUM` dispatches under `vacuum_analyze` (a
585        // separate gate from SQLite's `INTO`-shaped `vacuum`, which stays off — inherited
586        // from PostgreSQL); the leading `ANALYZE` under `analyze`, with the DuckDB column
587        // list under `analyze_columns`. Only the `ANALYZE` vacuum option parses —
588        // `FULL`/`FREEZE`/`VERBOSE`/`disable_page_skipping` throw in 1.5.4's transform.
589        vacuum_analyze: true,
590        analyze: true,
591        analyze_columns: true,
592        // Spread-inheritance invariant: inherits PostgreSQL's maintenance values for
593        // every gate not armed above (`vacuum` / `table_maintenance` stay `false` from the
594        // base); a PostgreSQL base flip propagates silently. Pinned by value in
595        // head_contention's `statement_head_gate_values_are_pinned_per_preset`.
596        ..MaintenanceSyntax::POSTGRES
597    };
598}
599
600impl AccessControlSyntax {
601    /// The `DUCKDB` preset for access control syntax.
602    pub const DUCKDB: Self = Self {
603        // Spread-inheritance invariant: DuckDB's access-control surface is exactly
604        // PostgreSQL's — it inherits every gate, including the live `true`s `access_control`
605        // and `access_control_extended_objects`, and re-stamps nothing. A PostgreSQL base flip
606        // propagates here silently; pinned by value in head_contention's
607        // `statement_head_gate_values_are_pinned_per_preset`.
608        ..AccessControlSyntax::POSTGRES
609    };
610}
611
612impl FeatureSet {
613    /// DuckDB as PostgreSQL-derived dialect data.
614    pub const DUCKDB: Self = Self {
615        identifier_casing: Casing::Lower,
616        identifier_quotes: STANDARD_IDENTIFIER_QUOTES,
617        default_null_ordering: NullOrdering::NullsLast,
618        reserved_column_name: DUCKDB_RESERVED_COLUMN_NAME,
619        reserved_function_name: DUCKDB_RESERVED_FUNCTION_NAME,
620        reserved_type_name: DUCKDB_RESERVED_TYPE_NAME,
621        reserved_bare_alias: DUCKDB_RESERVED_BARE_ALIAS,
622        reserved_as_label: KeywordSet::EMPTY,
623        // DuckDB relation names are `catalog.schema.table` in the shared table path (its
624        // own two-part narrowing is a separate, unstarted tightening).
625        catalog_qualified_names: true,
626        // The shared M1 table plus the vertical tab (`0x0b`) as statement-boundary trim
627        // (`DUCKDB_BYTE_CLASSES`): `libduckdb`-measured, DuckDB folds a `\v` at each
628        // `;`-segment's leading/trailing edge (`"\x0bSELECT 1"`, `"SELECT 1\x0b"` accept)
629        // but rejects one interior to a statement's content (`"SELECT\x0b1"` rejects,
630        // even beside a real space) — the tokenizer's `skip_trivia` boundary guard.
631        byte_classes: DUCKDB_BYTE_CLASSES,
632        binding_powers: DUCKDB_BINDING_POWERS,
633        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
634        string_literals: StringLiteralSyntax::POSTGRES,
635        numeric_literals: NumericLiteralSyntax::DUCKDB,
636        // PostgreSQL's `$1` positional parameters plus DuckDB's anonymous `?` placeholder
637        // (`SELECT 'Test' LIMIT ?`). `?` adds a second parameter claimant but no lexical
638        // conflict: DuckDB has no `?`-led operator (its JSON `?`/`?|`/`?&` existence
639        // operators are unimplemented — `SELECT '{}'::JSON ? 'a'` syntax-errors on 1.5.4),
640        // so `?` has a single claimant and the `is_lexically_consistent` ratchet below
641        // still holds.
642        parameters: ParameterSyntax {
643            anonymous_question: true,
644            ..ParameterSyntax::POSTGRES
645        },
646        session_variables: SessionVariableSyntax::ANSI,
647        identifier_syntax: IdentifierSyntax::POSTGRES,
648        table_expressions: TableExpressionSyntax::DUCKDB,
649        join_syntax: JoinSyntax::DUCKDB,
650        table_factor_syntax: TableFactorSyntax::DUCKDB,
651        expression_syntax: ExpressionSyntax::DUCKDB,
652        operator_syntax: OperatorSyntax::DUCKDB,
653        call_syntax: CallSyntax::DUCKDB,
654        string_func_forms: StringFuncForms::DUCKDB,
655        aggregate_call_syntax: AggregateCallSyntax::DUCKDB,
656        predicate_syntax: PredicateSyntax::DUCKDB,
657        pipe_operator: PipeOperator::StringConcat,
658        double_ampersand: DoubleAmpersand::Overlaps,
659        // DuckDB's `GLOB` infix (desugars to `~~~` glob match; probed 1.5.4). MATCH/REGEXP
660        // keyword forms are not DuckDB infix operators.
661        keyword_operators: KeywordOperators::DuckDb,
662        // The one bitwise divergence from PostgreSQL: DuckDB has no bitwise XOR operator —
663        // it rejects PostgreSQL's `#` and reads `^` as *exponentiation* (both measured on
664        // 1.5.4: `SELECT 5 # 3` syntax-errors, `SELECT 5 ^ 3` is `125`). It spells bitwise
665        // XOR as the `xor(a, b)` function instead, which parses as an ordinary call. The
666        // shared `| & ~ << >>` family stays on, inherited via `ExpressionSyntax::DUCKDB`.
667        // `^`-as-exponentiation is the honest `caret_operator` reading (rationale in the
668        // `OperatorSyntax::DUCKDB` block above); `#` is not the XOR operator.
669        caret_operator: CaretOperator::Exponent,
670        hash_bitwise_xor: false,
671        // DuckDB shares PostgreSQL's flex-derived scanner, so a `--` line comment ends at
672        // `\r` as well as `\n` (measured on 1.5.4) — `CommentSyntax::POSTGRES`, not the
673        // `\n`-only ANSI baseline. Block-comment nesting (also on in `POSTGRES`) already
674        // matched DuckDB, so this is the only comment-shape change from `ANSI`.
675        comment_syntax: CommentSyntax::POSTGRES,
676        mutation_syntax: MutationSyntax {
677            // DuckDB parse-rejects a DML CTE body (`A CTE needs a SELECT`; INSERT,
678            // UPDATE, and DELETE bodies all probed on 1.5.4), so it must not inherit
679            // the POSTGRES `true` through the spread below. Everything else — MERGE
680            // (1.4+), MERGE … RETURNING, and the leading `WITH` before MERGE, all
681            // probed accepted on 1.5.4 — is shared with PostgreSQL.
682            data_modifying_ctes: false,
683            // DuckDB rejects `OVERRIDING` *inside* MERGE (`syntax error at or near
684            // "OVERRIDING"`, probed on 1.5.4) even though it accepts it on a top-level
685            // INSERT, so it splits from the POSTGRES spread in this one merge knob (the
686            // `WHEN NOT MATCHED BY SOURCE/TARGET` arms and `INSERT DEFAULT VALUES` — both
687            // probed accepted on 1.5.4 — stay inherited `true`).
688            merge_insert_overriding: false,
689            // DuckDB MERGE extensions (probed on 1.5.4): `UPDATE SET *`, `INSERT *` /
690            // `INSERT BY NAME [*]`, and `THEN ERROR`.
691            merge_update_set_star: true,
692            merge_insert_star_by_name: true,
693            merge_error_action: true,
694            // DuckDB accepts SQLite-style `INSERT OR REPLACE` / `OR IGNORE` (probed 1.5.4).
695            or_conflict_action: true,
696            insert_column_matching: true,
697            // DuckDB parser rejects explicit tuple-assignment value-row arity mismatches
698            // (`UPDATE t SET (a, b, c) = (1, 2)`), unlike PostgreSQL parse-only.
699            update_tuple_value_row_arity: true,
700            // DuckDB rejects qualified SET targets (`UPDATE t SET t.i = 1` — probed 1.5.4).
701            update_set_qualified_column: false,
702            ..MutationSyntax::POSTGRES
703        },
704        // PostgreSQL's schema-change surface plus DuckDB's live-body macro DDL
705        // (`CREATE MACRO`/`CREATE FUNCTION … AS <expr>|TABLE <query>`), `CREATE OR REPLACE
706        // TABLE`, and the `CREATE [PERSISTENT] SECRET` secrets statement.
707        statement_ddl_gates: StatementDdlGates {
708            create_macro: true,
709            create_secret: true,
710            create_type: true,
711            // DuckDB has no `CREATE DATABASE` (it uses `ATTACH`); the shared gate is off so
712            // `DATABASE` after `CREATE` falls through as an unknown statement (probed
713            // 1.5.4: "syntax error at or near \"DATABASE\"").
714            databases: false,
715            // DuckDB rejects the SQL-standard embedded schema-element form that the
716            // POSTGRES spread turns on, so it is reset off here.
717            schema_elements: false,
718            // DuckDB accepts `CREATE [OR REPLACE] [TEMP] RECURSIVE VIEW v (cols) AS …`
719            // (engine-measured on 1.5.4); the POSTGRES spread leaves it `false`.
720            recursive_views: true,
721            // DuckDB manages extensions with `INSTALL`/`LOAD`, not the PostgreSQL
722            // `CREATE`/`ALTER EXTENSION` catalogue DDL, so it must clear the POSTGRES `true`
723            // rather than inherit it through the spread below.
724            extension_ddl: false,
725            // DuckDB has no transform catalogue (`pg_transform` / `CREATE TRANSFORM` is
726            // PostgreSQL-only), so it must clear the POSTGRES `true` rather than inherit it.
727            transform_ddl: false,
728            // DuckDB has no `ALTER SYSTEM` server-configuration DDL (it configures through
729            // `SET`/`RESET`), so it must clear the POSTGRES `true` rather than inherit it.
730            alter_system: false,
731            // MySQL's tablespace / logfile-group storage DDL is not a DuckDB statement.
732            tablespace_ddl: false,
733            logfile_group_ddl: false,
734            // DuckDB's `ALTER DATABASE … SET ALIAS TO`, `ALTER SEQUENCE …` option list, and
735            // `ALTER {TABLE|VIEW|SEQUENCE} … SET SCHEMA` forms (engine-measured on 1.5.4); the
736            // POSTGRES spread leaves them `false`, so they are set on explicitly here.
737            alter_database: true,
738            // MySQL-only families (no DuckDB equivalent); the POSTGRES spread leaves them
739            // `false`, set explicitly for clarity.
740            alter_database_options: false,
741            server_definition: false,
742            alter_instance: false,
743            spatial_reference_system: false,
744            resource_group: false,
745            alter_sequence: true,
746            alter_object_set_schema: true,
747            // Spread-inheritance invariant: the statement-head DDL gates not armed above
748            // (`view_definition_options`, `drop_database`) inherit PostgreSQL's `false`; a base
749            // flip propagates silently. Pinned by value in head_contention's
750            // `statement_head_gate_values_are_pinned_per_preset`.
751            ..StatementDdlGates::POSTGRES
752        },
753        create_table_clause_syntax: CreateTableClauseSyntax {
754            create_or_replace_table: true,
755            // DuckDB has no PostgreSQL-style declarative partitioning (its `PARTITION_BY` is a
756            // COPY/export option, not a `CREATE TABLE` clause), so it must not inherit the
757            // POSTGRES `true` through the spread below.
758            declarative_partitioning: false,
759            // DuckDB has no table inheritance and (probed against libduckdb) rejects the
760            // PostgreSQL `(LIKE src …)` source-table element, so both must clear the POSTGRES
761            // `true` rather than inherit it through the spread below.
762            table_inheritance: false,
763            like_source_table: false,
764            table_access_method: false,
765            without_oids: false,
766            typed_tables: false,
767            create_table_as_execute: false,
768            ..CreateTableClauseSyntax::POSTGRES
769        },
770        column_definition_syntax: ColumnDefinitionSyntax {
771            // The PostgreSQL `b_expr` column-default restriction is not modelled for DuckDB
772            // (conservative — DuckDB's default-expression grammar class is unprobed here); it
773            // reads the default as a full `a_expr`, unchanged.
774            column_default_requires_b_expr: false,
775            // DuckDB (probed against libduckdb) accepts a per-column `COLLATE <name>` and the
776            // `UNLOGGED` persistence keyword — both inherit the POSTGRES `true` through the
777            // spread — but rejects the column STORAGE/COMPRESSION attributes, the table USING
778            // access method, WITHOUT OIDS, and typed `OF <type>` tables, so those four must
779            // clear the POSTGRES `true` rather than inherit it.
780            column_storage: false,
781            // DuckDB accepts the keywordless generated-column shorthand `<col> <type> AS
782            // (<expr>) [VIRTUAL|STORED]` written without `GENERATED ALWAYS` (libduckdb 1.5.4:
783            // `y INT AS (x + 1)` and `… VIRTUAL` parse-accept; `STORED` parses but is a binder
784            // reject, out of this layer). PostgreSQL requires the keywords, so this must set
785            // rather than inherit the POSTGRES `false`.
786            generated_column_shorthand: true,
787            // DuckDB requires a data type on every column *except* a generated one: both the
788            // `AS (<expr>)` shorthand and the keyworded `GENERATED …` form may drop the type
789            // (`CREATE TABLE t (x INT, gen_x AS (x + 5))`), while a plain typeless column is a
790            // parse error. A narrowing of the SQLite typeless rule, distinct from PostgreSQL's
791            // type-required `false`, so it too must set rather than inherit.
792            typeless_generated_columns: true,
793            ..ColumnDefinitionSyntax::POSTGRES
794        },
795        constraint_syntax: ConstraintSyntax {
796            // DuckDB (probed against libduckdb 1.5.4) rejects PostgreSQL's `EXCLUDE` exclusion
797            // constraints, the `AS EXECUTE` CTAS form, and the `UNIQUE`/`PRIMARY KEY`
798            // index-parameter decorations (`INCLUDE`/`NULLS NOT DISTINCT`/`USING INDEX
799            // TABLESPACE`), so all three must clear the POSTGRES `true`. It *does* accept the
800            // `NO INHERIT` / `NOT VALID` constraint markers, which therefore inherit the POSTGRES
801            // `true` through the spread.
802            exclusion_constraints: false,
803            index_constraint_parameters: false,
804            // DuckDB admits `ON DELETE`/`ON UPDATE` only for `RESTRICT`/`NO ACTION` —
805            // `CASCADE`/`SET NULL`/`SET DEFAULT` are Parser Errors (probed 1.5.4).
806            referential_action_cascade_set: false,
807            // DuckDB (and SQLite) parse-reject subqueries in `CHECK` (probed 1.5.4).
808            check_constraint_subqueries: false,
809            ..ConstraintSyntax::POSTGRES
810        },
811        index_alter_syntax: IndexAlterSyntax {
812            // DuckDB's extended ALTER surface is on (IF EXISTS, RENAME, ALTER COLUMN, …)
813            // but multi-action lists are not ("Only one ALTER command per statement",
814            // probed 1.5.4).
815            alter_table_multiple_actions: false,
816            alter_nested_column_paths: true,
817            // Spread-inheritance invariant: the `index_drop_on_table` statement head
818            // inherits PostgreSQL's `false` through this spread; a base flip propagates
819            // silently. Pinned by value in head_contention's
820            // `statement_head_gate_values_are_pinned_per_preset`.
821            ..IndexAlterSyntax::POSTGRES
822        },
823        existence_guards: ExistenceGuards::POSTGRES,
824        select_syntax: SelectSyntax::DUCKDB,
825        query_tail_syntax: QueryTailSyntax::DUCKDB,
826        grouping_syntax: GroupingSyntax::DUCKDB,
827        utility_syntax: UtilitySyntax::DUCKDB,
828        show_syntax: ShowSyntax::DUCKDB,
829        maintenance_syntax: MaintenanceSyntax::DUCKDB,
830        access_control_syntax: AccessControlSyntax::DUCKDB,
831        type_name_syntax: TypeNameSyntax::DUCKDB,
832        // No DuckDB-specific Tier-1 output spelling yet: DuckDB shares PostgreSQL's
833        // canonical type names (`INTEGER`, `VARCHAR`, `DECIMAL`), so it renders through
834        // the PostgreSQL spelling table (minting a `TargetSpelling::DuckDb` is render
835        // work a later ticket owns).
836        target_spelling: TargetSpelling::Postgres,
837    };
838}
839
840/// Prefer [`FeatureSet::DUCKDB`] for struct update.
841pub const DUCKDB: FeatureSet = FeatureSet::DUCKDB;
842
843// Compile-time proof the DuckDB preset claims no shared tokenizer trigger twice. Beyond
844// PostgreSQL's lexical surface it adds exactly one trigger — the anonymous `?` parameter
845// (`anonymous_question`) — which has a single claimant: DuckDB implements no `?`-led
846// operator (its JSON `?`/`?|`/`?&` existence operators are absent), so `?` cannot contend
847// and needs no registered conflict. The rest is PostgreSQL's surface: the numeric-radix
848// scan, empty-target grammar gate, and QUALIFY reservation add no lexical trigger,
849// `collection_literals` reuses the `[` punctuation PostgreSQL's `subscript`/
850// `array_constructor` already claim (no preset here quotes identifiers with `[`, so the
851// registered `BracketIdentifierVersusArraySyntax` conflict cannot fire), and
852// `lambda_expressions` is a grammar-position gate over the `->` token
853// `json_arrow_operators` already lexes (one lexical claimant; the lambda/JSON split
854// happens in the parser by LHS shape). Kept as its own ratchet so a future DuckDB delta
855// that *does* add a trigger fails the build here.
856const _: () = assert!(FeatureSet::DUCKDB.is_lexically_consistent());
857// The two sibling self-consistency registries are ratcheted the same way, so the
858// parse-entry `debug_assert!` folds all three to dead code for this preset: every
859// refinement flag (`slice_step`, `checkpoint_database`, `analyze_columns`, the bare-name
860// utility tails) rides its enabled base, and no two features contend for one
861// parser-position head (`prepared_statements_from` stays off, so the typed-`AS` lifecycle
862// is unrivalled).
863const _: () = assert!(FeatureSet::DUCKDB.has_satisfied_feature_dependencies());
864const _: () = assert!(FeatureSet::DUCKDB.has_no_grammar_conflict());
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869
870    #[test]
871    fn duckdb_is_postgres_plus_the_measured_deltas() {
872        // The preset is PostgreSQL with a documented set of divergent axes (numeric
873        // radix, SELECT surface, expression surface, table expressions, call surface —
874        // `COLUMNS(…)` + `TRY_CAST` — type-name surface — the anonymous composite types —
875        // the binding-power `->` re-rank, and the keyword reservations); asserting the
876        // whole rest equals PostgreSQL keeps the "PG-derived, every delta documented"
877        // claim honest against a future stray edit.
878        let pg = FeatureSet::POSTGRES;
879        let duck = FeatureSet::DUCKDB;
880        assert_eq!(duck.numeric_literals, NumericLiteralSyntax::DUCKDB);
881        assert_eq!(duck.select_syntax, SelectSyntax::DUCKDB);
882        assert_eq!(duck.expression_syntax, ExpressionSyntax::DUCKDB);
883        assert_eq!(duck.table_expressions, TableExpressionSyntax::DUCKDB);
884        assert_eq!(duck.call_syntax, CallSyntax::DUCKDB);
885        assert_eq!(duck.type_name_syntax, TypeNameSyntax::DUCKDB);
886        assert_ne!(duck.numeric_literals, pg.numeric_literals);
887        assert_ne!(duck.select_syntax, pg.select_syntax);
888        assert_ne!(duck.expression_syntax, pg.expression_syntax);
889        assert_ne!(duck.table_expressions, pg.table_expressions);
890        assert_ne!(duck.call_syntax, pg.call_syntax);
891        assert_ne!(duck.type_name_syntax, pg.type_name_syntax);
892        assert_eq!(duck.binding_powers, DUCKDB_BINDING_POWERS);
893        assert_ne!(duck.numeric_literals, pg.numeric_literals);
894        assert_ne!(duck.select_syntax, pg.select_syntax);
895        assert_ne!(duck.expression_syntax, pg.expression_syntax);
896        assert_ne!(duck.binding_powers, pg.binding_powers);
897        assert_eq!(duck.reserved_column_name, DUCKDB_RESERVED_COLUMN_NAME);
898        assert_eq!(duck.reserved_function_name, DUCKDB_RESERVED_FUNCTION_NAME);
899        assert_eq!(duck.reserved_type_name, DUCKDB_RESERVED_TYPE_NAME);
900        assert_eq!(duck.reserved_bare_alias, DUCKDB_RESERVED_BARE_ALIAS);
901
902        // The utility surface adds the DuckDB `PRAGMA`, `USE`, and `CALL` statements plus
903        // the `ATTACH`/`DETACH` pair and the DuckDB `CHECKPOINT`/`LOAD`/`RESET`/`DETACH`
904        // extensions (`duckdb-settings-and-session-statements`, `duckdb-prepare-execute-call`,
905        // `duckdb-utility-checkpoint-detach-load-reset`) over the inherited PostgreSQL
906        // `COPY`/`COMMENT ON`/session/`CHECKPOINT`/`LOAD`/prepared-statement-lifecycle base
907        // — a purely additive delta, plus the two reverse deltas noted below.
908        assert_eq!(duck.utility_syntax, UtilitySyntax::DUCKDB);
909        assert_eq!(
910            duck.utility_syntax,
911            UtilitySyntax {
912                pragma: true,
913                use_statement: true,
914                use_qualified_name: true,
915                call: true,
916                attach: true,
917                load_bare_name: true,
918                reset_scope: true,
919                detach_if_exists: true,
920                // DuckDB's `EXPORT DATABASE`/`IMPORT DATABASE` catalogue round-trip, off in
921                // the PostgreSQL base.
922                export_import_database: true,
923                // DuckDB's `UPDATE EXTENSIONS` extension-refresh statement, off in the
924                // PostgreSQL base.
925                update_extensions: true,
926                // The two flags DuckDB turns *off* relative to PostgreSQL: PostgreSQL's `DO`
927                // anonymous code block (no DuckDB equivalent), and the `PREPARE` typed
928                // parameter-type list (DuckDB structurally rejects it: "Prepared statement
929                // argument types are not supported, use CAST" — probed on 1.5.4). The base
930                // `prepared_statements` lifecycle itself (`PREPARE`/`EXECUTE`/`DEALLOCATE`) is
931                // inherited unchanged from PostgreSQL, both on.
932                do_statement: false,
933                prepare_typed_parameters: false,
934                ..pg.utility_syntax
935            },
936        );
937        assert_eq!(
938            duck.maintenance_syntax,
939            MaintenanceSyntax {
940                checkpoint_database: true,
941                // DuckDB's `VACUUM [ANALYZE] [<table> [(<cols>)]]` / `ANALYZE [<table>
942                // [(<cols>)]]` statistics/compaction statements, off in the PostgreSQL base
943                // (whose own VACUUM/ANALYZE grammar is unmodelled).
944                vacuum_analyze: true,
945                analyze: true,
946                analyze_columns: true,
947                ..pg.maintenance_syntax
948            },
949        );
950        assert_eq!(
951            duck.show_syntax,
952            ShowSyntax {
953                // DuckDB's typed `SHOW [ALL] TABLES [FROM <schema>]` catalogue listing, off
954                // in the PostgreSQL base.
955                show_tables: true,
956                // DuckDB's leading-keyword `{DESCRIBE | SUMMARIZE}` introspection statement,
957                // off in the PostgreSQL base.
958                describe_summarize: true,
959                ..pg.show_syntax
960            },
961        );
962        assert!(duck.utility_syntax.pragma && !pg.utility_syntax.pragma);
963        assert!(duck.utility_syntax.use_statement && !pg.utility_syntax.use_statement);
964        assert!(duck.utility_syntax.prepared_statements && pg.utility_syntax.prepared_statements);
965        assert!(
966            !duck.utility_syntax.prepare_typed_parameters
967                && pg.utility_syntax.prepare_typed_parameters
968        );
969        assert!(duck.utility_syntax.call && !pg.utility_syntax.call);
970        assert!(duck.utility_syntax.attach && !pg.utility_syntax.attach);
971        // `checkpoint`/`load_extension` are inherited from PostgreSQL (both on there); the
972        // DuckDB operand/argument/scope/guard extensions are the divergence.
973        assert!(duck.maintenance_syntax.checkpoint && pg.maintenance_syntax.checkpoint);
974        assert!(duck.utility_syntax.load_extension && pg.utility_syntax.load_extension);
975        assert!(
976            duck.maintenance_syntax.checkpoint_database
977                && !pg.maintenance_syntax.checkpoint_database
978        );
979        assert!(duck.utility_syntax.load_bare_name && !pg.utility_syntax.load_bare_name);
980        assert!(duck.utility_syntax.reset_scope && !pg.utility_syntax.reset_scope);
981        assert!(duck.utility_syntax.detach_if_exists && !pg.utility_syntax.detach_if_exists);
982        assert!(duck.show_syntax.show_tables && !pg.show_syntax.show_tables);
983        assert!(duck.show_syntax.describe_summarize && !pg.show_syntax.describe_summarize);
984        // `UPDATE EXTENSIONS` is DuckDB-only over the PostgreSQL base.
985        assert!(duck.utility_syntax.update_extensions && !pg.utility_syntax.update_extensions);
986        // `do_statement` is the reverse divergence: on in PostgreSQL, off in DuckDB.
987        assert!(!duck.utility_syntax.do_statement && pg.utility_syntax.do_statement);
988
989        // Everything else is inherited verbatim from PostgreSQL.
990        assert_eq!(duck.string_literals, pg.string_literals);
991        // Parameters differ in exactly one knob: DuckDB lexes the anonymous `?`
992        // placeholder, which PostgreSQL does not (PG uses `$1` only). Every other field is
993        // inherited (forcing `anonymous_question` off recovers PG).
994        assert!(duck.parameters.anonymous_question);
995        assert!(!pg.parameters.anonymous_question);
996        assert_eq!(
997            ParameterSyntax {
998                anonymous_question: false,
999                ..duck.parameters
1000            },
1001            pg.parameters,
1002        );
1003        // The mutation surface differs in the listed DuckDB/PG deltas: PostgreSQL admits
1004        // data-modifying CTE bodies (which DuckDB parse-rejects, `A CTE needs a
1005        // SELECT`) and `OVERRIDING` inside a MERGE insert (which DuckDB parse-rejects,
1006        // `syntax error at or near "OVERRIDING"`) — both probed on 1.5.4 — while DuckDB
1007        // adds MERGE star/by-name/error actions, INSERT column matching / verb-level
1008        // conflict actions, parse-time tuple value-row arity checks, and rejects qualified
1009        // UPDATE SET targets. Everything
1010        // else — including MERGE, its RETURNING tail, the leading `WITH` before MERGE,
1011        // the `WHEN NOT MATCHED BY SOURCE/TARGET` arms, and `INSERT DEFAULT VALUES`
1012        // (all probed accepted on 1.5.4) — is inherited verbatim (forcing the two
1013        // knobs on recovers PG).
1014        assert!(!duck.mutation_syntax.data_modifying_ctes);
1015        assert!(pg.mutation_syntax.data_modifying_ctes);
1016        assert!(!duck.mutation_syntax.merge_insert_overriding);
1017        assert!(pg.mutation_syntax.merge_insert_overriding);
1018        assert!(duck.mutation_syntax.merge_update_set_star);
1019        assert!(duck.mutation_syntax.merge_insert_star_by_name);
1020        assert!(duck.mutation_syntax.merge_error_action);
1021        assert!(!pg.mutation_syntax.merge_update_set_star);
1022        assert!(duck.mutation_syntax.merge_when_not_matched_by);
1023        assert!(duck.mutation_syntax.merge_insert_default_values);
1024        assert!(!duck.mutation_syntax.update_set_qualified_column);
1025        assert!(pg.mutation_syntax.update_set_qualified_column);
1026        assert_eq!(
1027            MutationSyntax {
1028                data_modifying_ctes: true,
1029                merge_insert_overriding: true,
1030                merge_update_set_star: false,
1031                merge_insert_star_by_name: false,
1032                merge_error_action: false,
1033                insert_column_matching: false,
1034                or_conflict_action: false,
1035                update_tuple_value_row_arity: false,
1036                update_set_qualified_column: true,
1037                ..duck.mutation_syntax
1038            },
1039            pg.mutation_syntax,
1040        );
1041        // The schema-change surface differs in exactly four knobs: DuckDB enables the
1042        // live-body macro DDL (`create_macro`), `CREATE OR REPLACE TABLE`
1043        // (`create_or_replace_table`), the `CREATE [PERSISTENT] SECRET` statement
1044        // (`create_secret`), and the `CREATE`/`DROP TYPE` user-defined-type DDL
1045        // (`create_type`), all of which PostgreSQL lacks. Every other field is inherited
1046        // verbatim (forcing the four off recovers PG).
1047        assert!(duck.statement_ddl_gates.create_macro);
1048        assert!(duck.create_table_clause_syntax.create_or_replace_table);
1049        assert!(duck.statement_ddl_gates.create_secret);
1050        assert!(duck.statement_ddl_gates.create_type);
1051        assert!(!pg.statement_ddl_gates.create_macro);
1052        assert!(!pg.create_table_clause_syntax.create_or_replace_table);
1053        assert!(!pg.statement_ddl_gates.create_secret);
1054        assert!(!pg.statement_ddl_gates.create_type);
1055        // DuckDB matches the PostgreSQL schema-change surface except for the four
1056        // DuckDB-specific create forms above, the PostgreSQL-only `b_expr` column-default
1057        // restriction (DuckDB reads a full `a_expr` default), PostgreSQL-only declarative
1058        // partitioning, the two PostgreSQL-only legacy CREATE TABLE clauses (`INHERITS` and the
1059        // `(LIKE …)` element), and the four PostgreSQL-only residue clauses (column
1060        // STORAGE/COMPRESSION, the USING access method, WITHOUT OIDS, typed `OF <type>` tables);
1061        // DuckDB *does* share the column `COLLATE` and `UNLOGGED` surfaces. Forcing all twelve
1062        // divergent flags to PostgreSQL's values makes the rest equal.
1063        assert!(!duck.column_definition_syntax.column_default_requires_b_expr);
1064        assert!(pg.column_definition_syntax.column_default_requires_b_expr);
1065        assert!(!duck.create_table_clause_syntax.declarative_partitioning);
1066        assert!(pg.create_table_clause_syntax.declarative_partitioning);
1067        assert!(!duck.create_table_clause_syntax.table_inheritance);
1068        assert!(pg.create_table_clause_syntax.table_inheritance);
1069        assert!(!duck.create_table_clause_syntax.like_source_table);
1070        assert!(pg.create_table_clause_syntax.like_source_table);
1071        assert!(duck.column_definition_syntax.column_collation);
1072        assert!(duck.create_table_clause_syntax.unlogged_tables);
1073        assert!(!duck.column_definition_syntax.column_storage);
1074        assert!(pg.column_definition_syntax.column_storage);
1075        assert!(!duck.create_table_clause_syntax.table_access_method);
1076        assert!(pg.create_table_clause_syntax.table_access_method);
1077        assert!(!duck.create_table_clause_syntax.without_oids);
1078        assert!(pg.create_table_clause_syntax.without_oids);
1079        assert!(!duck.create_table_clause_syntax.typed_tables);
1080        assert!(pg.create_table_clause_syntax.typed_tables);
1081        // DuckDB also lacks PostgreSQL's SQL-standard embedded schema-element form
1082        // (`schema_elements`): DuckDB's `CREATE SCHEMA` takes no inline `CREATE TABLE`/…
1083        // children, so recovering PG forces that flag back on alongside the three
1084        // DuckDB-specific create forms.
1085        assert!(!duck.statement_ddl_gates.schema_elements);
1086        assert!(pg.statement_ddl_gates.schema_elements);
1087        assert!(!duck.statement_ddl_gates.databases);
1088        assert!(pg.statement_ddl_gates.databases);
1089        // DuckDB manages extensions with `INSTALL`/`LOAD`, not the PostgreSQL
1090        // `CREATE`/`ALTER EXTENSION` catalogue DDL, so recovering PG forces this on.
1091        assert!(!duck.statement_ddl_gates.extension_ddl);
1092        assert!(pg.statement_ddl_gates.extension_ddl);
1093        // DuckDB has no transform catalogue (`CREATE`/`DROP TRANSFORM` is PostgreSQL-only),
1094        // so recovering PG forces this on.
1095        assert!(!duck.statement_ddl_gates.transform_ddl);
1096        assert!(pg.statement_ddl_gates.transform_ddl);
1097        // DuckDB configures through `SET`/`RESET`, not `ALTER SYSTEM`, so recovering PG
1098        // forces this on.
1099        assert!(!duck.statement_ddl_gates.alter_system);
1100        assert!(pg.statement_ddl_gates.alter_system);
1101        assert_eq!(
1102            StatementDdlGates {
1103                create_macro: false,
1104                create_secret: false,
1105                create_type: false,
1106                schema_elements: true,
1107                // DuckDB adds `CREATE RECURSIVE VIEW`; PostgreSQL is gated off here.
1108                recursive_views: false,
1109                // DuckDB has no `CREATE DATABASE` (uses ATTACH); PostgreSQL admits it.
1110                databases: true,
1111                // DuckDB has no PostgreSQL-style extension catalogue DDL.
1112                extension_ddl: true,
1113                // DuckDB has no PostgreSQL transform catalogue (`DROP TRANSFORM`).
1114                transform_ddl: true,
1115                // DuckDB has no `ALTER SYSTEM` server-configuration DDL.
1116                alter_system: true,
1117                // MySQL's tablespace / logfile-group storage DDL is not a DuckDB statement.
1118                tablespace_ddl: false,
1119                logfile_group_ddl: false,
1120                // DuckDB adds `ALTER DATABASE … SET ALIAS TO`, the `ALTER SEQUENCE …` option
1121                // list, and `ALTER … SET SCHEMA`; PostgreSQL is gated off here (no-shadowing).
1122                alter_database: false,
1123                alter_sequence: false,
1124                alter_object_set_schema: false,
1125                ..duck.statement_ddl_gates
1126            },
1127            pg.statement_ddl_gates,
1128        );
1129        assert_eq!(
1130            CreateTableClauseSyntax {
1131                create_or_replace_table: false,
1132                declarative_partitioning: true,
1133                table_inheritance: true,
1134                like_source_table: true,
1135                table_access_method: true,
1136                without_oids: true,
1137                typed_tables: true,
1138                create_table_as_execute: true,
1139                ..duck.create_table_clause_syntax
1140            },
1141            pg.create_table_clause_syntax,
1142        );
1143        assert_eq!(
1144            ColumnDefinitionSyntax {
1145                column_default_requires_b_expr: true,
1146                column_storage: true,
1147                // DuckDB turns the keywordless generated-column shorthand and the
1148                // type-optional generated column on (both off in PostgreSQL).
1149                generated_column_shorthand: false,
1150                typeless_generated_columns: false,
1151                ..duck.column_definition_syntax
1152            },
1153            pg.column_definition_syntax,
1154        );
1155        assert!(!duck.constraint_syntax.referential_action_cascade_set);
1156        assert!(pg.constraint_syntax.referential_action_cascade_set);
1157        assert!(!duck.constraint_syntax.check_constraint_subqueries);
1158        assert!(pg.constraint_syntax.check_constraint_subqueries);
1159        assert_eq!(
1160            ConstraintSyntax {
1161                exclusion_constraints: true,
1162                index_constraint_parameters: true,
1163                referential_action_cascade_set: true,
1164                check_constraint_subqueries: true,
1165                ..duck.constraint_syntax
1166            },
1167            pg.constraint_syntax,
1168        );
1169        assert!(!duck.index_alter_syntax.alter_table_multiple_actions);
1170        assert!(pg.index_alter_syntax.alter_table_multiple_actions);
1171        assert!(duck.index_alter_syntax.alter_nested_column_paths);
1172        assert!(!pg.index_alter_syntax.alter_nested_column_paths);
1173        assert_eq!(
1174            IndexAlterSyntax {
1175                alter_table_multiple_actions: true,
1176                alter_nested_column_paths: false,
1177                ..duck.index_alter_syntax
1178            },
1179            pg.index_alter_syntax,
1180        );
1181        assert_eq!(duck.target_spelling, pg.target_spelling);
1182        assert_eq!(duck.identifier_casing, pg.identifier_casing);
1183    }
1184
1185    #[test]
1186    fn duckdb_reranks_the_arrow_and_double_equals_tokens() {
1187        use crate::precedence::Side;
1188
1189        // The lambda arrow binds below `OR` (the loosest binary operator), while
1190        // `->>`, containment, and everything else keep PostgreSQL's ranks — the
1191        // engine-measured split (`x -> x OR y` puts the OR in the body; `a ->> 'k' =
1192        // 5` compares the extraction).
1193        let duck = DUCKDB_BINDING_POWERS;
1194        let arrow = duck.binary(&BinaryOperator::JsonGet);
1195        assert!(arrow.left < duck.or.left, "`->` is looser than OR");
1196        assert_eq!(arrow.assoc, Assoc::Left, "`x -> y -> z` groups left");
1197        // `==` is the second reranked token: DuckDB lexes it as a generic `%left Op`, not
1198        // the `%nonassoc '='` comparison, so it sits at the `any_operator` rank (tighter
1199        // than the comparisons, looser than additive, left-associative). Its `=` sibling
1200        // (`Eq(Single)`) and every other operator keep PostgreSQL's ranks.
1201        let double_eq = duck.binary(&BinaryOperator::Eq(EqualsSpelling::Double));
1202        assert_eq!(
1203            double_eq, duck.any_operator,
1204            "`==` rides the generic-Op rank"
1205        );
1206        assert_eq!(double_eq.assoc, Assoc::Left, "`a == b == c` groups left");
1207        assert!(
1208            duck.comparison.left < double_eq.left,
1209            "`==` binds tighter than the comparisons"
1210        );
1211        assert!(
1212            double_eq.left < duck.additive.left,
1213            "`==` binds looser than additive"
1214        );
1215        for untouched in [
1216            BinaryOperator::Eq(EqualsSpelling::Single),
1217            BinaryOperator::JsonGetText,
1218            BinaryOperator::Contains,
1219            BinaryOperator::ContainedBy,
1220        ] {
1221            assert_eq!(
1222                duck.binary(&untouched),
1223                STANDARD_BINDING_POWERS.binary(&untouched),
1224            );
1225        }
1226        // The grouping consequence, at the table level: an `=` right operand of `->`
1227        // needs no parentheses (it binds tighter), so `x -> x % 2 = 0` keeps the
1228        // whole comparison in the body — where PostgreSQL's table demands the split.
1229        assert!(!crate::precedence::needs_parens_between(
1230            arrow,
1231            duck.comparison,
1232            Side::Right,
1233        ));
1234    }
1235
1236    #[test]
1237    fn duckdb_reserves_qualify_in_every_identifier_position() {
1238        // DuckDB's `duckdb_keywords()` classes QUALIFY `reserved` (like HAVING): every
1239        // per-position reject set names it, each strictly widening its shared base —
1240        // and the shared base itself must NOT contain it, or the "only DuckDB reserves
1241        // it" story (and PostgreSQL/ANSI identifier behaviour) silently breaks.
1242        for (duck_set, shared) in [
1243            (DUCKDB_RESERVED_COLUMN_NAME, RESERVED_COLUMN_NAME),
1244            (DUCKDB_RESERVED_FUNCTION_NAME, RESERVED_FUNCTION_NAME),
1245            (DUCKDB_RESERVED_TYPE_NAME, RESERVED_TYPE_NAME),
1246            (DUCKDB_RESERVED_BARE_ALIAS, RESERVED_BARE_ALIAS),
1247        ] {
1248            assert!(duck_set.contains(Keyword::Qualify));
1249            assert!(!shared.contains(Keyword::Qualify));
1250        }
1251        // The type set carries exactly the QUALIFY and PIVOT/UNPIVOT deltas (all
1252        // `reserved` class) and nothing else — the join words stay out (`CAST(1 AS
1253        // asof)`/`CAST(1 AS semi)` both parse). The function set carries those *plus*
1254        // SEMI/ANTI, whose grammar rejects `semi(1)` even though `duckdb_keywords()`
1255        // classes them `type_function` (the `ASOF`/`POSITIONAL` join words stay out —
1256        // `asof(1)` parses).
1257        assert_eq!(
1258            DUCKDB_RESERVED_FUNCTION_NAME,
1259            RESERVED_FUNCTION_NAME
1260                .union(DUCKDB_QUALIFY_RESERVATION)
1261                .union(DUCKDB_PIVOT_RESERVATION)
1262                .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION)
1263        );
1264        assert_eq!(
1265            DUCKDB_RESERVED_TYPE_NAME,
1266            RESERVED_TYPE_NAME
1267                .union(DUCKDB_QUALIFY_RESERVATION)
1268                .union(DUCKDB_PIVOT_RESERVATION)
1269        );
1270    }
1271
1272    #[test]
1273    fn duckdb_reserves_pivot_and_unpivot_in_every_identifier_position() {
1274        // DuckDB's `duckdb_keywords()` classes PIVOT/UNPIVOT `reserved` (like QUALIFY):
1275        // every per-position reject set names them (probed on 1.5.4), each strictly
1276        // widening its shared base — and the shared base must NOT contain them, or the
1277        // "only DuckDB reserves them" story (and PostgreSQL/ANSI identifier behaviour)
1278        // silently breaks.
1279        for kw in [Keyword::Pivot, Keyword::Unpivot] {
1280            for (duck_set, shared) in [
1281                (DUCKDB_RESERVED_COLUMN_NAME, RESERVED_COLUMN_NAME),
1282                (DUCKDB_RESERVED_FUNCTION_NAME, RESERVED_FUNCTION_NAME),
1283                (DUCKDB_RESERVED_TYPE_NAME, RESERVED_TYPE_NAME),
1284                (DUCKDB_RESERVED_BARE_ALIAS, RESERVED_BARE_ALIAS),
1285            ] {
1286                assert!(duck_set.contains(kw));
1287                assert!(!shared.contains(kw));
1288            }
1289        }
1290    }
1291
1292    #[test]
1293    fn duckdb_reserves_the_join_words_as_colid_and_bare_alias_only() {
1294        // `asof`/`positional` are `duckdb_keywords()` class `type_function` (like
1295        // `CROSS`): rejected as a column/table name and bare alias, admitted as a
1296        // function name, type name, and `AS` label. The set composition mirrors that
1297        // probed profile exactly, and the shared bases must stay free of both words
1298        // (every other dialect keeps them plain identifiers).
1299        for kw in [Keyword::Asof, Keyword::Positional] {
1300            assert!(DUCKDB_RESERVED_COLUMN_NAME.contains(kw));
1301            assert!(DUCKDB_RESERVED_BARE_ALIAS.contains(kw));
1302            assert!(!DUCKDB_RESERVED_FUNCTION_NAME.contains(kw));
1303            assert!(!DUCKDB_RESERVED_TYPE_NAME.contains(kw));
1304            for shared in [
1305                RESERVED_COLUMN_NAME,
1306                RESERVED_FUNCTION_NAME,
1307                RESERVED_TYPE_NAME,
1308                RESERVED_BARE_ALIAS,
1309            ] {
1310                assert!(!shared.contains(kw));
1311            }
1312        }
1313        assert_eq!(
1314            DUCKDB_RESERVED_COLUMN_NAME,
1315            RESERVED_COLUMN_NAME
1316                .union(DUCKDB_QUALIFY_RESERVATION)
1317                .union(DUCKDB_PIVOT_RESERVATION)
1318                .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
1319                .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION)
1320        );
1321        assert_eq!(
1322            DUCKDB_RESERVED_BARE_ALIAS,
1323            RESERVED_BARE_ALIAS
1324                .union(DUCKDB_QUALIFY_RESERVATION)
1325                .union(DUCKDB_PIVOT_RESERVATION)
1326                .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
1327                .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION)
1328        );
1329    }
1330
1331    #[test]
1332    fn duckdb_reserves_semi_and_anti_as_colid_bare_alias_and_function() {
1333        // `semi`/`anti` are `duckdb_keywords()` class `type_function` like the join
1334        // words, but the DuckDB grammar reserves them one position further: rejected as
1335        // a column/table name, a bare alias, *and a function name* (`semi(1)`
1336        // syntax-errors where `asof(1)` parses), while the type position and `AS` labels
1337        // still admit them (`CAST(1 AS semi)`/`SELECT 1 AS semi` parse). The set
1338        // composition mirrors that probed profile exactly, and the shared bases stay
1339        // free of both words.
1340        for kw in [Keyword::Semi, Keyword::Anti] {
1341            assert!(DUCKDB_RESERVED_COLUMN_NAME.contains(kw));
1342            assert!(DUCKDB_RESERVED_BARE_ALIAS.contains(kw));
1343            assert!(DUCKDB_RESERVED_FUNCTION_NAME.contains(kw));
1344            assert!(!DUCKDB_RESERVED_TYPE_NAME.contains(kw));
1345            for shared in [
1346                RESERVED_COLUMN_NAME,
1347                RESERVED_FUNCTION_NAME,
1348                RESERVED_TYPE_NAME,
1349                RESERVED_BARE_ALIAS,
1350            ] {
1351                assert!(!shared.contains(kw));
1352            }
1353        }
1354    }
1355
1356    #[test]
1357    fn duckdb_expression_deltas_are_additive_over_postgres() {
1358        // The delta is exactly the collection-literal and `#n` positional-column flags
1359        // (ExpressionSyntax), the lambda flag (OperatorSyntax), and the COLUMNS(…) flag
1360        // (CallSyntax); the whole PostgreSQL surface (subscript, ARRAY[…], `::`, …) is kept.
1361        // The lambda gate additionally depends on the inherited JSON-arrow lexing (`->`
1362        // must tokenize for the lambda grammar position to ever fire), so pin that
1363        // inheritance here too. Bind to locals so the const field reads are not flagged by
1364        // clippy's `assertions_on_constants`.
1365        let (duck_expr, pg_expr) = (ExpressionSyntax::DUCKDB, ExpressionSyntax::POSTGRES);
1366        let (duck_op, pg_op) = (OperatorSyntax::DUCKDB, OperatorSyntax::POSTGRES);
1367        let (duck_call, pg_call) = (CallSyntax::DUCKDB, CallSyntax::POSTGRES);
1368        let (duck_sf, pg_sf) = (StringFuncForms::DUCKDB, StringFuncForms::POSTGRES);
1369        let (duck_ag, pg_ag) = (AggregateCallSyntax::DUCKDB, AggregateCallSyntax::POSTGRES);
1370        assert!(duck_expr.collection_literals);
1371        assert!(!pg_expr.collection_literals);
1372        assert!(duck_expr.positional_column);
1373        assert!(!pg_expr.positional_column);
1374        assert!(duck_expr.lambda_keyword);
1375        assert!(!pg_expr.lambda_keyword);
1376        assert!(duck_expr.relaxed_interval_syntax);
1377        assert!(!pg_expr.relaxed_interval_syntax);
1378        assert!(duck_op.lambda_expressions);
1379        assert!(!pg_op.lambda_expressions);
1380        assert!(duck_op.double_equals);
1381        assert!(!pg_op.double_equals);
1382        assert!(duck_op.integer_divide_slash);
1383        assert!(!pg_op.integer_divide_slash);
1384        assert!(duck_call.columns_expression);
1385        assert!(!pg_call.columns_expression);
1386        assert!(duck_call.try_cast);
1387        assert!(!pg_call.try_cast);
1388        assert!(
1389            duck_op.json_arrow_operators,
1390            "lambda `->` rides the JSON-arrow lexeme"
1391        );
1392        // `field_wildcard` is a *subtractive* delta: DuckDB parses `(struct).field`
1393        // (field_selection, inherited) but has no `.*` value-expansion production, so it
1394        // vacates PostgreSQL's `true` (engine-probed 1.5.4).
1395        assert!(pg_expr.field_wildcard);
1396        assert!(!duck_expr.field_wildcard);
1397        // `multidim_array_literals` is a *subtractive* delta too: DuckDB nests `ARRAY[[1,2]]`
1398        // through `collection_literals` (a top-level `[…]` list is a value there and levels
1399        // may mix), so it vacates PostgreSQL's `true` for the multidim `array_expr` production.
1400        assert!(pg_expr.multidim_array_literals);
1401        assert!(!duck_expr.multidim_array_literals);
1402        assert_eq!(
1403            duck_expr,
1404            ExpressionSyntax {
1405                collection_literals: true,
1406                slice_step: true,
1407                positional_column: true,
1408                lambda_keyword: true,
1409                relaxed_interval_syntax: true,
1410                field_wildcard: false,
1411                multidim_array_literals: false,
1412                ..pg_expr
1413            },
1414        );
1415        // The PostgreSQL `jsonb` operators are a *subtractive* delta: DuckDB spells `?` as the
1416        // anonymous placeholder, which claims the same trigger as the `jsonb` `?` operators
1417        // (`LexicalConflict::JsonbKeyExistsVersusAnonymousParameter`), so DuckDB vacates the
1418        // whole family to stay lexically consistent — unlike the additive deltas above.
1419        assert!(pg_op.jsonb_operators);
1420        assert!(!duck_op.jsonb_operators);
1421        // `caret_operator` (top-level FeatureSet dimension) is SHARED with PostgreSQL (probed
1422        // identical on DuckDB 1.5.4 — see the preset comment): DuckDB's `^` is power at the
1423        // same precedence row, so both presets read `CaretOperator::Exponent`.
1424        // `custom_operators` is SHARED with PostgreSQL: DuckDB inherits its generalized
1425        // maximal-munch operator lexer and parse-accepts the same `Op`-class runs (bind-rejecting
1426        // the ones with no backing function). The one lexical divergence — DuckDB drops `#`/`?`
1427        // from the `Op` charset (positional-column and parameter sigils) — is carried by the
1428        // shared `is_operator_char` gate, not a separate flag. See the preset comment and
1429        // `duckdb-pg-operator-spelling-under-acceptance` for the probe.
1430        assert!(pg_op.custom_operators);
1431        assert!(duck_op.custom_operators);
1432        assert_eq!(FeatureSet::DUCKDB.caret_operator, CaretOperator::Exponent);
1433        assert_eq!(
1434            FeatureSet::DUCKDB.caret_operator,
1435            FeatureSet::POSTGRES.caret_operator
1436        );
1437        // The any-operator quantifier (`3 * ANY(list)`) is a *subtractive* delta: it is a
1438        // PostgreSQL extension, not a DuckDB construct, so DuckDB vacates PostgreSQL's `true`.
1439        assert!(pg_op.quantified_arbitrary_operator);
1440        assert!(!duck_op.quantified_arbitrary_operator);
1441        // Postfix symbolic operators (`10!`) are an *additive* delta: DuckDB keeps the postfix
1442        // reading PostgreSQL removed in 14, so it arms PostgreSQL's `false`
1443        // (`duckdb-postfix-operator-dimension`).
1444        assert!(!pg_op.postfix_operators);
1445        assert!(duck_op.postfix_operators);
1446        assert_eq!(
1447            duck_op,
1448            OperatorSyntax {
1449                lambda_expressions: true,
1450                double_equals: true,
1451                integer_divide_slash: true,
1452                starts_with_operator: true,
1453                jsonb_operators: false,
1454                quantified_arbitrary_operator: false,
1455                postfix_operators: true,
1456                ..pg_op
1457            },
1458        );
1459        // The PG-only SQL/JSON empty-constructor reject is a subtractive delta: DuckDB
1460        // deliberately keeps `json()` a plain call (unprobed surface, documented at the
1461        // preset).
1462        assert!(pg_call.sqljson_constructors_require_argument);
1463        assert!(!duck_call.sqljson_constructors_require_argument);
1464        // The SQL/JSON expression functions are a subtractive delta too: DuckDB has no
1465        // SQL:2016 special forms (only ordinary JSON functions), so it vacates PostgreSQL's
1466        // `true` to keep the keyword heads plain call/name forms.
1467        assert!(pg_call.sqljson_expression_functions);
1468        assert!(!duck_call.sqljson_expression_functions);
1469        // The SQL/XML expression functions are a subtractive delta too: DuckDB has no
1470        // SQL/XML special forms, so it vacates PostgreSQL's `true` here as well.
1471        assert!(pg_call.xml_expression_functions);
1472        assert!(!duck_call.xml_expression_functions);
1473        // The string special forms diverge in exactly two probed knobs: the SIMILAR
1474        // regex substring is dropped from DuckDB's PG-fork grammar, and OVERLAY kept
1475        // only the PLACING production (no plain-call fallback).
1476        assert!(pg_sf.substring_similar);
1477        assert!(!duck_sf.substring_similar);
1478        assert!(!pg_sf.overlay_requires_placing);
1479        assert!(duck_sf.overlay_requires_placing);
1480        assert_eq!(
1481            duck_call,
1482            CallSyntax {
1483                columns_expression: true,
1484                try_cast: true,
1485                extract_string_field: true,
1486                method_chaining: true,
1487                sqljson_constructors_require_argument: false,
1488                sqljson_expression_functions: false,
1489                xml_expression_functions: false,
1490                merge_action_function: false,
1491                ..pg_call
1492            },
1493        );
1494        assert_eq!(
1495            duck_ag,
1496            AggregateCallSyntax {
1497                null_treatment: true,
1498                standalone_argument_order_by: true,
1499                filter_optional_where: true,
1500                ..pg_ag
1501            },
1502        );
1503        assert_eq!(
1504            duck_sf,
1505            StringFuncForms {
1506                substring_similar: false,
1507                overlay_requires_placing: true,
1508                collation_for_expression: false,
1509                ..pg_sf
1510            },
1511        );
1512    }
1513
1514    #[test]
1515    fn duckdb_numeric_surface_relaxes_postgres_trailing_junk_reject() {
1516        // DuckDB and PostgreSQL now share the *same* radix/separator surface (both model
1517        // PG 14+ `0x`/`0o`/`0b` and `_` grouping) — the delta is the strictness knob:
1518        // PostgreSQL rejects trailing junk after a number, DuckDB (probed) lexes it
1519        // loosely and accepts. Bound to locals so the field reads are runtime asserts.
1520        let (duck, pg) = (NumericLiteralSyntax::DUCKDB, NumericLiteralSyntax::POSTGRES);
1521        assert!(duck.hex_integers && duck.octal_integers && duck.binary_integers);
1522        assert!(duck.underscore_separators);
1523        assert!(!duck.money_literals);
1524        // The radix/separator forms are identical; only the reject differs.
1525        assert_eq!(duck.hex_integers, pg.hex_integers);
1526        assert_eq!(duck.octal_integers, pg.octal_integers);
1527        assert_eq!(duck.binary_integers, pg.binary_integers);
1528        assert_eq!(duck.underscore_separators, pg.underscore_separators);
1529        assert!(pg.reject_trailing_junk && !duck.reject_trailing_junk);
1530    }
1531
1532    #[test]
1533    fn duckdb_select_surface_is_postgres_modulo_the_documented_deltas() {
1534        // Subtractive deltas (`empty_target_list`, and the `locking_clauses` family —
1535        // `locking_clauses` / `key_lock_strengths` / `stacked_locking_clauses` — since
1536        // DuckDB has no row locking) and additive clauses (`qualify`, the `GROUP BY ALL` /
1537        // `ORDER BY ALL` modes, `UNION [ALL] BY NAME`, and the FROM-first SELECT order);
1538        // the rest of the SELECT surface is PostgreSQL's (DISTINCT ON, FETCH FIRST,
1539        // SELECT INTO, …).
1540        let (duck, pg) = (SelectSyntax::DUCKDB, SelectSyntax::POSTGRES);
1541        let (duck_g, pg_g) = (GroupingSyntax::DUCKDB, GroupingSyntax::POSTGRES);
1542        let (duck_q, pg_q) = (QueryTailSyntax::DUCKDB, QueryTailSyntax::POSTGRES);
1543        assert!(!duck.empty_target_list);
1544        assert!(pg.empty_target_list);
1545        assert!(duck.qualify);
1546        assert!(!pg.qualify);
1547        assert!(duck_g.group_by_all && duck_g.order_by_all);
1548        assert!(!pg_g.group_by_all && !pg_g.order_by_all);
1549        assert!(duck.from_first);
1550        assert!(!pg.from_first);
1551        assert!(!duck_q.locking_clauses);
1552        assert!(pg_q.locking_clauses);
1553        assert!(!duck_q.key_lock_strengths && !duck_q.stacked_locking_clauses);
1554        assert!(pg_q.key_lock_strengths && pg_q.stacked_locking_clauses);
1555        assert!(duck.union_by_name);
1556        assert!(!pg.union_by_name);
1557        assert!(duck.wildcard_modifiers);
1558        assert!(!pg.wildcard_modifiers);
1559        assert!(duck.values_rows_require_equal_arity);
1560        assert!(!pg.values_rows_require_equal_arity);
1561        assert!(duck_q.limit_percent);
1562        assert!(!pg_q.limit_percent);
1563        assert!(duck.alias_string_literals);
1564        assert!(!pg.alias_string_literals);
1565        assert_eq!(
1566            duck,
1567            SelectSyntax {
1568                empty_target_list: false,
1569                qualify: true,
1570                from_first: true,
1571                union_by_name: true,
1572                wildcard_modifiers: true,
1573                values_rows_require_equal_arity: true,
1574                alias_string_literals: true,
1575                bare_alias_string_literals: false,
1576                trailing_comma: true,
1577                // DuckDB's prefix colon alias — additive over the PostgreSQL base.
1578                prefix_colon_alias: true,
1579                ..pg
1580            },
1581        );
1582        assert_eq!(
1583            duck_g,
1584            GroupingSyntax {
1585                group_by_all: true,
1586                // PostgreSQL admits the `GROUP BY {DISTINCT | ALL} <items>` grouping-set
1587                // quantifier; DuckDB does not (its `ALL` is the standalone mode above) —
1588                // a subtractive delta.
1589                group_by_set_quantifier: false,
1590                order_by_all: true,
1591                ..pg_g
1592            },
1593        );
1594        assert_eq!(
1595            duck_q,
1596            QueryTailSyntax {
1597                locking_clauses: false,
1598                key_lock_strengths: false,
1599                stacked_locking_clauses: false,
1600                using_sample: true,
1601                limit_percent: true,
1602                // PG-only raw-parse `WITH TIES` guards — a subtractive delta (DuckDB's own
1603                // `WITH TIES` validity is unprobed, documented at the preset).
1604                with_ties_requires_order_by: false,
1605                ..pg_q
1606            },
1607        );
1608    }
1609}