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}