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, but every field is enumerated here.
7//! Values shared with PostgreSQL are repeated deliberately so either preset changing forces
8//! an explicit review of the other.
9//!
10//! Comments focus on syntax and oracle evidence: parse-time tightenings, keyword reservation
11//! needed to disambiguate grammar, and measured precedence differences.
12
13use super::{
14    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
15    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
16    DUCKDB_BYTE_CLASSES, DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet,
17    GroupingSyntax, IdentifierSyntax, IndexAlterSyntax, JoinSyntax, Keyword, KeywordOperators,
18    KeywordSet, MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax,
19    OperatorSyntax, ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax,
20    RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_SET_VALUE_WORDS,
21    RESERVED_TYPE_NAME, STANDARD_IDENTIFIER_QUOTES, SelectSyntax, SessionVariableSyntax,
22    ShowSyntax, StatementDdlGates, StringFuncForms, StringLiteralSyntax, TableExpressionSyntax,
23    TableFactorSyntax, TargetSpelling, TransactionSyntax, TypeNameSyntax, UtilitySyntax,
24    ViewSequenceClauseSyntax,
25};
26use crate::precedence::{
27    Assoc, BindingPower, BindingPowerTable, IS_PREDICATE_BELOW_COMPARISON,
28    STANDARD_SET_OPERATION_BINDING_POWERS,
29};
30
31/// `QUALIFY` (`duckdb_keywords()` class `reserved`, DuckDB 1.5.4), the fully-reserved
32/// half of DuckDB's reservation delta over the shared PostgreSQL-derived model.
33/// Unioned into all four per-position reject sets below because DuckDB's `reserved`
34/// class rejects the word as a column/table name, function name, type name, *and*
35/// bare alias (probed: `SELECT qualify FROM t`, `SELECT * FROM qualify`,
36/// `SELECT qualify(1)`, `CAST(1 AS qualify)`, `SELECT 1 qualify`, and
37/// `FROM t qualify` all syntax-error; `SELECT 1 AS qualify` labels). The same
38/// hand-composition pattern the SQLite/MySQL presets use for their reservation deltas.
39pub const DUCKDB_QUALIFY_RESERVATION: KeywordSet = KeywordSet::from_keywords(&[Keyword::Qualify]);
40
41/// `PIVOT` and `UNPIVOT` (`duckdb_keywords()` class `reserved`, DuckDB 1.5.4), the
42/// row/column rotation operators. Like `QUALIFY`'s `reserved` class (and unlike the
43/// `type_function` join words), both are rejected in all four identifier positions —
44/// column/table name, function name, type name, and bare alias — while `AS pivot` still
45/// labels (probed: `SELECT pivot FROM t`, `SELECT * FROM pivot`, `SELECT pivot(1)`,
46/// `CAST(1 AS pivot)`, `SELECT 1 pivot` all syntax-error; `SELECT 1 AS pivot` parses —
47/// identically for `unpivot`). The bare-alias reservation is load-bearing for the
48/// grammar: it is what lets `FROM t PIVOT (…)` read the operator instead of a table
49/// alias named `pivot`. Unioned into all four reject sets below, exactly like
50/// [`DUCKDB_QUALIFY_RESERVATION`].
51pub const DUCKDB_PIVOT_RESERVATION: KeywordSet =
52    KeywordSet::from_keywords(&[Keyword::Pivot, Keyword::Unpivot]);
53
54/// The nonstandard-join keywords, `ASOF` and `POSITIONAL` (`duckdb_keywords()` class
55/// `type_function`, like `CROSS`, DuckDB 1.5.4). Unlike `QUALIFY`'s `reserved` class,
56/// this profile rejects the words only as a column/table name (`ColId`) and as a bare
57/// alias, while function/type positions and `AS` labels still admit them (probed:
58/// `SELECT asof FROM t`, `CREATE TABLE asof(…)`, `FROM t asof`, and `SELECT 1 asof`
59/// all syntax-error; `SELECT asof(1)`, `CAST(1 AS asof)`, and `SELECT 1 AS asof`
60/// parse — identically for `positional`). The `ColId` reservation is load-bearing for
61/// the grammar: it is what lets `FROM l ASOF JOIN r …` read the join instead of a
62/// table alias named `asof`.
63pub const DUCKDB_NONSTANDARD_JOIN_RESERVATION: KeywordSet =
64    KeywordSet::from_keywords(&[Keyword::Asof, Keyword::Positional]);
65
66/// The semi-/anti-join keywords, `SEMI` and `ANTI` (`duckdb_keywords()` class
67/// `type_function`, DuckDB 1.5.4). Their `type_function` category matches
68/// `ASOF`/`POSITIONAL`, but the DuckDB grammar reserves them one position *further*:
69/// they reject as a column/table name (`ColId`), a bare alias, *and a function name*,
70/// while only the type position and `AS` labels admit them (probed: `SELECT semi FROM
71/// t`, `CREATE TABLE semi(…)`, `FROM t semi`, `SELECT 1 semi`, and — unlike `asof(1)` —
72/// `SELECT semi(1)` all syntax-error; `CAST(1 AS semi)` and `SELECT 1 AS semi` parse,
73/// identically for `anti`). So this set unions into the `ColId`, function-name, and
74/// bare-alias rejects but *not* the type-name one. The `ColId`/bare-alias reservation
75/// is load-bearing for the grammar: it is what lets `FROM l SEMI JOIN r …` read the
76/// join instead of a table alias named `semi`.
77pub const DUCKDB_SEMI_ANTI_JOIN_RESERVATION: KeywordSet =
78    KeywordSet::from_keywords(&[Keyword::Semi, Keyword::Anti]);
79
80/// PostgreSQL-reserved words that DuckDB 1.5.4 classifies as `unreserved`.
81/// Both words are valid as a column/table name, function name, type name, and
82/// generic `SET` value. They remain rejected as bare projection aliases.
83pub const DUCKDB_UNRESERVED_CARVEOUT: KeywordSet =
84    KeywordSet::from_keywords(&[Keyword::Grant, Keyword::User]);
85
86/// PostgreSQL special-value keywords that are ordinary identifiers in DuckDB's
87/// keyword inventory. They remain available as built-in names, including call
88/// spellings, but carry no identifier-position reservation.
89pub const DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES: KeywordSet = KeywordSet::from_keywords(&[
90    Keyword::CurrentCatalog,
91    Keyword::CurrentDate,
92    Keyword::CurrentRole,
93    Keyword::CurrentSchema,
94    Keyword::CurrentTime,
95    Keyword::CurrentTimestamp,
96    Keyword::CurrentUser,
97    Keyword::Localtime,
98    Keyword::Localtimestamp,
99    Keyword::SessionUser,
100    Keyword::SystemUser,
101]);
102
103/// DuckDB's unreserved `GRANT` and `USER` words are not valid bare projection
104/// aliases, although they are valid identifiers in the other positions above.
105pub const DUCKDB_UNRESERVED_BARE_ALIAS_RESERVATION: KeywordSet =
106    KeywordSet::from_keywords(&[Keyword::Grant, Keyword::User]);
107
108/// DuckDB `ColId` reject set: the shared model plus `QUALIFY`, the `PIVOT`/`UNPIVOT`
109/// operators, and the nonstandard-join / semi-anti-join keywords.
110pub const DUCKDB_RESERVED_COLUMN_NAME: KeywordSet = RESERVED_COLUMN_NAME
111    .difference(DUCKDB_UNRESERVED_CARVEOUT)
112    .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
113    .union(DUCKDB_QUALIFY_RESERVATION)
114    .union(DUCKDB_PIVOT_RESERVATION)
115    .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
116    .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION);
117
118/// DuckDB function-name reject set: the shared model plus `QUALIFY` (DuckDB reads
119/// `SELECT qualify(1)` as an empty projection followed by the QUALIFY clause, never a
120/// call), `PIVOT`/`UNPIVOT` (their `reserved` class likewise rejects `pivot(1)`), and
121/// `SEMI`/`ANTI` (DuckDB's grammar rejects `semi(1)` despite the `type_function` class).
122/// The `ASOF`/`POSITIONAL` words are *not* here: their `type_function` class admits
123/// `asof(1)` / `positional(1)` as calls, matching the engine.
124pub const DUCKDB_RESERVED_FUNCTION_NAME: KeywordSet = RESERVED_FUNCTION_NAME
125    .difference(DUCKDB_UNRESERVED_CARVEOUT)
126    .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
127    .union(DUCKDB_QUALIFY_RESERVATION)
128    .union(DUCKDB_PIVOT_RESERVATION)
129    .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION);
130
131/// DuckDB type-name reject set: the shared model plus `QUALIFY` and `PIVOT`/`UNPIVOT`
132/// (their `reserved` class rejects `CAST(1 AS pivot)`). The nonstandard-join and
133/// semi-anti-join words are *not* here: `CAST(1 AS asof)` and `CAST(1 AS semi)` parse in
134/// the engine (the type position admits any non-`reserved` word).
135pub const DUCKDB_RESERVED_TYPE_NAME: KeywordSet = RESERVED_TYPE_NAME
136    .difference(DUCKDB_UNRESERVED_CARVEOUT)
137    .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
138    .union(DUCKDB_QUALIFY_RESERVATION)
139    .union(DUCKDB_PIVOT_RESERVATION);
140
141/// Fully reserved words rejected in a DuckDB generic `SET` value. The PostgreSQL-derived
142/// base is extended by DuckDB's own `reserved` keyword additions.
143pub const DUCKDB_RESERVED_SET_VALUE_WORDS: KeywordSet = RESERVED_SET_VALUE_WORDS
144    .difference(DUCKDB_UNRESERVED_CARVEOUT)
145    .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
146    .union(DUCKDB_QUALIFY_RESERVATION)
147    .union(DUCKDB_PIVOT_RESERVATION);
148
149/// DuckDB bare-label reject set: the shared model plus `QUALIFY`, so a projection or
150/// FROM-relation bare alias cannot swallow the clause keyword (`SELECT a FROM t
151/// QUALIFY …` reads the clause); `PIVOT`/`UNPIVOT`, so a source's alias cannot swallow a
152/// trailing operator (`FROM t PIVOT (…)` reads the operator); and the nonstandard-join /
153/// semi-anti-join keywords, whose `type_function` class the engine likewise rejects as a
154/// bare projection label (`SELECT 1 asof` / `SELECT 1 semi` syntax-error while
155/// `SELECT 1 AS asof` / `SELECT 1 AS semi` parse).
156pub const DUCKDB_RESERVED_BARE_ALIAS: KeywordSet = RESERVED_BARE_ALIAS
157    .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
158    .union(DUCKDB_UNRESERVED_BARE_ALIAS_RESERVATION)
159    .union(DUCKDB_QUALIFY_RESERVATION)
160    .union(DUCKDB_PIVOT_RESERVATION)
161    .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
162    .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION);
163
164impl NumericLiteralSyntax {
165    /// The `DUCKDB` preset for numeric literal syntax.
166    pub const DUCKDB: Self = Self {
167        hex_integers: true,
168        octal_integers: true,
169        binary_integers: true,
170        underscore_separators: true,
171        // DuckDB lexes numerics loosely (`123abc` re-reads as `123` aliased), so the
172        // leading-underscore radix opener stays off and `0x_1F` keeps its `0` + word split.
173        radix_leading_underscore: false,
174        money_literals: false,
175        reject_trailing_junk: false,
176    };
177}
178
179impl PredicateSyntax {
180    /// The `DUCKDB` preset for predicate syntax.
181    pub const DUCKDB: Self = Self {
182        unparenthesized_in_list: true,
183        // DuckDB rejects the SQL-standard `OVERLAPS` period predicate (engine-probed
184        // 1.5.4).
185        overlaps_period_predicate: false,
186        // The PostgreSQL `LIKE/ILIKE ANY|ALL (array)` pattern-match quantifier is not a
187        // DuckDB construct.
188        pattern_match_quantifier: false,
189        between_symmetric: false,
190        is_normalized: false,
191        // DuckDB accepts the two-word `<expr> NOT NULL` postfix (engine-measured).
192        null_test_two_word_postfix: true,
193        is_distinct_from: true,
194        like: true,
195        ilike: true,
196        similar_to: true,
197        empty_in_list: false,
198    };
199}
200
201/// The DuckDB binding-power table: the standard table with the `->` token re-ranked
202/// below every expression operator.
203///
204/// DuckDB lexes `->` as its own `LAMBDA_ARROW` grammar token (its `->>` stays an
205/// ordinary `Op`), ranked looser than everything — measured on 1.5.4 via
206/// `json_serialize_sql`: `x -> x % 2 = 0`, `x -> x OR y`, and
207/// `elem -> extract(…) BETWEEN 2000 AND 2022` each put the whole right side in the
208/// arrow's right operand, `NOT x -> y` and `a = x -> y` each take the full left
209/// expression as the arrow's left operand, and `x -> y -> z` groups left. `4`/`5`
210/// sits below `or` (10) with left-associativity, reproducing exactly that. The rank
211/// belongs to the *token*, not to the lambda reading: a non-parameter left operand
212/// still folds as the JSON accessor, at this same DuckDB rank (one table
213/// drives parser and renderer, per dialect).
214pub const DUCKDB_BINDING_POWERS: BindingPowerTable = BindingPowerTable {
215    or: BindingPower {
216        left: 10,
217        right: 11,
218        assoc: Assoc::Left,
219    },
220    xor: BindingPower {
221        left: 15,
222        right: 16,
223        assoc: Assoc::Left,
224    },
225    and: BindingPower {
226        left: 20,
227        right: 21,
228        assoc: Assoc::Left,
229    },
230    comparison: BindingPower {
231        left: 40,
232        right: 41,
233        assoc: Assoc::NonAssoc,
234    },
235    range_predicate_override: None,
236    // The `IS`-family predicates (`IS NULL`, `IS DISTINCT FROM`, `IS TRUE`, …) rank one tier
237    // below comparison, so `a <> b IS NULL` groups `(a <> b) IS NULL` and `a IS DISTINCT FROM
238    // b = c` groups `a IS DISTINCT FROM (b = c)` (measured on 1.5.4 via `json_serialize_sql`).
239    is_predicate_override: Some(IS_PREDICATE_BELOW_COMPARISON),
240    // DuckDB lexes `==` as a generic `%left Op`, not the `%nonassoc '='` comparison.
241    double_equals: BindingPower {
242        left: 45,
243        right: 46,
244        assoc: Assoc::Left,
245    },
246    additive: BindingPower {
247        left: 50,
248        right: 51,
249        assoc: Assoc::Left,
250    },
251    multiplicative: BindingPower {
252        left: 60,
253        right: 61,
254        assoc: Assoc::Left,
255    },
256    exponent: BindingPower {
257        left: 65,
258        right: 66,
259        assoc: Assoc::Left,
260    },
261    string_concat: BindingPower {
262        left: 45,
263        right: 46,
264        assoc: Assoc::Left,
265    },
266    any_operator: BindingPower {
267        left: 45,
268        right: 46,
269        assoc: Assoc::Left,
270    },
271    json_get: BindingPower {
272        left: 4,
273        right: 5,
274        assoc: Assoc::Left,
275    },
276    bitwise_or: BindingPower {
277        left: 45,
278        right: 46,
279        assoc: Assoc::Left,
280    },
281    bitwise_and: BindingPower {
282        left: 45,
283        right: 46,
284        assoc: Assoc::Left,
285    },
286    bitwise_shift: BindingPower {
287        left: 45,
288        right: 46,
289        assoc: Assoc::Left,
290    },
291    bitwise_xor: BindingPower {
292        left: 45,
293        right: 46,
294        assoc: Assoc::Left,
295    },
296    prefix_not: 30,
297    prefix_sign: 80,
298    prefix_bitwise_not: 46,
299    at_time_zone: BindingPower {
300        left: 70,
301        right: 71,
302        assoc: Assoc::Left,
303    },
304    collate: BindingPower {
305        left: 74,
306        right: 75,
307        assoc: Assoc::Left,
308    },
309    subscript: BindingPower {
310        left: 84,
311        right: 85,
312        assoc: Assoc::Left,
313    },
314    typecast: BindingPower {
315        left: 88,
316        right: 89,
317        assoc: Assoc::Left,
318    },
319    field_selection: BindingPower {
320        left: 92,
321        right: 93,
322        assoc: Assoc::Left,
323    },
324};
325
326impl SelectSyntax {
327    /// The `DUCKDB` preset for select syntax.
328    pub const DUCKDB: Self = Self {
329        // DuckDB rejects the empty target list where PostgreSQL's raw grammar accepts it.
330        empty_target_list: false,
331        qualify: true,
332        // DuckDB's `UNION [ALL] BY NAME` name-matched set operation (probed on 1.5.4):
333        // an additive grammar delta over the PostgreSQL base, UNION-only (the engine
334        // rejects `INTERSECT`/`EXCEPT BY NAME`). `duckdb-union-by-name`.
335        union_by_name: true,
336        // DuckDB's FROM-first SELECT (`FROM t SELECT x`, bare `FROM t`) — an additive
337        // grammar delta above the PostgreSQL base, which rejects a statement-position
338        // `FROM`. `FROM` is reserved under the shared model, so the leading-`FROM` primary
339        // can never shadow an identifier read (`duckdb-from-first-select`).
340        from_first: true,
341        explicit_table: true,
342        // DuckDB's `*`/`t.*` wildcard modifiers `EXCLUDE`/`REPLACE`/`RENAME` (probed on
343        // 1.5.4) — an additive grammar delta over the PostgreSQL base, which has no
344        // wildcard tail. `duckdb-select-star-modifiers`.
345        wildcard_modifiers: true,
346        // DuckDB aliases a qualified wildcard (`t.* AS x`, engine-probed 1.5.4) — the plain
347        // alias axis PostgreSQL shares, distinct from the DuckDB-only modifier tail above.
348        qualified_wildcard_alias: true,
349        // DuckDB rejects a ragged VALUES constructor (rows of differing width) at *parse*
350        // — `Parser Error: VALUES lists must all be the same length`, in every VALUES
351        // position (standalone, derived, INSERT; measured on 1.5.4) — where PostgreSQL's
352        // raw grammar accepts it and defers the check to bind. A shape-level tightening
353        // *below* the PostgreSQL base (like `empty_target_list`), enforced by the parser
354        // comparing the parsed rows' arities. `duckdb-from-clause-parse-overaccept`.
355        values_rows_require_equal_arity: true,
356        // DuckDB admits a single-quoted string literal as a projection alias
357        // (`(a = b) AS '(a = b)'`; probed on 1.5.4) — the MySQL-precedent
358        // `alias_string_literals` gate, reusing its projection-alias round-trip machinery.
359        // `duckdb-operator-and-literal-gaps`.
360        alias_string_literals: true,
361        // DuckDB accepts ONLY the `AS 'x'` form — the *bare* `SELECT 1 'x'` rejects (probed on
362        // 1.5.4), unlike SQLite/MySQL — so the bare axis stays off here.
363        bare_alias_string_literals: false,
364        // DuckDB tolerates a single trailing comma in its list positions — the SELECT /
365        // VALUES / collection-literal / `IN` lists (engine-probed 1.5.4), but not function
366        // arguments, `ORDER BY`, or a bare row constructor. `duckdb-trailing-comma`.
367        trailing_comma: true,
368        // DuckDB's prefix colon alias (`SELECT j : 42`, `FROM b : a`; probed on 1.5.4) — an
369        // additive grammar delta over the PostgreSQL base, pure sugar for a trailing `AS`
370        // alias that folds onto the existing alias field. Conflict-free here: DuckDB has no
371        // top-level semi-structured `a:b` access, the one construct that would claim the
372        // same `<ident> :` head. `duckdb-colon-alias`.
373        prefix_colon_alias: true,
374        distinct_on: true,
375        select_into: true,
376        wildcard_replace: false,
377        intersect_all: true,
378        except_all: true,
379        parenthesized_query_operands: true,
380        values_row_constructor: true,
381        as_alias_rejects_reserved: false,
382        lateral_view_clause: false,
383        connect_by_clause: false,
384    };
385}
386
387impl QueryTailSyntax {
388    /// The `DUCKDB` preset for query tail syntax.
389    pub const DUCKDB: Self = Self {
390        // DuckDB has no `FOR UPDATE`/`FOR SHARE` row locking. The
391        // strength/stacking refinements stay off too
392        // rather than inherit PostgreSQL's `true` for a dialect with no locking clause.
393        locking_clauses: false,
394        key_lock_strengths: false,
395        stacked_locking_clauses: false,
396        // DuckDB's `USING SAMPLE <entry>` query-level sample clause (probed on 1.5.4) —
397        // an additive grammar delta over the PostgreSQL base, which has no such clause.
398        // `duckdb-expression-and-clause-tails`.
399        using_sample: true,
400        // DuckDB's percentage `LIMIT` (`LIMIT 40 PERCENT`, `LIMIT 35%`; probed on 1.5.4) —
401        // an additive grammar delta over the PostgreSQL base, which has no percentage form.
402        // The marker folds only onto a numeric-literal count at a clause boundary, so
403        // ordinary modulo (`LIMIT 10 % 3`) and non-literal counts stay unaffected.
404        // `duckdb-limit-percent`.
405        limit_percent: true,
406        // PostgreSQL's raw-parse `WITH TIES` guards are not modelled for DuckDB (conservative
407        // — DuckDB's own `WITH TIES` validity is unprobed here); keep the PG-only behaviour.
408        with_ties_requires_order_by: false,
409        fetch_first: true,
410        limit_offset_comma: false,
411        leading_offset: true,
412        limit_expressions: true,
413        pipe_syntax: false,
414        limit_by_clause: false,
415        settings_clause: false,
416        format_clause: false,
417        for_xml_json_clause: false,
418    };
419}
420
421impl GroupingSyntax {
422    /// The `DUCKDB` preset for grouping syntax.
423    pub const DUCKDB: Self = Self {
424        // DuckDB's `GROUP BY ALL` / `ORDER BY ALL` clause modes (probed on 1.5.4) —
425        // purely additive grammar deltas: `ALL` is reserved under the shared
426        // PostgreSQL-derived model, so neither branch can shadow an identifier read.
427        group_by_all: true,
428        group_by_set_quantifier: false,
429        order_by_all: true,
430        grouping_sets: true,
431        with_rollup: false,
432        order_by_using: true,
433    };
434}
435
436impl ExpressionSyntax {
437    /// The `DUCKDB` preset for expression syntax.
438    pub const DUCKDB: Self = Self {
439        collection_literals: true,
440        // The three-bound `[lower:upper:step]` slice with its `-` open-upper placeholder.
441        slice_step: true,
442        // The `#n` positional column reference — a DuckDB-only extension.
443        positional_column: true,
444        lambda_keyword: true,
445        // DuckDB parses `(struct).field` but
446        // has no `.*` value-expansion production — `(struct).*`, `ROW(t.*)`, `f(t.*)`,
447        // `t.*::type` all parse-reject (engine-probed 1.5.4). Override the POSTGRES `true`.
448        field_wildcard: false,
449        // DuckDB reaches nested `ARRAY[[1,2],[3,4]]` through `collection_literals` (a
450        // top-level `[…]` list is a value there, and levels may mix scalars and lists),
451        // so the multidimensional array production stays off.
452        multidim_array_literals: false,
453        semi_structured_access: false,
454        // The relaxed interval spellings (`INTERVAL 3 DAYS`, `INTERVAL (x) DAY`).
455        relaxed_interval_syntax: true,
456        typecast_operator: true,
457        subscript: true,
458        collate: true,
459        at_time_zone: true,
460        array_constructor: true,
461        row_constructor: true,
462        struct_constructor: false,
463        field_selection: true,
464        typed_string_literals: true,
465        typed_interval_literal: true,
466        mysql_interval_operator: false,
467    };
468}
469
470impl OperatorSyntax {
471    /// The `DUCKDB` preset for operator syntax.
472    pub const DUCKDB: Self = Self {
473        lambda_expressions: true,
474        double_equals: true,
475        integer_divide_slash: true,
476        starts_with_operator: true,
477        // Off, overriding the inherited PostgreSQL `true`: DuckDB spells `?` as the anonymous
478        // placeholder (`anonymous_question`), which contends with the `?`-led `jsonb`
479        // operators, and it has none of that PostgreSQL `jsonb` operator family. Forcing it
480        // off keeps `FeatureSet::DUCKDB` lexically consistent (the `const` assert below).
481        jsonb_operators: false,
482        // On, inheriting PostgreSQL's `true`. `^`-as-exponentiation (`caret_operator:
483        // Exponent`, on the preset below) turns on too: both were probed against DuckDB 1.5.4
484        // (`duckdb-operator-surface-sweep`, `duckdb-pg-operator-spelling-under-acceptance`).
485        //
486        // `^` is exponentiation with the *same* precedence row this preset already carries
487        // (the shared [`exponent`](crate::precedence::BindingPowerTable::exponent) rank, which
488        // `DUCKDB_BINDING_POWERS` inherits unchanged): probed `2^3*2 = 16` (`^` tighter than
489        // `*`), `2^3^2 = 64` (left-associative), `-2^2 = 4` (unary sign tighter than `^`) —
490        // identical to the PostgreSQL fit. So `CaretOperator::Exponent` is the honest model.
491        // (DuckDB also spells the same power as `**`, an unmodelled synonym with no corpus
492        // member — tracked on the sweep ticket, not this flag.)
493        //
494        // `custom_operators` turns on: DuckDB inherits PostgreSQL's generalized maximal-munch
495        // operator lexer and *parse*-accepts the same `Op`-class runs — `1 <<| 2`, `1 <-> 2`,
496        // `p &&&&&@ q`, regex `~`/`!~`/`~*` — via `duckdb_extract_statements`, then
497        // bind-rejects the ones with no backing function (`1 <<| 2` → Catalog error). A
498        // parse-accept that binds-fail is still under-acceptance when we reject at *parse*: our
499        // parser is parse-only and the DuckDB accept/reject oracle compares parse acceptance
500        // (`m2::duckdb_raw_bytes_divergence` reads `extract_statement_count`), so folding an
501        // unknown run onto [`Expr::NamedOperator`](crate::ast::Expr::NamedOperator) matches the
502        // engine's parse verdict — it does not claim DuckDB is user-extensible. DuckDB's real
503        // operators still fold onto their dedicated [`BinaryOperator`] keys ahead of the generic
504        // surface (`&&`/`^@`/`//`/`==` in `known_operator_token`), so their shape is unchanged.
505        //
506        // DuckDB's `Op` charset is PostgreSQL's *minus* `#` and `?`, which it repurposes as the
507        // positional-column sigil (`#1`) and the anonymous parameter placeholder — the lexer's
508        // `is_operator_char` drops them under `positional_column` / `anonymous_question`, so a
509        // run stops at either (`1 @#@ 2` is `@` then a stray `#` — reject on both DuckDB and
510        // here; `1 @?@ 2` is `@` then a `?` placeholder). Engine-measured across the full
511        // single/doubled/prefix/postfix/trailing-sign matrix on DuckDB 1.5.4
512        // (`duckdb-pg-operator-spelling-under-acceptance`). Backtick is an `Op`-class byte here
513        // (DuckDB does not quote identifiers with it), so `` `= `` lexes as an operator. DuckDB
514        // *postfix* symbolic operators (`1 !`, `1 ~`, `1 @` — PostgreSQL removed postfix in 14)
515        // are a distinct axis carried by `postfix_operators` below (the parser-side postfix
516        // reduction), not by this tokenizer-plus-infix/prefix flag.
517        custom_operators: true,
518        // On, overriding the inherited PostgreSQL `false`: DuckDB keeps the generalized postfix
519        // reading PostgreSQL removed in version 14 — `10!`, `1 ~`, `1 <->`, `1 &` all
520        // parse-accept (then bind-reject the ones with no backing `__postfix` function).
521        // Engine-measured on DuckDB 1.5.4 (`duckdb-postfix-operator-dimension`). See the flag
522        // doc for the MECE split against `custom_operators` and the eligible-token set.
523        postfix_operators: true,
524        // Inherited from DuckDB's PostgreSQL-fork grammar, which keeps the `a_expr ISNULL` /
525        // `a_expr NOTNULL` postfix synonyms (additive over PostgreSQL like the other shared
526        // operator knobs).
527        null_test_postfix: true,
528        // The PostgreSQL any-operator quantifier (`3 * ANY(list)`) is not a DuckDB
529        // construct.
530        quantified_arbitrary_operator: false,
531        operator_construct: true,
532        containment_operators: true,
533        json_arrow_operators: true,
534        is_general_equality: false,
535        truth_value_tests: true,
536        null_safe_equals: false,
537        bitwise_operators: true,
538        quantified_comparisons: true,
539        quantified_comparison_lists: true,
540    };
541}
542
543impl CallSyntax {
544    /// The `DUCKDB` preset for call syntax.
545    pub const DUCKDB: Self = Self {
546        columns_expression: true,
547        try_cast: true,
548        extract_string_field: true,
549        method_chaining: true,
550        variadic_argument: true,
551        // The PostgreSQL SQL/JSON empty-constructor reject is not modelled for DuckDB
552        // (conservative — DuckDB's `json()` surface is unprobed here); keep it a plain call.
553        sqljson_constructors_require_argument: false,
554        // DuckDB has no SQL:2016 SQL/JSON expression-function special forms (its JSON
555        // support is ordinary functions like `json_extract`), so the keyword heads stay
556        // plain call/name forms — override the inherited PostgreSQL `true`.
557        sqljson_expression_functions: false,
558        // DuckDB has no SQL/XML expression functions; override the inherited PostgreSQL
559        // `true` so the `xml*` keyword heads stay plain call/name forms.
560        xml_expression_functions: false,
561        // DuckDB has no `merge_action()` support function; override PostgreSQL's `true` so
562        // the reserved keyword head stays the "no call form" reject (conservative — DuckDB's
563        // MERGE surface is unprobed here).
564        merge_action_function: false,
565        named_argument: true,
566        utc_special_functions: false,
567        extract_from_syntax: true,
568        restricted_cast_targets: false,
569        convert_function: false,
570    };
571}
572
573impl StringFuncForms {
574    /// The `DUCKDB` preset for string func forms.
575    pub const DUCKDB: Self = Self {
576        // DuckDB's PG-fork string special forms diverge from PostgreSQL in exactly two
577        // knobs (both probed on 1.5.4): the SIMILAR/ESCAPE regex substring production
578        // was dropped (parser error), and OVERLAY kept *only* the PLACING form —
579        // `overlay('abc', 'X', 2, 1)` / `overlay('abc')` / `overlay()` are parser
580        // errors where PostgreSQL parse-accepts them as plain calls. Everything else
581        // (FROM/FOR + FOR-leading substring orders, b_expr POSITION operands, the
582        // loose trim_list tails) inherits PostgreSQL's `true` verbatim, each probed
583        // parse-accepting on the live engine.
584        substring_similar: false,
585        overlay_requires_placing: true,
586        // DuckDB's `COLLATION FOR (<expr>)` surface is unprobed; override PostgreSQL's
587        // `true` back to `false` (conservative — `COLLATION` stays an ordinary name head).
588        collation_for_expression: false,
589        substring_from_for: true,
590        substring_leading_for: true,
591        substring_plain_call_requires_2_or_3_args: false,
592        substr_from_for: false,
593        position_in: true,
594        position_asymmetric_operands: false,
595        overlay_placing: true,
596        trim_from: true,
597        trim_list_syntax: true,
598        ceil_to_field: false,
599        floor_to_field: false,
600        match_against: false,
601    };
602}
603
604impl AggregateCallSyntax {
605    /// The `DUCKDB` preset for aggregate call syntax.
606    pub const DUCKDB: Self = Self {
607        null_treatment: true,
608        standalone_argument_order_by: true,
609        // DuckDB accepts `FILTER (<predicate>)` without the standard `WHERE` (probed on 1.5.4).
610        filter_optional_where: true,
611        group_concat_separator: false,
612        within_group: true,
613        aggregate_filter: true,
614        aggregate_args_require_adjacent_paren: false,
615        aggregate_calls_reject_empty_arguments: false,
616        over_requires_windowable_function: false,
617        window_function_tail: false,
618    };
619}
620
621impl TypeNameSyntax {
622    /// The `DUCKDB` preset for type name syntax.
623    pub const DUCKDB: Self = Self {
624        composite_types: true,
625        enum_type: true,
626        // Empty `DECIMAL()`/`DEC()`/`NUMERIC()` parens mean the default `(18,3)` — probed on
627        // 1.5.4, byte-identical to a bare `DECIMAL` (`duckdb-empty-type-parens`).
628        empty_type_parens: true,
629        // DuckDB requires an unsigned `DECIMAL` modifier — a negative scale is rejected
630        // (probed on 1.5.4), unlike PostgreSQL, so this PG-inherited flag is turned back off.
631        signed_type_modifier: false,
632        // DuckDB admits a string-literal type modifier on a user-defined type name —
633        // `GEOMETRY('OGC:CRS84')` and the general `type_name('constant', ...)` form (probed
634        // on 1.5.4). `duckdb-geometry-type-and-overlaps-operator`.
635        string_type_modifiers: true,
636        extended_scalar_type_names: false,
637        set_type: false,
638        numeric_modifiers: false,
639        integer_display_width: false,
640        varchar_requires_length: false,
641        zoned_temporal_types: true,
642        character_set_annotation: false,
643        nullable_type: false,
644        low_cardinality_type: false,
645        fixed_string_type: false,
646        datetime64_type: false,
647        nested_type: false,
648        bit_width_integer_names: false,
649        liberal_type_names: false,
650        angle_bracket_types: false,
651    };
652}
653
654impl TableExpressionSyntax {
655    /// The `DUCKDB` preset for table expression syntax.
656    pub const DUCKDB: Self = Self {
657        // DuckDB's string-literal table alias (`FROM integers AS 't'('k')` / `t('k')`;
658        // probed on 1.5.4). `duckdb-string-literal-table-alias`.
659        string_literal_aliases: true,
660        only: true,
661        table_sample: true,
662        parenthesized_joins: true,
663        table_alias_column_lists: true,
664        join_using_alias: true,
665        index_hints: false,
666        table_hints: false,
667        partition_selection: false,
668        base_table_alias_column_lists: true,
669        aliased_parenthesized_join: true,
670        bare_table_alias_is_bare_label: false,
671        table_version: false,
672        table_json_path: false,
673        indexed_by: false,
674        prefix_colon_alias: true,
675    };
676}
677
678impl JoinSyntax {
679    /// The `DUCKDB` preset for join syntax.
680    pub const DUCKDB: Self = Self {
681        asof_join: true,
682        positional_join: true,
683        semi_anti_join: true,
684        // Spark/Hive-only; DuckDB parse-rejects the sided `LEFT/RIGHT SEMI/ANTI JOIN`
685        // spelling (engine-probed), accepting only its own side-less `SEMI`/`ANTI JOIN`.
686        sided_semi_anti_join: false,
687        // MSSQL-only; DuckDB parse-rejects `APPLY` in join position.
688        apply_join: false,
689        // DuckDB parse-rejects the SQL:2023 recursive-query SEARCH/CYCLE clauses
690        // (`syntax error at or near "SEARCH"`, probed on 1.5.4), so it overrides the
691        // PostgreSQL surface it otherwise inherits below — the `data_modifying_ctes` split.
692        recursive_search_cycle: false,
693        // DuckDB parse-rejects a top-level ORDER BY/LIMIT/OFFSET on a `UNION`-bodied
694        // recursive CTE (`Parser Error: ORDER BY in a recursive query is not allowed`;
695        // probed on 1.5.4), overriding the inherited PostgreSQL parse-accept.
696        // `duckdb-recursive-cte-term-restrictions-over-accept`.
697        recursive_union_rejects_order_limit: true,
698        // DuckDB's keyed-recursion `USING KEY (cols)` clause between the CTE column list and
699        // `AS` (stable since 1.3; probed accepting on 1.5.4), overriding the inherited
700        // PostgreSQL off. `duckdb-with-using-key`.
701        recursive_using_key: true,
702        stacked_join_qualifiers: true,
703        full_outer_join: true,
704        natural_cross_join: false,
705        straight_join: false,
706    };
707}
708
709impl TableFactorSyntax {
710    /// The `DUCKDB` preset for table factor syntax.
711    pub const DUCKDB: Self = Self {
712        pivot: true,
713        unpivot: true,
714        // DuckDB's `DESCRIBE`/`SHOW`/`SUMMARIZE` utility as a parenthesized `FROM`
715        // table source (its `SHOW_REF` table_ref; probed on 1.5.4).
716        // `duckdb-statement-in-query-position`.
717        show_ref: true,
718        // DuckDB's bare `FROM VALUES (…) AS t` row-list table factor (no parentheses;
719        // probed on 1.5.4). `duckdb-from-values-table-factor`.
720        from_values: true,
721        // DuckDB parse-rejects `JSON_TABLE(… COLUMNS …)` (reads `json_table` as an ordinary
722        // name) and `XMLTABLE(` (probed on 1.5.4), so both override the inherited PostgreSQL
723        // surface off — the same `recursive_search_cycle` split above.
724        json_table: false,
725        xml_table: false,
726        lateral: true,
727        table_functions: true,
728        rows_from: true,
729        unnest: true,
730        unnest_with_offset: false,
731        table_function_ordinality: true,
732        special_function_table_source: true,
733        table_expr_factor: false,
734        pivot_value_sources: false,
735        match_recognize: false,
736        open_json: false,
737    };
738}
739
740impl UtilitySyntax {
741    /// The `DUCKDB` preset for utility syntax.
742    pub const DUCKDB: Self = Self {
743        pragma: true,
744        use_statement: true,
745        // DuckDB's `USE <catalog> . <schema>` admits the dotted two-part name (MySQL's
746        // single-ident form is a subset); the deeper `USE a.b.c` is still parser-rejected.
747        use_qualified_name: true,
748        // DuckDB's `USE` name production also admits a single-part Sconst (`USE 'n'`,
749        // `USE E'n'`, `USE $$n$$`; engine-measured on libduckdb 1.5.4). Dotted string
750        // names (`USE 'a'.'b'`) remain rejects.
751        use_string_literal_name: true,
752        prepared_statements: true,
753        // DuckDB structurally rejects the PostgreSQL `PREPARE name(<type>, …)` typed
754        // parameter-type list
755        // ("Prepared statement argument types are not supported, use CAST"; probed on
756        // 1.5.4), so only the bare `PREPARE <name> AS <statement>` form is admitted.
757        prepare_typed_parameters: false,
758        call: true,
759        // DuckDB has `ATTACH`/`DETACH` (its own catalog databases), so the shared gate is
760        // on — closing the `DETACH <db>` coverage gap. It extends `DETACH DATABASE` with an
761        // `IF EXISTS` guard (`detach_if_exists`), and adds the `[FORCE] CHECKPOINT [db]`
762        // operands (`checkpoint_database`), the bare-identifier `LOAD tpch` argument
763        // (`load_bare_name`), and the `RESET`-scope prefix (`reset_scope`) — all DuckDB
764        // extensions alongside `CHECKPOINT` and extension loading.
765        attach: true,
766        load_bare_name: true,
767        reset_scope: true,
768        detach_if_exists: true,
769        // Override the PostgreSQL base: DuckDB has no `DO` anonymous code block (`DO $$...$$`
770        // is a parser error, probed on 1.5.4), so the leading `DO` keyword stays
771        // undispatched and surfaces as an unknown statement.
772        do_statement: false,
773        // DuckDB's `EXPORT DATABASE`/`IMPORT DATABASE` catalogue round-trip — the pair is a
774        // DuckDB extension gated as one unit.
775        export_import_database: true,
776        // DuckDB's `UPDATE EXTENSIONS [( <name>, ... )]` extension-refresh statement — a
777        // DuckDB extension with its own statement head.
778        update_extensions: true,
779        // Every remaining utility head is explicitly pinned below.
780        copy: true,
781        copy_into: false,
782        stage_references: false,
783        comment_on: true,
784        comment_if_exists: false,
785        kill: false,
786        handler_statements: false,
787        plugin_component_statements: false,
788        shutdown: false,
789        restart: false,
790        clone: false,
791        import_table: false,
792        help_statement: false,
793        binlog: false,
794        key_cache_statements: false,
795        prepared_statements_from: false,
796        call_bare_name: false,
797        load_extension: true,
798        load_data: false,
799        do_expression_list: false,
800        lock_tables: false,
801        lock_instance: false,
802        rename_statement: false,
803        signal_diagnostics: false,
804        flush: false,
805        purge_binary_logs: false,
806        replication_statements: false,
807    };
808}
809impl TransactionSyntax {
810    /// Transaction-control surface for the `DUCKDB` preset (split from UtilitySyntax).
811    pub const DUCKDB: Self = Self {
812        start_transaction: true,
813        start_transaction_block_optional: true,
814        transaction_work_keyword: true,
815        begin_transaction_keyword: true,
816        commit_transaction_keyword: true,
817        rollback_transaction_keyword: true,
818        transaction_name: false,
819        begin_transaction_modes: true,
820        transaction_savepoints: false,
821        set_transaction: false,
822        transaction_isolation_mode: false,
823        transaction_access_mode: true,
824        transaction_deferrable_mode: false,
825        start_transaction_isolation_mode: false,
826        start_transaction_deferrable_mode: false,
827        start_transaction_consistent_snapshot: false,
828        transaction_multiple_modes: false,
829        transaction_modes_require_commas: false,
830        transaction_modes_reject_duplicates: false,
831        abort_transaction_alias: true,
832        end_transaction_alias: true,
833        transaction_release: false,
834        transaction_chain: false,
835        release_savepoint_keyword_optional: true,
836        begin_transaction_mode: false,
837        xa_transactions: false,
838    };
839}
840
841impl ShowSyntax {
842    /// The `DUCKDB` preset for show syntax.
843    pub const DUCKDB: Self = Self {
844        // DuckDB's `{DESCRIBE | SUMMARIZE} <query> | <table>` introspection statement — the
845        // `SHOW_REF` utility (probed on 1.5.4), which it desugars to `SELECT * FROM
846        // (SHOW_REF …)` and shares with the parenthesized-`FROM` `show_ref` table factor.
847        describe_summarize: true,
848        // DuckDB's `SHOW [ALL] TABLES [FROM <schema>]` catalogue listing (engine-probed
849        // 1.5.4), which it desugars to `SELECT * FROM (SHOW_REF …)`; modelled as the typed
850        // statement, distinct from the generic session `SHOW <var>` inherited from PG.
851        show_tables: true,
852        describe: false,
853        session_statements: true,
854        set_value_reserved_words: DUCKDB_RESERVED_SET_VALUE_WORDS,
855        set_value_on_keyword: false,
856        set_value_null_keyword: true,
857        show_columns: false,
858        show_create_table: false,
859        show_functions: false,
860        show_routine_status: false,
861        show_verbose: false,
862        show_admin: false,
863    };
864}
865
866impl MaintenanceSyntax {
867    /// The `DUCKDB` preset for maintenance syntax.
868    pub const DUCKDB: Self = Self {
869        checkpoint_database: true,
870        // DuckDB's `VACUUM [ANALYZE] [<table> [(<cols>)]]` and `ANALYZE [<table>
871        // [(<cols>)]]` statistics/compaction statements (both `PGVacuumStmt` in libpg_query;
872        // engine-probed 1.5.4). The leading `VACUUM` dispatches under `vacuum_analyze` (a
873        // separate gate from SQLite's `INTO`-shaped `vacuum`, which stays off); the leading
874        // `ANALYZE` under `analyze`, with the DuckDB column
875        // list under `analyze_columns`. Only the `ANALYZE` vacuum option parses —
876        // `FULL`/`FREEZE`/`VERBOSE`/`disable_page_skipping` throw in 1.5.4's transform.
877        vacuum_analyze: true,
878        analyze: true,
879        analyze_columns: true,
880        // The other maintenance statement heads stay off.
881        vacuum: false,
882        reindex: false,
883        checkpoint: true,
884        table_maintenance: false,
885    };
886}
887
888impl AccessControlSyntax {
889    /// The `DUCKDB` preset for access control syntax.
890    pub const DUCKDB: Self = Self {
891        // DuckDB recognizes the PostgreSQL-shaped GRANT/REVOKE and ALTER ROLE forms.
892        alter_role_rename: true,
893        access_control: true,
894        access_control_extended_objects: true,
895        user_role_management: false,
896        access_control_account_grants: false,
897    };
898}
899
900impl FeatureSet {
901    /// DuckDB as PostgreSQL-derived dialect data.
902    pub const DUCKDB: Self = Self {
903        identifier_casing: Casing::Lower,
904        identifier_quotes: STANDARD_IDENTIFIER_QUOTES,
905        default_null_ordering: NullOrdering::NullsLast,
906        reserved_column_name: DUCKDB_RESERVED_COLUMN_NAME,
907        reserved_function_name: DUCKDB_RESERVED_FUNCTION_NAME,
908        reserved_type_name: DUCKDB_RESERVED_TYPE_NAME,
909        reserved_bare_alias: DUCKDB_RESERVED_BARE_ALIAS,
910        reserved_as_label: KeywordSet::EMPTY,
911        // DuckDB relation names are `catalog.schema.table` in the shared table path (its
912        // own two-part narrowing is a separate, unstarted tightening).
913        catalog_qualified_names: true,
914        // The shared M1 table plus the vertical tab (`0x0b`) as statement-boundary trim
915        // (`DUCKDB_BYTE_CLASSES`): `libduckdb`-measured, DuckDB folds a `\v` at each
916        // `;`-segment's leading/trailing edge (`"\x0bSELECT 1"`, `"SELECT 1\x0b"` accept)
917        // but rejects one interior to a statement's content (`"SELECT\x0b1"` rejects,
918        // even beside a real space) — the tokenizer's `skip_trivia` boundary guard.
919        byte_classes: DUCKDB_BYTE_CLASSES,
920        binding_powers: DUCKDB_BINDING_POWERS,
921        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
922        string_literals: StringLiteralSyntax::POSTGRES,
923        numeric_literals: NumericLiteralSyntax::DUCKDB,
924        // PostgreSQL's `$1` positional parameters plus DuckDB's anonymous `?` placeholder
925        // (`SELECT 'Test' LIMIT ?`). `?` adds a second parameter claimant but no lexical
926        // conflict: DuckDB has no `?`-led operator (its JSON `?`/`?|`/`?&` existence
927        // operators are unimplemented — `SELECT '{}'::JSON ? 'a'` syntax-errors on 1.5.4),
928        // so `?` has a single claimant and the `is_lexically_consistent` ratchet below
929        // still holds.
930        parameters: ParameterSyntax {
931            anonymous_question: true,
932            positional_dollar: true,
933            positional_dollar_large: true,
934            named_colon: false,
935            named_at: false,
936            named_dollar: false,
937            numbered_question: false,
938        },
939        session_variables: SessionVariableSyntax::ANSI,
940        // DuckDB admits a single-part Sconst table name (`FROM 't'`, `FROM ''`;
941        // engine-measured on libduckdb 1.5.4) on top of the PostgreSQL identifier base
942        // (any non-ASCII, `$` continuation, no SQLite multi-part string identifiers).
943        identifier_syntax: IdentifierSyntax {
944            non_ascii: super::NonAsciiIdentifierSyntax::Any,
945            dollar_in_identifiers: true,
946            string_literal_identifiers: false,
947            string_literal_table_names: true,
948            empty_quoted_identifiers: false,
949        },
950        table_expressions: TableExpressionSyntax::DUCKDB,
951        join_syntax: JoinSyntax::DUCKDB,
952        table_factor_syntax: TableFactorSyntax::DUCKDB,
953        expression_syntax: ExpressionSyntax::DUCKDB,
954        operator_syntax: OperatorSyntax::DUCKDB,
955        call_syntax: CallSyntax::DUCKDB,
956        string_func_forms: StringFuncForms::DUCKDB,
957        aggregate_call_syntax: AggregateCallSyntax::DUCKDB,
958        predicate_syntax: PredicateSyntax::DUCKDB,
959        pipe_operator: PipeOperator::StringConcat,
960        double_ampersand: DoubleAmpersand::Overlaps,
961        // DuckDB's `GLOB` infix (desugars to `~~~` glob match; probed 1.5.4). MATCH/REGEXP
962        // keyword forms are not DuckDB infix operators.
963        keyword_operators: KeywordOperators::DuckDb,
964        // The one bitwise divergence from PostgreSQL: DuckDB has no bitwise XOR operator —
965        // it rejects PostgreSQL's `#` and reads `^` as *exponentiation* (both measured on
966        // 1.5.4: `SELECT 5 # 3` syntax-errors, `SELECT 5 ^ 3` is `125`). It spells bitwise
967        // XOR as the `xor(a, b)` function instead, which parses as an ordinary call. The
968        // shared `| & ~ << >>` family stays on, inherited via `ExpressionSyntax::DUCKDB`.
969        // `^`-as-exponentiation is the honest `caret_operator` reading (rationale in the
970        // `OperatorSyntax::DUCKDB` block above); `#` is not the XOR operator.
971        caret_operator: CaretOperator::Exponent,
972        hash_bitwise_xor: false,
973        // DuckDB shares PostgreSQL's flex-derived scanner, so a `--` line comment ends at
974        // `\r` as well as `\n` (measured on 1.5.4) — `CommentSyntax::POSTGRES`, not the
975        // `\n`-only ANSI baseline. Block-comment nesting (also on in `POSTGRES`) already
976        // matched DuckDB, so this is the only comment-shape change from `ANSI`.
977        comment_syntax: CommentSyntax::POSTGRES,
978        mutation_syntax: MutationSyntax {
979            // DuckDB parse-rejects a DML CTE body (`A CTE needs a SELECT`; INSERT,
980            // UPDATE, and DELETE bodies all probed on 1.5.4). MERGE
981            // (1.4+), MERGE … RETURNING, and the leading `WITH` before MERGE, all
982            // probed accepted on 1.5.4 — is shared with PostgreSQL.
983            data_modifying_ctes: false,
984            // DuckDB rejects `OVERRIDING` *inside* MERGE (`syntax error at or near
985            // "OVERRIDING"`, probed on 1.5.4) even though it accepts it on a top-level
986            // INSERT. The `WHEN NOT MATCHED BY SOURCE/TARGET` arms and
987            // `INSERT DEFAULT VALUES` are both accepted on 1.5.4.
988            merge_insert_overriding: false,
989            // DuckDB MERGE extensions (probed on 1.5.4): `UPDATE SET *`, `INSERT *` /
990            // `INSERT BY NAME [*]`, and `THEN ERROR`.
991            merge_update_set_star: true,
992            merge_insert_star_by_name: true,
993            merge_error_action: true,
994            // DuckDB accepts SQLite-style `INSERT OR REPLACE` / `OR IGNORE` (probed 1.5.4).
995            or_conflict_action: true,
996            insert_column_matching: true,
997            // DuckDB parser rejects explicit tuple-assignment value-row arity mismatches
998            // (`UPDATE t SET (a, b, c) = (1, 2)`), unlike PostgreSQL parse-only.
999            update_tuple_value_row_arity: true,
1000            // DuckDB rejects qualified SET targets (`UPDATE t SET t.i = 1` — probed 1.5.4).
1001            update_set_qualified_column: false,
1002            insert_ignore: false,
1003            insert_overwrite: false,
1004            returning: true,
1005            on_conflict: true,
1006            on_duplicate_key_update: false,
1007            multi_column_assignment: true,
1008            where_current_of: true,
1009            merge: true,
1010            replace_into: false,
1011            insert_set: false,
1012            update_delete_tails: false,
1013            joined_update_delete: false,
1014            delete_using: true,
1015            update_from: true,
1016            delete_using_target_alias: true,
1017            cte_before_insert: true,
1018            cte_before_merge: true,
1019            merge_when_not_matched_by: true,
1020            merge_insert_default_values: true,
1021            merge_insert_multirow: false,
1022        },
1023        // PostgreSQL's schema-change surface plus DuckDB's live-body macro DDL
1024        // (`CREATE MACRO`/`CREATE FUNCTION … AS <expr>|TABLE <query>`), `CREATE OR REPLACE
1025        // TABLE`, and the `CREATE [PERSISTENT] SECRET` secrets statement.
1026        statement_ddl_gates: StatementDdlGates {
1027            // DuckDB accepts the shared sequence option core but rejects PostgreSQL's
1028            // `CACHE` extension.
1029            colocation_groups: false,
1030            create_trigger: false,
1031            create_macro: true,
1032            create_secret: true,
1033            create_type: true,
1034            create_virtual_table: false,
1035            create_sequence: true,
1036            // DuckDB has no `CREATE DATABASE` (it uses `ATTACH`); the shared gate is off so
1037            // `DATABASE` after `CREATE` falls through as an unknown statement (probed
1038            // 1.5.4: "syntax error at or near \"DATABASE\"").
1039            databases: false,
1040            // DuckDB rejects the SQL-standard embedded schema-element form.
1041            schema_elements: false,
1042            schemas: true,
1043            drop_database: false,
1044            materialized_views: true,
1045            routines: true,
1046            or_replace: true,
1047            create_or_replace_table: true,
1048            // DuckDB accepts `CREATE [OR REPLACE] [TEMP] RECURSIVE VIEW v (cols) AS …`
1049            // (engine-measured on 1.5.4).
1050            compound_statements: false,
1051            // DuckDB manages extensions with `INSTALL`/`LOAD`, not the PostgreSQL
1052            // `CREATE`/`ALTER EXTENSION` catalogue DDL.
1053            extension_ddl: false,
1054            // DuckDB has no transform catalogue (`pg_transform` / `CREATE TRANSFORM` is
1055            // PostgreSQL-only), so it must clear the POSTGRES `true` rather than inherit it.
1056            transform_ddl: false,
1057            // DuckDB has no `ALTER SYSTEM` server-configuration DDL (it configures through
1058            // `SET`/`RESET`), so it must clear the POSTGRES `true` rather than inherit it.
1059            alter_system: false,
1060            // MySQL's tablespace / logfile-group storage DDL is not a DuckDB statement.
1061            tablespace_ddl: false,
1062            logfile_group_ddl: false,
1063            // DuckDB's `ALTER DATABASE … SET ALIAS TO`, `ALTER SEQUENCE …` option list, and
1064            // `ALTER {TABLE|VIEW|SEQUENCE} … SET SCHEMA` forms (engine-measured on 1.5.4).
1065            alter_database: true,
1066            // MySQL-only families have no DuckDB equivalent.
1067            alter_database_options: false,
1068            server_definition: false,
1069            alter_instance: false,
1070            spatial_reference_system: false,
1071            resource_group: false,
1072            alter_sequence: true,
1073            alter_object_set_schema: true,
1074        },
1075        view_sequence_clause_syntax: ViewSequenceClauseSyntax {
1076            create_sequence_cache: false,
1077            materialized_view_to: false,
1078            temporary_views: true,
1079            recursive_views: true,
1080            view_definition_options: false,
1081        },
1082        create_table_clause_syntax: CreateTableClauseSyntax {
1083            // DuckDB has no PostgreSQL-style declarative partitioning (its `PARTITION_BY` is a
1084            // COPY/export option, not a `CREATE TABLE` clause).
1085            declarative_partitioning: false,
1086            // DuckDB has no table inheritance and (probed against libduckdb) rejects the
1087            // PostgreSQL `(LIKE src …)` source-table element.
1088            table_inheritance: false,
1089            like_source_table: false,
1090            table_access_method: false,
1091            without_oids: false,
1092            typed_tables: false,
1093            create_table_as_execute: false,
1094            table_options: false,
1095            without_rowid_table_option: false,
1096            strict_table_option: false,
1097            storage_parameters: true,
1098            on_commit: true,
1099            create_table_as_with_data: true,
1100            statement_level_table_like: false,
1101            unlogged_tables: true,
1102        },
1103        column_definition_syntax: ColumnDefinitionSyntax {
1104            // The PostgreSQL `b_expr` column-default restriction is not modelled for DuckDB
1105            // (conservative — DuckDB's default-expression grammar class is unprobed here); it
1106            // reads the default as a full `a_expr`, unchanged.
1107            column_default_requires_b_expr: false,
1108            // DuckDB (probed against libduckdb) accepts a per-column `COLLATE <name>` and the
1109            // `UNLOGGED` persistence keyword, but rejects the column STORAGE/COMPRESSION
1110            // attributes, the table USING
1111            // access method, WITHOUT OIDS, and typed `OF <type>` tables, so those four must
1112            // clear the POSTGRES `true` rather than inherit it.
1113            column_storage: false,
1114            // DuckDB accepts the keywordless generated-column shorthand `<col> <type> AS
1115            // (<expr>) [VIRTUAL|STORED]` written without `GENERATED ALWAYS` (libduckdb 1.5.4:
1116            // `y INT AS (x + 1)` and `… VIRTUAL` parse-accept; `STORED` parses but is a binder
1117            // reject, out of this layer). PostgreSQL requires the keywords, so this must set
1118            // rather than inherit the POSTGRES `false`.
1119            generated_column_shorthand: true,
1120            // DuckDB requires a data type on every column *except* a generated one: both the
1121            // `AS (<expr>)` shorthand and the keyworded `GENERATED …` form may drop the type
1122            // (`CREATE TABLE t (x INT, gen_x AS (x + 5))`), while a plain typeless column is a
1123            // parse error. A narrowing of the SQLite typeless rule, distinct from PostgreSQL's
1124            // type-required `false`, so it too must set rather than inherit.
1125            typeless_generated_columns: true,
1126            column_conflict_resolution_clause: false,
1127            typeless_column_definitions: false,
1128            joined_autoincrement_attribute: false,
1129            inline_primary_key_ordering: false,
1130            named_column_collate_constraint: false,
1131            identity_columns: true,
1132            compact_identity_columns: false,
1133            default_expression_requires_parens: false,
1134            column_collation: true,
1135        },
1136        constraint_syntax: ConstraintSyntax {
1137            // DuckDB (probed against libduckdb 1.5.4) rejects PostgreSQL's `EXCLUDE` exclusion
1138            // constraints, the `AS EXECUTE` CTAS form, and the `UNIQUE`/`PRIMARY KEY`
1139            // index-parameter decorations (`INCLUDE`/`NULLS NOT DISTINCT`/`USING INDEX
1140            // TABLESPACE`), so all three must clear the POSTGRES `true`. It *does* accept the
1141            // `NO INHERIT` / `NOT VALID` constraint markers.
1142            exclusion_constraints: false,
1143            index_constraint_parameters: false,
1144            // DuckDB admits `ON DELETE`/`ON UPDATE` only for `RESTRICT`/`NO ACTION` —
1145            // `CASCADE`/`SET NULL`/`SET DEFAULT` are Parser Errors (probed 1.5.4).
1146            referential_action_cascade_set: false,
1147            // DuckDB (and SQLite) parse-reject subqueries in `CHECK` (probed 1.5.4).
1148            check_constraint_subqueries: false,
1149            deferrable_constraints: true,
1150            named_inline_non_check_constraints: true,
1151            bare_constraint_name: false,
1152            constraint_no_inherit_not_valid: true,
1153            constraint_column_collate_order: false,
1154        },
1155        index_alter_syntax: IndexAlterSyntax {
1156            // DuckDB's extended ALTER surface is on (IF EXISTS, RENAME, ALTER COLUMN, …)
1157            // but multi-action lists are not ("Only one ALTER command per statement",
1158            // probed 1.5.4).
1159            alter_table_multiple_actions: false,
1160            alter_nested_column_paths: true,
1161            // DuckDB parses table-option assignment lists and standalone index storage
1162            // parameters; option names and values remain binder concerns.
1163            alter_table_set_options: true,
1164            index_storage_parameters: true,
1165            // Table-scoped `DROP INDEX` remains outside the grammar.
1166            rename_constraint: true,
1167            drop_primary_key: false,
1168            alter_column_add_identity: false,
1169            drop_behavior: true,
1170            index_drop_on_table: false,
1171            index_concurrently: true,
1172            index_using_method: true,
1173            partial_index: true,
1174            index_if_not_exists: true,
1175            index_nulls_order: true,
1176            alter_table_extended: true,
1177            alter_existence_guards: true,
1178            alter_column_set_data_type: true,
1179            routine_arg_types: true,
1180            routine_arg_defaults: true,
1181            routine_arg_modes: true,
1182            routine_language_string: true,
1183        },
1184        existence_guards: ExistenceGuards::POSTGRES,
1185        select_syntax: SelectSyntax::DUCKDB,
1186        query_tail_syntax: QueryTailSyntax::DUCKDB,
1187        grouping_syntax: GroupingSyntax::DUCKDB,
1188        utility_syntax: UtilitySyntax::DUCKDB,
1189        transaction_syntax: TransactionSyntax::DUCKDB,
1190        show_syntax: ShowSyntax::DUCKDB,
1191        maintenance_syntax: MaintenanceSyntax::DUCKDB,
1192        access_control_syntax: AccessControlSyntax::DUCKDB,
1193        type_name_syntax: TypeNameSyntax::DUCKDB,
1194        // No DuckDB-specific Tier-1 output spelling yet: DuckDB shares PostgreSQL's
1195        // canonical type names (`INTEGER`, `VARCHAR`, `DECIMAL`), so it renders through
1196        // the PostgreSQL spelling table (minting a `TargetSpelling::DuckDb` is render
1197        // work a later ticket owns).
1198        target_spelling: TargetSpelling::Postgres,
1199    };
1200}
1201
1202/// Prefer [`FeatureSet::DUCKDB`] for struct update.
1203pub const DUCKDB: FeatureSet = FeatureSet::DUCKDB;
1204
1205// Compile-time proof the DuckDB preset claims no shared tokenizer trigger twice. Beyond
1206// PostgreSQL's lexical surface it adds exactly one trigger — the anonymous `?` parameter
1207// (`anonymous_question`) — which has a single claimant: DuckDB implements no `?`-led
1208// operator (its JSON `?`/`?|`/`?&` existence operators are absent), so `?` cannot contend
1209// and needs no registered conflict. The rest is PostgreSQL's surface: the numeric-radix
1210// scan, empty-target grammar gate, and QUALIFY reservation add no lexical trigger,
1211// `collection_literals` reuses the `[` punctuation PostgreSQL's `subscript`/
1212// `array_constructor` already claim (no preset here quotes identifiers with `[`, so the
1213// registered `BracketIdentifierVersusArraySyntax` conflict cannot fire), and
1214// `lambda_expressions` is a grammar-position gate over the `->` token
1215// `json_arrow_operators` already lexes (one lexical claimant; the lambda/JSON split
1216// happens in the parser by LHS shape). Kept as its own ratchet so a future DuckDB delta
1217// that *does* add a trigger fails the build here.
1218const _: () = assert!(FeatureSet::DUCKDB.is_lexically_consistent());
1219// The two sibling self-consistency registries are ratcheted the same way, so the
1220// parse-entry `debug_assert!` folds all three to dead code for this preset: every
1221// refinement flag (`slice_step`, `checkpoint_database`, `analyze_columns`, the bare-name
1222// utility tails) rides its enabled base, and no two features contend for one
1223// parser-position head (`prepared_statements_from` stays off, so the typed-`AS` lifecycle
1224// is unrivalled).
1225const _: () = assert!(FeatureSet::DUCKDB.has_satisfied_feature_dependencies());
1226const _: () = assert!(FeatureSet::DUCKDB.has_no_grammar_conflict());
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231    use crate::ast::{BinaryOperator, EqualsSpelling};
1232    use crate::precedence::STANDARD_BINDING_POWERS;
1233
1234    #[test]
1235    fn duckdb_is_postgres_plus_the_measured_deltas() {
1236        // The preset is PostgreSQL with a documented set of divergent axes (numeric
1237        // radix, SELECT surface, expression surface, table expressions, call surface —
1238        // `COLUMNS(…)` + `TRY_CAST` — type-name surface — the anonymous composite types —
1239        // the binding-power `->` re-rank, and the keyword reservations); asserting the
1240        // whole rest equals PostgreSQL keeps the "PG-derived, every delta documented"
1241        // claim honest against a future stray edit.
1242        let pg = FeatureSet::POSTGRES;
1243        let duck = FeatureSet::DUCKDB;
1244        assert_eq!(duck.numeric_literals, NumericLiteralSyntax::DUCKDB);
1245        assert_eq!(duck.select_syntax, SelectSyntax::DUCKDB);
1246        assert_eq!(duck.expression_syntax, ExpressionSyntax::DUCKDB);
1247        assert_eq!(duck.table_expressions, TableExpressionSyntax::DUCKDB);
1248        assert_eq!(duck.call_syntax, CallSyntax::DUCKDB);
1249        assert_eq!(duck.type_name_syntax, TypeNameSyntax::DUCKDB);
1250        assert_ne!(duck.numeric_literals, pg.numeric_literals);
1251        assert_ne!(duck.select_syntax, pg.select_syntax);
1252        assert_ne!(duck.expression_syntax, pg.expression_syntax);
1253        assert_ne!(duck.table_expressions, pg.table_expressions);
1254        assert_ne!(duck.call_syntax, pg.call_syntax);
1255        assert_ne!(duck.type_name_syntax, pg.type_name_syntax);
1256        assert_eq!(duck.binding_powers, DUCKDB_BINDING_POWERS);
1257        assert_ne!(duck.numeric_literals, pg.numeric_literals);
1258        assert_ne!(duck.select_syntax, pg.select_syntax);
1259        assert_ne!(duck.expression_syntax, pg.expression_syntax);
1260        assert_ne!(duck.binding_powers, pg.binding_powers);
1261        assert_eq!(duck.reserved_column_name, DUCKDB_RESERVED_COLUMN_NAME);
1262        assert_eq!(duck.reserved_function_name, DUCKDB_RESERVED_FUNCTION_NAME);
1263        assert_eq!(duck.reserved_type_name, DUCKDB_RESERVED_TYPE_NAME);
1264        assert_eq!(duck.reserved_bare_alias, DUCKDB_RESERVED_BARE_ALIAS);
1265
1266        // The utility surface adds the DuckDB `PRAGMA`, `USE`, and `CALL` statements plus
1267        // the `ATTACH`/`DETACH` pair and the DuckDB `CHECKPOINT`/`LOAD`/`RESET`/`DETACH`
1268        // extensions (`duckdb-settings-and-session-statements`, `duckdb-prepare-execute-call`,
1269        // `duckdb-utility-checkpoint-detach-load-reset`) over the inherited PostgreSQL
1270        // `COPY`/`COMMENT ON`/session/`CHECKPOINT`/`LOAD`/prepared-statement-lifecycle base
1271        // — a purely additive delta, plus the two reverse deltas noted below.
1272        assert_eq!(duck.utility_syntax, UtilitySyntax::DUCKDB);
1273        assert_eq!(
1274            duck.utility_syntax,
1275            UtilitySyntax {
1276                pragma: true,
1277                use_statement: true,
1278                use_qualified_name: true,
1279                use_string_literal_name: true,
1280                call: true,
1281                attach: true,
1282                load_bare_name: true,
1283                reset_scope: true,
1284                detach_if_exists: true,
1285                // DuckDB's `EXPORT DATABASE`/`IMPORT DATABASE` catalogue round-trip, off in
1286                // the PostgreSQL base.
1287                export_import_database: true,
1288                // DuckDB's `UPDATE EXTENSIONS` extension-refresh statement, off in the
1289                // PostgreSQL base.
1290                update_extensions: true,
1291                // The two flags DuckDB turns *off* relative to PostgreSQL: PostgreSQL's `DO`
1292                // anonymous code block (no DuckDB equivalent), and the `PREPARE` typed
1293                // parameter-type list (DuckDB structurally rejects it: "Prepared statement
1294                // argument types are not supported, use CAST" — probed on 1.5.4). The base
1295                // `prepared_statements` lifecycle itself (`PREPARE`/`EXECUTE`/`DEALLOCATE`) is
1296                // inherited unchanged from PostgreSQL, both on.
1297                do_statement: false,
1298                prepare_typed_parameters: false,
1299                ..pg.utility_syntax
1300            },
1301        );
1302        assert_eq!(duck.transaction_syntax, TransactionSyntax::DUCKDB);
1303        assert_eq!(
1304            duck.transaction_syntax,
1305            TransactionSyntax {
1306                // DuckDB 1.5.4 accepts bare `START`, `START WORK`, and a single access
1307                // mode, but has no savepoint, SET TRANSACTION, isolation/deferrable,
1308                // repeated-mode, or transaction-chain grammar.
1309                start_transaction_block_optional: true,
1310                transaction_savepoints: false,
1311                set_transaction: false,
1312                transaction_isolation_mode: false,
1313                transaction_deferrable_mode: false,
1314                start_transaction_isolation_mode: false,
1315                start_transaction_deferrable_mode: false,
1316                transaction_multiple_modes: false,
1317                abort_transaction_alias: true,
1318                end_transaction_alias: true,
1319                transaction_chain: false,
1320                ..pg.transaction_syntax
1321            },
1322        );
1323        assert_eq!(
1324            duck.maintenance_syntax,
1325            MaintenanceSyntax {
1326                checkpoint_database: true,
1327                // DuckDB's `VACUUM [ANALYZE] [<table> [(<cols>)]]` / `ANALYZE [<table>
1328                // [(<cols>)]]` statistics/compaction statements, off in the PostgreSQL base
1329                // (whose own VACUUM/ANALYZE grammar is unmodelled).
1330                vacuum_analyze: true,
1331                analyze: true,
1332                analyze_columns: true,
1333                ..pg.maintenance_syntax
1334            },
1335        );
1336        assert_eq!(
1337            duck.show_syntax,
1338            ShowSyntax {
1339                // DuckDB's typed `SHOW [ALL] TABLES [FROM <schema>]` catalogue listing, off
1340                // in the PostgreSQL base.
1341                show_tables: true,
1342                // DuckDB's leading-keyword `{DESCRIBE | SUMMARIZE}` introspection statement,
1343                // off in the PostgreSQL base.
1344                describe_summarize: true,
1345                // DuckDB does not admit PostgreSQL's special `ON` SET value.
1346                set_value_on_keyword: false,
1347                set_value_null_keyword: true,
1348                set_value_reserved_words: DUCKDB_RESERVED_SET_VALUE_WORDS,
1349                ..pg.show_syntax
1350            },
1351        );
1352        assert!(duck.utility_syntax.pragma && !pg.utility_syntax.pragma);
1353        assert!(duck.utility_syntax.use_statement && !pg.utility_syntax.use_statement);
1354        assert!(duck.utility_syntax.prepared_statements && pg.utility_syntax.prepared_statements);
1355        assert!(
1356            !duck.utility_syntax.prepare_typed_parameters
1357                && pg.utility_syntax.prepare_typed_parameters
1358        );
1359        assert!(duck.utility_syntax.call && !pg.utility_syntax.call);
1360        assert!(duck.utility_syntax.attach && !pg.utility_syntax.attach);
1361        // `checkpoint`/`load_extension` are inherited from PostgreSQL (both on there); the
1362        // DuckDB operand/argument/scope/guard extensions are the divergence.
1363        assert!(duck.maintenance_syntax.checkpoint && pg.maintenance_syntax.checkpoint);
1364        assert!(duck.utility_syntax.load_extension && pg.utility_syntax.load_extension);
1365        assert!(
1366            duck.maintenance_syntax.checkpoint_database
1367                && !pg.maintenance_syntax.checkpoint_database
1368        );
1369        assert!(duck.utility_syntax.load_bare_name && !pg.utility_syntax.load_bare_name);
1370        assert!(duck.utility_syntax.reset_scope && !pg.utility_syntax.reset_scope);
1371        assert!(duck.utility_syntax.detach_if_exists && !pg.utility_syntax.detach_if_exists);
1372        assert!(duck.show_syntax.show_tables && !pg.show_syntax.show_tables);
1373        assert!(duck.show_syntax.describe_summarize && !pg.show_syntax.describe_summarize);
1374        // `UPDATE EXTENSIONS` is DuckDB-only over the PostgreSQL base.
1375        assert!(duck.utility_syntax.update_extensions && !pg.utility_syntax.update_extensions);
1376        // `do_statement` is the reverse divergence: on in PostgreSQL, off in DuckDB.
1377        assert!(!duck.utility_syntax.do_statement && pg.utility_syntax.do_statement);
1378
1379        // Everything else is inherited verbatim from PostgreSQL.
1380        assert_eq!(duck.string_literals, pg.string_literals);
1381        // Parameters differ in exactly one knob: DuckDB lexes the anonymous `?`
1382        // placeholder, which PostgreSQL does not (PG uses `$1` only). Every other field is
1383        // inherited (forcing `anonymous_question` off recovers PG).
1384        assert!(duck.parameters.anonymous_question);
1385        assert!(!pg.parameters.anonymous_question);
1386        assert_eq!(
1387            ParameterSyntax {
1388                anonymous_question: false,
1389                ..duck.parameters
1390            },
1391            pg.parameters,
1392        );
1393        // The mutation surface differs in the listed DuckDB/PG deltas: PostgreSQL admits
1394        // data-modifying CTE bodies (which DuckDB parse-rejects, `A CTE needs a
1395        // SELECT`) and `OVERRIDING` inside a MERGE insert (which DuckDB parse-rejects,
1396        // `syntax error at or near "OVERRIDING"`) — both probed on 1.5.4 — while DuckDB
1397        // adds MERGE star/by-name/error actions, INSERT column matching / verb-level
1398        // conflict actions, parse-time tuple value-row arity checks, and rejects qualified
1399        // UPDATE SET targets. Everything
1400        // else — including MERGE, its RETURNING tail, the leading `WITH` before MERGE,
1401        // the `WHEN NOT MATCHED BY SOURCE/TARGET` arms, and `INSERT DEFAULT VALUES`
1402        // (all probed accepted on 1.5.4) — is inherited verbatim (forcing the two
1403        // knobs on recovers PG).
1404        assert!(!duck.mutation_syntax.data_modifying_ctes);
1405        assert!(pg.mutation_syntax.data_modifying_ctes);
1406        assert!(!duck.mutation_syntax.merge_insert_overriding);
1407        assert!(pg.mutation_syntax.merge_insert_overriding);
1408        assert!(duck.mutation_syntax.merge_update_set_star);
1409        assert!(duck.mutation_syntax.merge_insert_star_by_name);
1410        assert!(duck.mutation_syntax.merge_error_action);
1411        assert!(!pg.mutation_syntax.merge_update_set_star);
1412        assert!(duck.mutation_syntax.merge_when_not_matched_by);
1413        assert!(duck.mutation_syntax.merge_insert_default_values);
1414        assert!(!duck.mutation_syntax.update_set_qualified_column);
1415        assert!(pg.mutation_syntax.update_set_qualified_column);
1416        assert_eq!(
1417            MutationSyntax {
1418                data_modifying_ctes: true,
1419                merge_insert_overriding: true,
1420                merge_update_set_star: false,
1421                merge_insert_star_by_name: false,
1422                merge_error_action: false,
1423                insert_column_matching: false,
1424                or_conflict_action: false,
1425                update_tuple_value_row_arity: false,
1426                update_set_qualified_column: true,
1427                ..duck.mutation_syntax
1428            },
1429            pg.mutation_syntax,
1430        );
1431        // The schema-change surface differs in exactly four knobs: DuckDB enables the
1432        // live-body macro DDL (`create_macro`), `CREATE OR REPLACE TABLE`
1433        // (`create_or_replace_table`), the `CREATE [PERSISTENT] SECRET` statement
1434        // (`create_secret`), and the `CREATE`/`DROP TYPE` user-defined-type DDL
1435        // (`create_type`), all of which PostgreSQL lacks. Every other field is inherited
1436        // verbatim (forcing the four off recovers PG).
1437        assert!(duck.statement_ddl_gates.create_macro);
1438        assert!(duck.statement_ddl_gates.create_or_replace_table);
1439        assert!(duck.statement_ddl_gates.create_secret);
1440        assert!(duck.statement_ddl_gates.create_type);
1441        assert!(!pg.statement_ddl_gates.create_macro);
1442        assert!(!pg.statement_ddl_gates.create_or_replace_table);
1443        assert!(!pg.statement_ddl_gates.create_secret);
1444        assert!(!pg.statement_ddl_gates.create_type);
1445        // DuckDB matches the PostgreSQL schema-change surface except for the four
1446        // DuckDB-specific create forms above, the PostgreSQL-only `b_expr` column-default
1447        // restriction (DuckDB reads a full `a_expr` default), PostgreSQL-only declarative
1448        // partitioning, the two PostgreSQL-only legacy CREATE TABLE clauses (`INHERITS` and the
1449        // `(LIKE …)` element), and the four PostgreSQL-only residue clauses (column
1450        // STORAGE/COMPRESSION, the USING access method, WITHOUT OIDS, typed `OF <type>` tables);
1451        // DuckDB *does* share the column `COLLATE` and `UNLOGGED` surfaces. Forcing all twelve
1452        // divergent flags to PostgreSQL's values makes the rest equal.
1453        assert!(!duck.column_definition_syntax.column_default_requires_b_expr);
1454        assert!(pg.column_definition_syntax.column_default_requires_b_expr);
1455        assert!(!duck.create_table_clause_syntax.declarative_partitioning);
1456        assert!(pg.create_table_clause_syntax.declarative_partitioning);
1457        assert!(!duck.create_table_clause_syntax.table_inheritance);
1458        assert!(pg.create_table_clause_syntax.table_inheritance);
1459        assert!(!duck.create_table_clause_syntax.like_source_table);
1460        assert!(pg.create_table_clause_syntax.like_source_table);
1461        assert!(duck.column_definition_syntax.column_collation);
1462        assert!(duck.create_table_clause_syntax.unlogged_tables);
1463        assert!(!duck.column_definition_syntax.column_storage);
1464        assert!(pg.column_definition_syntax.column_storage);
1465        assert!(!duck.create_table_clause_syntax.table_access_method);
1466        assert!(pg.create_table_clause_syntax.table_access_method);
1467        assert!(!duck.create_table_clause_syntax.without_oids);
1468        assert!(pg.create_table_clause_syntax.without_oids);
1469        assert!(!duck.create_table_clause_syntax.typed_tables);
1470        assert!(pg.create_table_clause_syntax.typed_tables);
1471        // DuckDB also lacks PostgreSQL's SQL-standard embedded schema-element form
1472        // (`schema_elements`): DuckDB's `CREATE SCHEMA` takes no inline `CREATE TABLE`/…
1473        // children, so recovering PG forces that flag back on alongside the three
1474        // DuckDB-specific create forms.
1475        assert!(!duck.statement_ddl_gates.schema_elements);
1476        assert!(pg.statement_ddl_gates.schema_elements);
1477        assert!(!duck.statement_ddl_gates.databases);
1478        assert!(pg.statement_ddl_gates.databases);
1479        // DuckDB manages extensions with `INSTALL`/`LOAD`, not the PostgreSQL
1480        // `CREATE`/`ALTER EXTENSION` catalogue DDL, so recovering PG forces this on.
1481        assert!(!duck.statement_ddl_gates.extension_ddl);
1482        assert!(pg.statement_ddl_gates.extension_ddl);
1483        // DuckDB has no transform catalogue (`CREATE`/`DROP TRANSFORM` is PostgreSQL-only),
1484        // so recovering PG forces this on.
1485        assert!(!duck.statement_ddl_gates.transform_ddl);
1486        assert!(pg.statement_ddl_gates.transform_ddl);
1487        // DuckDB configures through `SET`/`RESET`, not `ALTER SYSTEM`, so recovering PG
1488        // forces this on.
1489        assert!(!duck.statement_ddl_gates.alter_system);
1490        assert!(pg.statement_ddl_gates.alter_system);
1491        assert_eq!(
1492            StatementDdlGates {
1493                create_macro: false,
1494                create_secret: false,
1495                create_type: false,
1496                // DuckDB adds `CREATE OR REPLACE TABLE`; PostgreSQL is gated off here.
1497                create_or_replace_table: false,
1498                schema_elements: true,
1499                // DuckDB adds `CREATE RECURSIVE VIEW`; PostgreSQL is gated off here.
1500                // DuckDB has no `CREATE DATABASE` (uses ATTACH); PostgreSQL admits it.
1501                databases: true,
1502                // DuckDB has no PostgreSQL-style extension catalogue DDL.
1503                extension_ddl: true,
1504                // DuckDB has no PostgreSQL transform catalogue (`DROP TRANSFORM`).
1505                transform_ddl: true,
1506                // DuckDB has no `ALTER SYSTEM` server-configuration DDL.
1507                alter_system: true,
1508                // PostgreSQL accepts the `CACHE` sequence option.
1509                // MySQL's tablespace / logfile-group storage DDL is not a DuckDB statement.
1510                tablespace_ddl: false,
1511                logfile_group_ddl: false,
1512                // DuckDB adds `ALTER DATABASE … SET ALIAS TO`, the `ALTER SEQUENCE …` option
1513                // list, and `ALTER … SET SCHEMA`; PostgreSQL is gated off here (no-shadowing).
1514                alter_database: false,
1515                alter_sequence: false,
1516                alter_object_set_schema: false,
1517                ..duck.statement_ddl_gates
1518            },
1519            pg.statement_ddl_gates,
1520        );
1521        assert_eq!(
1522            CreateTableClauseSyntax {
1523                declarative_partitioning: true,
1524                table_inheritance: true,
1525                like_source_table: true,
1526                table_access_method: true,
1527                without_oids: true,
1528                typed_tables: true,
1529                create_table_as_execute: true,
1530                ..duck.create_table_clause_syntax
1531            },
1532            pg.create_table_clause_syntax,
1533        );
1534        assert_eq!(
1535            ColumnDefinitionSyntax {
1536                column_default_requires_b_expr: true,
1537                column_storage: true,
1538                // DuckDB turns the keywordless generated-column shorthand and the
1539                // type-optional generated column on (both off in PostgreSQL).
1540                generated_column_shorthand: false,
1541                typeless_generated_columns: false,
1542                ..duck.column_definition_syntax
1543            },
1544            pg.column_definition_syntax,
1545        );
1546        assert!(!duck.constraint_syntax.referential_action_cascade_set);
1547        assert!(pg.constraint_syntax.referential_action_cascade_set);
1548        assert!(!duck.constraint_syntax.check_constraint_subqueries);
1549        assert!(pg.constraint_syntax.check_constraint_subqueries);
1550        assert_eq!(
1551            ConstraintSyntax {
1552                exclusion_constraints: true,
1553                index_constraint_parameters: true,
1554                referential_action_cascade_set: true,
1555                check_constraint_subqueries: true,
1556                ..duck.constraint_syntax
1557            },
1558            pg.constraint_syntax,
1559        );
1560        assert!(!duck.index_alter_syntax.alter_table_multiple_actions);
1561        assert!(pg.index_alter_syntax.alter_table_multiple_actions);
1562        assert!(duck.index_alter_syntax.alter_nested_column_paths);
1563        assert!(!pg.index_alter_syntax.alter_nested_column_paths);
1564        assert_eq!(
1565            IndexAlterSyntax {
1566                alter_table_multiple_actions: true,
1567                alter_nested_column_paths: false,
1568                ..duck.index_alter_syntax
1569            },
1570            pg.index_alter_syntax,
1571        );
1572        assert_eq!(duck.target_spelling, pg.target_spelling);
1573        assert_eq!(duck.identifier_casing, pg.identifier_casing);
1574    }
1575
1576    #[test]
1577    fn duckdb_reranks_the_arrow_and_double_equals_tokens() {
1578        use crate::precedence::Side;
1579
1580        // The lambda arrow binds below `OR` (the loosest binary operator), while
1581        // `->>`, containment, and everything else keep PostgreSQL's ranks — the
1582        // engine-measured split (`x -> x OR y` puts the OR in the body; `a ->> 'k' =
1583        // 5` compares the extraction).
1584        let duck = DUCKDB_BINDING_POWERS;
1585        let arrow = duck.binary(&BinaryOperator::JsonGet);
1586        assert!(arrow.left < duck.or.left, "`->` is looser than OR");
1587        assert_eq!(arrow.assoc, Assoc::Left, "`x -> y -> z` groups left");
1588        // `==` is the second reranked token: DuckDB lexes it as a generic `%left Op`, not
1589        // the `%nonassoc '='` comparison, so it sits at the `any_operator` rank (tighter
1590        // than the comparisons, looser than additive, left-associative). Its `=` sibling
1591        // (`Eq(Single)`) and every other operator keep PostgreSQL's ranks.
1592        let double_eq = duck.binary(&BinaryOperator::Eq(EqualsSpelling::Double));
1593        assert_eq!(
1594            double_eq, duck.any_operator,
1595            "`==` rides the generic-Op rank"
1596        );
1597        assert_eq!(double_eq.assoc, Assoc::Left, "`a == b == c` groups left");
1598        assert!(
1599            duck.comparison.left < double_eq.left,
1600            "`==` binds tighter than the comparisons"
1601        );
1602        assert!(
1603            double_eq.left < duck.additive.left,
1604            "`==` binds looser than additive"
1605        );
1606        for untouched in [
1607            BinaryOperator::Eq(EqualsSpelling::Single),
1608            BinaryOperator::JsonGetText,
1609            BinaryOperator::Contains,
1610            BinaryOperator::ContainedBy,
1611        ] {
1612            assert_eq!(
1613                duck.binary(&untouched),
1614                STANDARD_BINDING_POWERS.binary(&untouched),
1615            );
1616        }
1617        // The grouping consequence, at the table level: an `=` right operand of `->`
1618        // needs no parentheses (it binds tighter), so `x -> x % 2 = 0` keeps the
1619        // whole comparison in the body — where PostgreSQL's table demands the split.
1620        assert!(!crate::precedence::needs_parens_between(
1621            arrow,
1622            duck.comparison,
1623            Side::Right,
1624        ));
1625    }
1626
1627    #[test]
1628    fn duckdb_reserves_qualify_in_every_identifier_position() {
1629        // DuckDB's `duckdb_keywords()` classes QUALIFY `reserved` (like HAVING): every
1630        // per-position reject set names it, each strictly widening its shared base —
1631        // and the shared base itself must NOT contain it, or the "only DuckDB reserves
1632        // it" story (and PostgreSQL/ANSI identifier behaviour) silently breaks.
1633        for (duck_set, shared) in [
1634            (DUCKDB_RESERVED_COLUMN_NAME, RESERVED_COLUMN_NAME),
1635            (DUCKDB_RESERVED_FUNCTION_NAME, RESERVED_FUNCTION_NAME),
1636            (DUCKDB_RESERVED_TYPE_NAME, RESERVED_TYPE_NAME),
1637            (DUCKDB_RESERVED_BARE_ALIAS, RESERVED_BARE_ALIAS),
1638        ] {
1639            assert!(duck_set.contains(Keyword::Qualify));
1640            assert!(!shared.contains(Keyword::Qualify));
1641        }
1642        // The type set carries exactly the QUALIFY and PIVOT/UNPIVOT deltas (all
1643        // `reserved` class) and nothing else — the join words stay out (`CAST(1 AS
1644        // asof)`/`CAST(1 AS semi)` both parse). The function set carries those *plus*
1645        // SEMI/ANTI, whose grammar rejects `semi(1)` even though `duckdb_keywords()`
1646        // classes them `type_function` (the `ASOF`/`POSITIONAL` join words stay out —
1647        // `asof(1)` parses).
1648        assert_eq!(
1649            DUCKDB_RESERVED_FUNCTION_NAME,
1650            RESERVED_FUNCTION_NAME
1651                .difference(DUCKDB_UNRESERVED_CARVEOUT)
1652                .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
1653                .union(DUCKDB_QUALIFY_RESERVATION)
1654                .union(DUCKDB_PIVOT_RESERVATION)
1655                .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION)
1656        );
1657        assert_eq!(
1658            DUCKDB_RESERVED_TYPE_NAME,
1659            RESERVED_TYPE_NAME
1660                .difference(DUCKDB_UNRESERVED_CARVEOUT)
1661                .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
1662                .union(DUCKDB_QUALIFY_RESERVATION)
1663                .union(DUCKDB_PIVOT_RESERVATION)
1664        );
1665    }
1666
1667    #[test]
1668    fn duckdb_carves_its_unreserved_words_out_of_postgres_reservation() {
1669        for keyword in [Keyword::Grant, Keyword::User] {
1670            assert!(DUCKDB_UNRESERVED_CARVEOUT.contains(keyword));
1671            assert!(!DUCKDB_RESERVED_COLUMN_NAME.contains(keyword));
1672            assert!(!DUCKDB_RESERVED_FUNCTION_NAME.contains(keyword));
1673            assert!(!DUCKDB_RESERVED_TYPE_NAME.contains(keyword));
1674            assert!(!DUCKDB_RESERVED_SET_VALUE_WORDS.contains(keyword));
1675            assert!(DUCKDB_RESERVED_BARE_ALIAS.contains(keyword));
1676        }
1677    }
1678
1679    #[test]
1680    fn duckdb_special_value_names_are_ordinary_identifiers() {
1681        for keyword in [
1682            Keyword::CurrentCatalog,
1683            Keyword::CurrentDate,
1684            Keyword::CurrentRole,
1685            Keyword::CurrentSchema,
1686            Keyword::CurrentTime,
1687            Keyword::CurrentTimestamp,
1688            Keyword::CurrentUser,
1689            Keyword::Localtime,
1690            Keyword::Localtimestamp,
1691            Keyword::SessionUser,
1692            Keyword::SystemUser,
1693        ] {
1694            assert!(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES.contains(keyword));
1695            assert!(!DUCKDB_RESERVED_COLUMN_NAME.contains(keyword));
1696            assert!(!DUCKDB_RESERVED_FUNCTION_NAME.contains(keyword));
1697            assert!(!DUCKDB_RESERVED_TYPE_NAME.contains(keyword));
1698            assert!(!DUCKDB_RESERVED_BARE_ALIAS.contains(keyword));
1699            assert!(!DUCKDB_RESERVED_SET_VALUE_WORDS.contains(keyword));
1700        }
1701    }
1702
1703    #[test]
1704    fn duckdb_reserves_pivot_and_unpivot_in_every_identifier_position() {
1705        // DuckDB's `duckdb_keywords()` classes PIVOT/UNPIVOT `reserved` (like QUALIFY):
1706        // every per-position reject set names them (probed on 1.5.4), each strictly
1707        // widening its shared base — and the shared base must NOT contain them, or the
1708        // "only DuckDB reserves them" story (and PostgreSQL/ANSI identifier behaviour)
1709        // silently breaks.
1710        for kw in [Keyword::Pivot, Keyword::Unpivot] {
1711            for (duck_set, shared) in [
1712                (DUCKDB_RESERVED_COLUMN_NAME, RESERVED_COLUMN_NAME),
1713                (DUCKDB_RESERVED_FUNCTION_NAME, RESERVED_FUNCTION_NAME),
1714                (DUCKDB_RESERVED_TYPE_NAME, RESERVED_TYPE_NAME),
1715                (DUCKDB_RESERVED_BARE_ALIAS, RESERVED_BARE_ALIAS),
1716            ] {
1717                assert!(duck_set.contains(kw));
1718                assert!(!shared.contains(kw));
1719            }
1720        }
1721    }
1722
1723    #[test]
1724    fn duckdb_reserves_the_join_words_as_colid_and_bare_alias_only() {
1725        // `asof`/`positional` are `duckdb_keywords()` class `type_function` (like
1726        // `CROSS`): rejected as a column/table name and bare alias, admitted as a
1727        // function name, type name, and `AS` label. The set composition mirrors that
1728        // probed profile exactly, and the shared bases must stay free of both words
1729        // (every other dialect keeps them plain identifiers).
1730        for kw in [Keyword::Asof, Keyword::Positional] {
1731            assert!(DUCKDB_RESERVED_COLUMN_NAME.contains(kw));
1732            assert!(DUCKDB_RESERVED_BARE_ALIAS.contains(kw));
1733            assert!(!DUCKDB_RESERVED_FUNCTION_NAME.contains(kw));
1734            assert!(!DUCKDB_RESERVED_TYPE_NAME.contains(kw));
1735            for shared in [
1736                RESERVED_COLUMN_NAME,
1737                RESERVED_FUNCTION_NAME,
1738                RESERVED_TYPE_NAME,
1739                RESERVED_BARE_ALIAS,
1740            ] {
1741                assert!(!shared.contains(kw));
1742            }
1743        }
1744        assert_eq!(
1745            DUCKDB_RESERVED_COLUMN_NAME,
1746            RESERVED_COLUMN_NAME
1747                .difference(DUCKDB_UNRESERVED_CARVEOUT)
1748                .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
1749                .union(DUCKDB_QUALIFY_RESERVATION)
1750                .union(DUCKDB_PIVOT_RESERVATION)
1751                .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
1752                .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION)
1753        );
1754        assert_eq!(
1755            DUCKDB_RESERVED_BARE_ALIAS,
1756            RESERVED_BARE_ALIAS
1757                .difference(DUCKDB_ORDINARY_SPECIAL_VALUE_NAMES)
1758                .union(DUCKDB_UNRESERVED_BARE_ALIAS_RESERVATION)
1759                .union(DUCKDB_QUALIFY_RESERVATION)
1760                .union(DUCKDB_PIVOT_RESERVATION)
1761                .union(DUCKDB_NONSTANDARD_JOIN_RESERVATION)
1762                .union(DUCKDB_SEMI_ANTI_JOIN_RESERVATION)
1763        );
1764    }
1765
1766    #[test]
1767    fn duckdb_reserves_semi_and_anti_as_colid_bare_alias_and_function() {
1768        // `semi`/`anti` are `duckdb_keywords()` class `type_function` like the join
1769        // words, but the DuckDB grammar reserves them one position further: rejected as
1770        // a column/table name, a bare alias, *and a function name* (`semi(1)`
1771        // syntax-errors where `asof(1)` parses), while the type position and `AS` labels
1772        // still admit them (`CAST(1 AS semi)`/`SELECT 1 AS semi` parse). The set
1773        // composition mirrors that probed profile exactly, and the shared bases stay
1774        // free of both words.
1775        for kw in [Keyword::Semi, Keyword::Anti] {
1776            assert!(DUCKDB_RESERVED_COLUMN_NAME.contains(kw));
1777            assert!(DUCKDB_RESERVED_BARE_ALIAS.contains(kw));
1778            assert!(DUCKDB_RESERVED_FUNCTION_NAME.contains(kw));
1779            assert!(!DUCKDB_RESERVED_TYPE_NAME.contains(kw));
1780            for shared in [
1781                RESERVED_COLUMN_NAME,
1782                RESERVED_FUNCTION_NAME,
1783                RESERVED_TYPE_NAME,
1784                RESERVED_BARE_ALIAS,
1785            ] {
1786                assert!(!shared.contains(kw));
1787            }
1788        }
1789    }
1790
1791    #[test]
1792    fn duckdb_expression_deltas_are_additive_over_postgres() {
1793        // The delta is exactly the collection-literal and `#n` positional-column flags
1794        // (ExpressionSyntax), the lambda flag (OperatorSyntax), and the COLUMNS(…) flag
1795        // (CallSyntax); the whole PostgreSQL surface (subscript, ARRAY[…], `::`, …) is kept.
1796        // The lambda gate additionally depends on the inherited JSON-arrow lexing (`->`
1797        // must tokenize for the lambda grammar position to ever fire), so pin that
1798        // inheritance here too. Bind to locals so the const field reads are not flagged by
1799        // clippy's `assertions_on_constants`.
1800        let (duck_expr, pg_expr) = (ExpressionSyntax::DUCKDB, ExpressionSyntax::POSTGRES);
1801        let (duck_op, pg_op) = (OperatorSyntax::DUCKDB, OperatorSyntax::POSTGRES);
1802        let (duck_call, pg_call) = (CallSyntax::DUCKDB, CallSyntax::POSTGRES);
1803        let (duck_sf, pg_sf) = (StringFuncForms::DUCKDB, StringFuncForms::POSTGRES);
1804        let (duck_ag, pg_ag) = (AggregateCallSyntax::DUCKDB, AggregateCallSyntax::POSTGRES);
1805        assert!(duck_expr.collection_literals);
1806        assert!(!pg_expr.collection_literals);
1807        assert!(duck_expr.positional_column);
1808        assert!(!pg_expr.positional_column);
1809        assert!(duck_expr.lambda_keyword);
1810        assert!(!pg_expr.lambda_keyword);
1811        assert!(duck_expr.relaxed_interval_syntax);
1812        assert!(!pg_expr.relaxed_interval_syntax);
1813        assert!(duck_op.lambda_expressions);
1814        assert!(!pg_op.lambda_expressions);
1815        assert!(duck_op.double_equals);
1816        assert!(!pg_op.double_equals);
1817        assert!(duck_op.integer_divide_slash);
1818        assert!(!pg_op.integer_divide_slash);
1819        assert!(duck_call.columns_expression);
1820        assert!(!pg_call.columns_expression);
1821        assert!(duck_call.try_cast);
1822        assert!(!pg_call.try_cast);
1823        assert!(
1824            duck_op.json_arrow_operators,
1825            "lambda `->` rides the JSON-arrow lexeme"
1826        );
1827        // `field_wildcard` is a *subtractive* delta: DuckDB parses `(struct).field`
1828        // (field_selection, inherited) but has no `.*` value-expansion production, so it
1829        // vacates PostgreSQL's `true` (engine-probed 1.5.4).
1830        assert!(pg_expr.field_wildcard);
1831        assert!(!duck_expr.field_wildcard);
1832        // `multidim_array_literals` is a *subtractive* delta too: DuckDB nests `ARRAY[[1,2]]`
1833        // through `collection_literals` (a top-level `[…]` list is a value there and levels
1834        // may mix), so it vacates PostgreSQL's `true` for the multidim `array_expr` production.
1835        assert!(pg_expr.multidim_array_literals);
1836        assert!(!duck_expr.multidim_array_literals);
1837        assert_eq!(
1838            duck_expr,
1839            ExpressionSyntax {
1840                collection_literals: true,
1841                slice_step: true,
1842                positional_column: true,
1843                lambda_keyword: true,
1844                relaxed_interval_syntax: true,
1845                field_wildcard: false,
1846                multidim_array_literals: false,
1847                ..pg_expr
1848            },
1849        );
1850        // The PostgreSQL `jsonb` operators are a *subtractive* delta: DuckDB spells `?` as the
1851        // anonymous placeholder, which claims the same trigger as the `jsonb` `?` operators
1852        // (`LexicalConflict::JsonbKeyExistsVersusAnonymousParameter`), so DuckDB vacates the
1853        // whole family to stay lexically consistent — unlike the additive deltas above.
1854        assert!(pg_op.jsonb_operators);
1855        assert!(!duck_op.jsonb_operators);
1856        // `caret_operator` (top-level FeatureSet dimension) is SHARED with PostgreSQL (probed
1857        // identical on DuckDB 1.5.4 — see the preset comment): DuckDB's `^` is power at the
1858        // same precedence row, so both presets read `CaretOperator::Exponent`.
1859        // `custom_operators` is SHARED with PostgreSQL: DuckDB inherits its generalized
1860        // maximal-munch operator lexer and parse-accepts the same `Op`-class runs (bind-rejecting
1861        // the ones with no backing function). The one lexical divergence — DuckDB drops `#`/`?`
1862        // from the `Op` charset (positional-column and parameter sigils) — is carried by the
1863        // shared `is_operator_char` gate, not a separate flag. See the preset comment and
1864        // `duckdb-pg-operator-spelling-under-acceptance` for the probe.
1865        assert!(pg_op.custom_operators);
1866        assert!(duck_op.custom_operators);
1867        assert_eq!(FeatureSet::DUCKDB.caret_operator, CaretOperator::Exponent);
1868        assert_eq!(
1869            FeatureSet::DUCKDB.caret_operator,
1870            FeatureSet::POSTGRES.caret_operator
1871        );
1872        // The any-operator quantifier (`3 * ANY(list)`) is a *subtractive* delta: it is a
1873        // PostgreSQL extension, not a DuckDB construct, so DuckDB vacates PostgreSQL's `true`.
1874        assert!(pg_op.quantified_arbitrary_operator);
1875        assert!(!duck_op.quantified_arbitrary_operator);
1876        // Postfix symbolic operators (`10!`) are an *additive* delta: DuckDB keeps the postfix
1877        // reading PostgreSQL removed in 14, so it arms PostgreSQL's `false`
1878        // (`duckdb-postfix-operator-dimension`).
1879        assert!(!pg_op.postfix_operators);
1880        assert!(duck_op.postfix_operators);
1881        assert_eq!(
1882            duck_op,
1883            OperatorSyntax {
1884                lambda_expressions: true,
1885                double_equals: true,
1886                integer_divide_slash: true,
1887                starts_with_operator: true,
1888                jsonb_operators: false,
1889                quantified_arbitrary_operator: false,
1890                postfix_operators: true,
1891                ..pg_op
1892            },
1893        );
1894        // The PG-only SQL/JSON empty-constructor reject is a subtractive delta: DuckDB
1895        // deliberately keeps `json()` a plain call (unprobed surface, documented at the
1896        // preset).
1897        assert!(pg_call.sqljson_constructors_require_argument);
1898        assert!(!duck_call.sqljson_constructors_require_argument);
1899        // The SQL/JSON expression functions are a subtractive delta too: DuckDB has no
1900        // SQL:2016 special forms (only ordinary JSON functions), so it vacates PostgreSQL's
1901        // `true` to keep the keyword heads plain call/name forms.
1902        assert!(pg_call.sqljson_expression_functions);
1903        assert!(!duck_call.sqljson_expression_functions);
1904        // The SQL/XML expression functions are a subtractive delta too: DuckDB has no
1905        // SQL/XML special forms, so it vacates PostgreSQL's `true` here as well.
1906        assert!(pg_call.xml_expression_functions);
1907        assert!(!duck_call.xml_expression_functions);
1908        // The string special forms diverge in exactly two probed knobs: the SIMILAR
1909        // regex substring is dropped from DuckDB's PG-fork grammar, and OVERLAY kept
1910        // only the PLACING production (no plain-call fallback).
1911        assert!(pg_sf.substring_similar);
1912        assert!(!duck_sf.substring_similar);
1913        assert!(!pg_sf.overlay_requires_placing);
1914        assert!(duck_sf.overlay_requires_placing);
1915        assert_eq!(
1916            duck_call,
1917            CallSyntax {
1918                columns_expression: true,
1919                try_cast: true,
1920                extract_string_field: true,
1921                method_chaining: true,
1922                sqljson_constructors_require_argument: false,
1923                sqljson_expression_functions: false,
1924                xml_expression_functions: false,
1925                merge_action_function: false,
1926                ..pg_call
1927            },
1928        );
1929        assert_eq!(
1930            duck_ag,
1931            AggregateCallSyntax {
1932                null_treatment: true,
1933                standalone_argument_order_by: true,
1934                filter_optional_where: true,
1935                ..pg_ag
1936            },
1937        );
1938        assert_eq!(
1939            duck_sf,
1940            StringFuncForms {
1941                substring_similar: false,
1942                overlay_requires_placing: true,
1943                collation_for_expression: false,
1944                ..pg_sf
1945            },
1946        );
1947    }
1948
1949    #[test]
1950    fn duckdb_numeric_surface_relaxes_postgres_trailing_junk_reject() {
1951        // DuckDB and PostgreSQL now share the *same* radix/separator surface (both model
1952        // PG 14+ `0x`/`0o`/`0b` and `_` grouping) — the delta is the strictness knob:
1953        // PostgreSQL rejects trailing junk after a number, DuckDB (probed) lexes it
1954        // loosely and accepts. Bound to locals so the field reads are runtime asserts.
1955        let (duck, pg) = (NumericLiteralSyntax::DUCKDB, NumericLiteralSyntax::POSTGRES);
1956        assert!(duck.hex_integers && duck.octal_integers && duck.binary_integers);
1957        assert!(duck.underscore_separators);
1958        assert!(!duck.money_literals);
1959        // The radix/separator forms are identical; only the reject differs.
1960        assert_eq!(duck.hex_integers, pg.hex_integers);
1961        assert_eq!(duck.octal_integers, pg.octal_integers);
1962        assert_eq!(duck.binary_integers, pg.binary_integers);
1963        assert_eq!(duck.underscore_separators, pg.underscore_separators);
1964        assert!(pg.reject_trailing_junk && !duck.reject_trailing_junk);
1965    }
1966
1967    #[test]
1968    fn duckdb_select_surface_is_postgres_modulo_the_documented_deltas() {
1969        // Subtractive deltas (`empty_target_list`, and the `locking_clauses` family —
1970        // `locking_clauses` / `key_lock_strengths` / `stacked_locking_clauses` — since
1971        // DuckDB has no row locking) and additive clauses (`qualify`, the `GROUP BY ALL` /
1972        // `ORDER BY ALL` modes, `UNION [ALL] BY NAME`, and the FROM-first SELECT order);
1973        // the rest of the SELECT surface is PostgreSQL's (DISTINCT ON, FETCH FIRST,
1974        // SELECT INTO, …).
1975        let (duck, pg) = (SelectSyntax::DUCKDB, SelectSyntax::POSTGRES);
1976        let (duck_g, pg_g) = (GroupingSyntax::DUCKDB, GroupingSyntax::POSTGRES);
1977        let (duck_q, pg_q) = (QueryTailSyntax::DUCKDB, QueryTailSyntax::POSTGRES);
1978        assert!(!duck.empty_target_list);
1979        assert!(pg.empty_target_list);
1980        assert!(duck.qualify);
1981        assert!(!pg.qualify);
1982        assert!(duck_g.group_by_all && duck_g.order_by_all);
1983        assert!(!pg_g.group_by_all && !pg_g.order_by_all);
1984        assert!(duck.from_first);
1985        assert!(!pg.from_first);
1986        assert!(!duck_q.locking_clauses);
1987        assert!(pg_q.locking_clauses);
1988        assert!(!duck_q.key_lock_strengths && !duck_q.stacked_locking_clauses);
1989        assert!(pg_q.key_lock_strengths && pg_q.stacked_locking_clauses);
1990        assert!(duck.union_by_name);
1991        assert!(!pg.union_by_name);
1992        assert!(duck.wildcard_modifiers);
1993        assert!(!pg.wildcard_modifiers);
1994        assert!(duck.values_rows_require_equal_arity);
1995        assert!(!pg.values_rows_require_equal_arity);
1996        assert!(duck_q.limit_percent);
1997        assert!(!pg_q.limit_percent);
1998        assert!(duck.alias_string_literals);
1999        assert!(!pg.alias_string_literals);
2000        assert_eq!(
2001            duck,
2002            SelectSyntax {
2003                empty_target_list: false,
2004                qualify: true,
2005                from_first: true,
2006                explicit_table: true,
2007                union_by_name: true,
2008                wildcard_modifiers: true,
2009                values_rows_require_equal_arity: true,
2010                alias_string_literals: true,
2011                bare_alias_string_literals: false,
2012                trailing_comma: true,
2013                // DuckDB's prefix colon alias — additive over the PostgreSQL base.
2014                prefix_colon_alias: true,
2015                ..pg
2016            },
2017        );
2018        assert_eq!(
2019            duck_g,
2020            GroupingSyntax {
2021                group_by_all: true,
2022                // PostgreSQL admits the `GROUP BY {DISTINCT | ALL} <items>` grouping-set
2023                // quantifier; DuckDB does not (its `ALL` is the standalone mode above) —
2024                // a subtractive delta.
2025                group_by_set_quantifier: false,
2026                order_by_all: true,
2027                ..pg_g
2028            },
2029        );
2030        assert_eq!(
2031            duck_q,
2032            QueryTailSyntax {
2033                locking_clauses: false,
2034                key_lock_strengths: false,
2035                stacked_locking_clauses: false,
2036                using_sample: true,
2037                limit_percent: true,
2038                // PG-only raw-parse `WITH TIES` guards — a subtractive delta (DuckDB's own
2039                // `WITH TIES` validity is unprobed, documented at the preset).
2040                with_ties_requires_order_by: false,
2041                ..pg_q
2042            },
2043        );
2044    }
2045    #[test]
2046    fn duckdb_closed_delta_axes_vs_postgres() {
2047        crate::dialect::closed_delta::assert_closed_delta(
2048            &FeatureSet::POSTGRES,
2049            &FeatureSet::DUCKDB,
2050            &[
2051                "reserved_column_name",
2052                "reserved_function_name",
2053                "reserved_type_name",
2054                "reserved_bare_alias",
2055                "byte_classes",
2056                "binding_powers",
2057                "numeric_literals",
2058                "parameters",
2059                "identifier_syntax",
2060                "table_expressions",
2061                "join_syntax",
2062                "table_factor_syntax",
2063                "expression_syntax",
2064                "operator_syntax",
2065                "call_syntax",
2066                "string_func_forms",
2067                "aggregate_call_syntax",
2068                "predicate_syntax",
2069                "double_ampersand",
2070                "keyword_operators",
2071                "hash_bitwise_xor",
2072                "mutation_syntax",
2073                "statement_ddl_gates",
2074                "view_sequence_clause_syntax",
2075                "create_table_clause_syntax",
2076                "column_definition_syntax",
2077                "constraint_syntax",
2078                "index_alter_syntax",
2079                "select_syntax",
2080                "query_tail_syntax",
2081                "grouping_syntax",
2082                "utility_syntax",
2083                "transaction_syntax",
2084                "show_syntax",
2085                "maintenance_syntax",
2086                "type_name_syntax",
2087            ],
2088        );
2089    }
2090}