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