Skip to main content

squonk_ast/dialect/
postgres.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! The PostgreSQL dialect preset.
5//!
6//! The module is self-contained for feature gating: a build without the `postgres`
7//! cargo feature compiles none of this preset's data.
8
9use super::{
10    AccessControlSyntax, AggregateCallSyntax, CallSyntax, CaretOperator, Casing,
11    ColumnDefinitionSyntax, CommentSyntax, ConstraintSyntax, CreateTableClauseSyntax,
12    DoubleAmpersand, ExistenceGuards, ExpressionSyntax, FeatureSet, GroupingSyntax,
13    IdentifierSyntax, IndexAlterSyntax, JoinSyntax, KeywordOperators, KeywordSet,
14    MaintenanceSyntax, MutationSyntax, NullOrdering, NumericLiteralSyntax, OperatorSyntax,
15    POSTGRES_BYTE_CLASSES, ParameterSyntax, PipeOperator, PredicateSyntax, QueryTailSyntax,
16    RESERVED_BARE_ALIAS, RESERVED_COLUMN_NAME, RESERVED_FUNCTION_NAME, RESERVED_TYPE_NAME,
17    STANDARD_IDENTIFIER_QUOTES, SelectSyntax, SessionVariableSyntax, ShowSyntax, StatementDdlGates,
18    StringFuncForms, StringLiteralSyntax, TableExpressionSyntax, TableFactorSyntax, TargetSpelling,
19    TypeNameSyntax, UtilitySyntax,
20};
21use crate::precedence::{
22    BindingPowerTable, IS_PREDICATE_BELOW_COMPARISON, RANGE_PREDICATE_ABOVE_COMPARISON,
23    STANDARD_BINDING_POWERS, STANDARD_SET_OPERATION_BINDING_POWERS,
24};
25
26impl CommentSyntax {
27    /// The `POSTGRES` preset for comment syntax: the ANSI baseline with line comments
28    /// additionally terminated by a carriage return.
29    ///
30    /// PostgreSQL's flex scanner defines a `--` line comment as `("--"{non_newline}*)`
31    /// with `non_newline [^\n\r]`, so the comment ends at the first `\n` *or* `\r`
32    /// (engine-verified against libpg_query: `SELECT 1 -- c\rFROM` reads `FROM` as a live
33    /// token and rejects, where the `\n`-only reading accepts). DuckDB shares this
34    /// PostgreSQL-derived scanner, so it adopts this preset too — the only two shipped
35    /// dialects whose line comments end at a bare `\r` (SQLite/MySQL keep the `\n`-only
36    /// `CommentSyntax::ANSI`/`::MYSQL` reading). Everything else matches `ANSI`.
37    pub const POSTGRES: Self = Self {
38        line_comment_ends_at_carriage_return: true,
39        ..Self::ANSI
40    };
41}
42
43impl StringLiteralSyntax {
44    /// The `POSTGRES` preset for string literal syntax.
45    pub const POSTGRES: Self = Self {
46        escape_strings: true,
47        dollar_quoted_strings: true,
48        // PostgreSQL has NO `N'…'` national-string constant: its scanner rewrites a
49        // quote-adjacent `[nN]'` to the identifier `nchar` plus a separate string, so
50        // `N'x'` is the typed literal `nchar 'x'` and `DO LANGUAGE N'p'` reads `nchar` as
51        // the language name (engine-probed against pg_query 6.1.1). Arming a national-string
52        // token here mislexed both — it read `N'x'` as one string constant (a different
53        // shape from PG's typed cast) and rejected `DO LANGUAGE N'p'` outright. With the flag
54        // off, `N` lexes as an ordinary word, so `N'x'` folds into the generalized typed
55        // literal `N '…'` (an `Expr::Cast`) — matching PG's accept/reject in every position.
56        // (We do not replicate PG's source-discarding `N`→`nchar` substitution: it would
57        // break span round-trip, and the residual is a type-name-spelling shape nicety, not
58        // an accept/reject divergence.)
59        national_strings: false,
60        double_quoted_strings: false,
61        backslash_escapes: false,
62        unicode_strings: true,
63        bit_string_literals: true,
64        // PostgreSQL's `X'…'` is a deferred *bit-string* (odd-length hex allowed), not
65        // the eager even-byte blob — so this stays off and `bit_string_literals` owns the
66        // `x`/`X` marker.
67        blob_literals: false,
68        charset_introducers: false,
69        // PostgreSQL requires a newline in the separator between adjacent literals.
70        same_line_adjacent_concat: false,
71    };
72}
73
74impl NumericLiteralSyntax {
75    /// The `POSTGRES` preset for numeric literal syntax.
76    pub const POSTGRES: Self = Self {
77        hex_integers: true,
78        octal_integers: true,
79        binary_integers: true,
80        underscore_separators: true,
81        // PG's `0[xX](_?{hexdigit})+` admits a `_` ahead of the first radix digit
82        // (`0x_1F`, oracle-probed); SQLite does not, so this is its own axis.
83        radix_leading_underscore: true,
84        // `$` is a parameter or dollar-quote opener here, not a T-SQL money sigil.
85        money_literals: false,
86        // PG's scanner treats a numeric literal as a maximal-munch lexeme and errors on
87        // trailing junk (`123abc`, `0x`, `100_`), which only distinguishes the malformed
88        // forms from valid radix forms once those forms are recognised.
89        reject_trailing_junk: true,
90    };
91}
92
93impl ParameterSyntax {
94    /// The `POSTGRES` preset for parameter syntax.
95    pub const POSTGRES: Self = Self {
96        positional_dollar: true,
97        anonymous_question: false,
98        named_colon: false,
99        named_at: false,
100        // SQLite's `$name`; PostgreSQL spells `$` as positional/`$tag$`, never a
101        // dollar-named parameter.
102        named_dollar: false,
103        numbered_question: false,
104    };
105}
106
107impl IdentifierSyntax {
108    /// The `POSTGRES` preset for identifier syntax.
109    pub const POSTGRES: Self = Self {
110        // `$` is an identifier-continue byte, not an identifier-start byte, so `a$b` is
111        // one word while a leading `$1` still dispatches to the parameter scanner.
112        dollar_in_identifiers: true,
113        // PostgreSQL syntax-rejects a string literal in a name position — `DELETE FROM 't'`
114        // is a parse error — so the SQLite string-identifier misfeature stays off.
115        string_literal_identifiers: false,
116        empty_quoted_identifiers: false,
117    };
118}
119
120impl TableExpressionSyntax {
121    /// The `POSTGRES` preset for table expression syntax.
122    pub const POSTGRES: Self = Self {
123        only: true,
124        table_sample: true,
125        parenthesized_joins: true,
126        table_alias_column_lists: true,
127        join_using_alias: true,
128        // Index hints and `PARTITION (…)` selection are MySQL-only table-factor tails.
129        index_hints: false,
130        // MSSQL-only `WITH (...)` table hints; under PostgreSQL `WITH` stays CTE-only.
131        table_hints: false,
132        partition_selection: false,
133        base_table_alias_column_lists: true,
134        // DuckDB-only string-literal table alias (`FROM t AS 't'('k')`).
135        string_literal_aliases: false,
136        aliased_parenthesized_join: true,
137        // PostgreSQL's bare table alias is a `ColId` (`alias_clause: [AS] ColId …`), not the
138        // SQLite `ids` class, so the JOIN keywords stay reserved as a ColId either way.
139        bare_table_alias_is_bare_label: false,
140        // No table version / time-travel modifier under PostgreSQL.
141        table_version: false,
142        // No PartiQL / SUPER table-position JSON path under PostgreSQL.
143        table_json_path: false,
144        // No SQLite `INDEXED BY` / `NOT INDEXED` index directive under PostgreSQL.
145        indexed_by: false,
146    };
147}
148
149impl JoinSyntax {
150    /// The `POSTGRES` preset for join syntax.
151    pub const POSTGRES: Self = Self {
152        stacked_join_qualifiers: true,
153        full_outer_join: true,
154        // PostgreSQL parse-rejects `NATURAL CROSS JOIN` (engine-probed on 16).
155        natural_cross_join: false,
156        // PostgreSQL has no `STRAIGHT_JOIN`; the word is a non-reserved identifier there.
157        straight_join: false,
158        // The DuckDB nonstandard joins; `asof`/`positional` are plain identifiers here.
159        asof_join: false,
160        positional_join: false,
161        // `SEMI`/`ANTI` are plain identifiers here too.
162        semi_anti_join: false,
163        sided_semi_anti_join: false,
164        apply_join: false,
165        // PostgreSQL/SQL:2023 recursive-query SEARCH/CYCLE clauses on a CTE.
166        recursive_search_cycle: true,
167        // PostgreSQL parse-accepts ORDER BY/LIMIT/OFFSET on a recursive CTE's UNION body
168        // (engine-probed on pg_query 17); the recursion restriction is a binder-layer check.
169        recursive_union_rejects_order_limit: false,
170        // `USING KEY` is DuckDB's keyed-recursion clause; PostgreSQL has no such spelling.
171        recursive_using_key: false,
172    };
173}
174
175impl TableFactorSyntax {
176    /// The `POSTGRES` preset for table factor syntax.
177    pub const POSTGRES: Self = Self {
178        lateral: true,
179        table_functions: true,
180        rows_from: true,
181        // `FROM unnest(array[…]) [WITH ORDINALITY] [AS u(…)]` — engine-probed accept.
182        // `WITH OFFSET` is BigQuery-only (PostgreSQL parse-rejects it), so off here.
183        unnest: true,
184        unnest_with_offset: false,
185        table_function_ordinality: true,
186        // PostgreSQL admits `FROM current_date` and an alias on a parenthesized join.
187        special_function_table_source: true,
188        // PIVOT/UNPIVOT are DuckDB-only operators.
189        pivot: false,
190        unpivot: false,
191        // DuckDB-only DESCRIBE/SHOW/SUMMARIZE table source.
192        show_ref: false,
193        // DuckDB-only bare `FROM VALUES (…) AS t` row-list table factor.
194        from_values: false,
195        // SQL/JSON JSON_TABLE and SQL/XML XMLTABLE table factors — engine-verified.
196        json_table: true,
197        xml_table: true,
198        // `TABLE(<expr>)` is a Snowflake/Oracle form PostgreSQL parse-rejects
199        // (engine-probed); off here.
200        table_expr_factor: false,
201        // The standard PIVOT is a Snowflake/BigQuery/Oracle form PostgreSQL has no
202        // grammar for; off here (and inherited off by the DuckDB preset).
203        pivot_value_sources: false,
204        // MATCH_RECOGNIZE is a Snowflake/Oracle form PostgreSQL has no grammar for; off
205        // here (and inherited off by the DuckDB preset).
206        match_recognize: false,
207        // OPENJSON is a SQL Server form PostgreSQL has no grammar for; off here (and
208        // inherited off by the DuckDB preset).
209        open_json: false,
210    };
211}
212
213impl MutationSyntax {
214    /// The `POSTGRES` preset for mutation syntax.
215    pub const POSTGRES: Self = Self {
216        returning: true,
217        on_conflict: true,
218        on_duplicate_key_update: false,
219        multi_column_assignment: true,
220        update_tuple_value_row_arity: false,
221        where_current_of: true,
222        // PostgreSQL 15+ implements the standard `MERGE` statement.
223        merge: true,
224        // The MySQL `REPLACE` statement and `INSERT ... SET` source are not PostgreSQL.
225        replace_into: false,
226        insert_set: false,
227        // PostgreSQL row-limits neither `UPDATE` nor `DELETE`, so the MySQL
228        // `ORDER BY ... LIMIT` tails are rejected.
229        update_delete_tails: false,
230        // PostgreSQL spells conflict resolution `ON CONFLICT`, not the SQLite verb-level
231        // `INSERT OR`/`UPDATE OR <action>` prefix, so it is rejected.
232        or_conflict_action: false,
233        insert_column_matching: false,
234        delete_using: true,
235        update_from: true,
236        // PostgreSQL admits an alias on a `DELETE … USING` target and a leading `WITH`
237        // before `INSERT` and (15+) before `MERGE`.
238        delete_using_target_alias: true,
239        cte_before_insert: true,
240        cte_before_merge: true,
241        // Data-modifying CTEs (`WITH t AS (DELETE … RETURNING *) SELECT …`) — the
242        // PostgreSQL `PreparableStmt` CTE-body extension, admitted at every `WITH`
243        // site during raw parsing (probed on pg_query 17).
244        data_modifying_ctes: true,
245        // MERGE residual grammar (pg-merge-residual-roots): `WHEN NOT MATCHED BY
246        // SOURCE/TARGET` (PG 17), `INSERT DEFAULT VALUES`, and the `OVERRIDING` merge
247        // insert override — all accepted (probed on pg_query 17).
248        merge_when_not_matched_by: true,
249        merge_insert_default_values: true,
250        merge_insert_overriding: true,
251        merge_update_set_star: false,
252        merge_insert_star_by_name: false,
253        merge_error_action: false,
254        update_set_qualified_column: true,
255    };
256}
257
258impl StatementDdlGates {
259    /// The `POSTGRES` preset for statement ddl gates.
260    pub const POSTGRES: Self = Self {
261        // PostgreSQL's `CREATE TRIGGER` uses an `EXECUTE FUNCTION` body, not the
262        // modelled SQLite `BEGIN … END` form, so it is not dispatched here.
263        create_trigger: false,
264        // The macro DDL is DuckDB-specific; PostgreSQL's `CREATE FUNCTION` is the
265        // string-body routine gated by `routines`, not a live-body macro.
266        create_macro: false,
267        create_secret: false,
268        create_type: false,
269        // Virtual tables are SQLite-only; PostgreSQL rejects `CREATE VIRTUAL TABLE`. DuckDB
270        // inherits this `false` through the POSTGRES spread.
271        create_virtual_table: false,
272        // PostgreSQL and DuckDB both have the SQL:2003 T176 sequence generator; DuckDB
273        // inherits this `true` through the POSTGRES spread.
274        create_sequence: true,
275        extension_ddl: true,
276        transform_ddl: true,
277        // PostgreSQL is the only shipped dialect with `ALTER SYSTEM` server-configuration
278        // DDL; DuckDB clears this `true` back to `false` through the POSTGRES spread.
279        alter_system: true,
280        // MySQL's tablespace / logfile-group storage DDL is not a PostgreSQL statement
281        // (PostgreSQL's `CREATE TABLESPACE` is a different, location-based grammar, not modelled
282        // here).
283        tablespace_ddl: false,
284        logfile_group_ddl: false,
285        schemas: true,
286        // PostgreSQL is the reference for the SQL-standard embedded schema-element form
287        // (`CREATE SCHEMA s CREATE TABLE t ...`); DuckDB overrides this back to `false`
288        // through the POSTGRES spread, as it rejects the embedding.
289        schema_elements: true,
290        databases: true,
291        // PostgreSQL's `DROP DATABASE` is its own single-name form (unmodelled here) and its
292        // `DROP SCHEMA` is the shared name-list drop, not the MySQL DATABASE/SCHEMA synonym.
293        drop_database: false,
294        materialized_views: true,
295        temporary_views: true,
296        routines: true,
297        or_replace: true,
298        // `CREATE RECURSIVE VIEW` is gated to DuckDB/Lenient (measured on the DuckDB
299        // corpus); although PostgreSQL spells the same form, it is not widened here
300        // without a differential, and DuckDB overrides this `false` to `true` through
301        // the POSTGRES spread.
302        recursive_views: false,
303        // PostgreSQL routine bodies are opaque `$$…$$`/string definitions, not the
304        // MySQL SQL/PSM compound statement.
305        compound_statements: false,
306        // PostgreSQL has these `ALTER` forms, but they stay gated to the measured dialect
307        // (DuckDB) per the no-shadowing doctrine until a PostgreSQL differential lands.
308        alter_database: false,
309        alter_database_options: false,
310        server_definition: false,
311        alter_instance: false,
312        spatial_reference_system: false,
313        resource_group: false,
314        alter_sequence: false,
315        alter_object_set_schema: false,
316        view_definition_options: false,
317    };
318}
319
320impl CreateTableClauseSyntax {
321    /// The `POSTGRES` preset for create table clause syntax.
322    pub const POSTGRES: Self = Self {
323        table_options: false,
324        // PostgreSQL has no SQLite trailing `WITHOUT ROWID` table option; the trailing
325        // `WITHOUT ROWID` is rejected as leftover input.
326        without_rowid_table_option: false,
327        // PostgreSQL has no SQLite trailing `STRICT` table option; the trailing `STRICT` is
328        // rejected as leftover input. (DuckDB inherits this `false` via the POSTGRES spread.)
329        strict_table_option: false,
330        // `OR REPLACE TABLE` and `CREATE SECRET` are DuckDB-specific.
331        create_or_replace_table: false,
332        storage_parameters: true,
333        on_commit: true,
334        create_table_as_with_data: true,
335        create_table_as_execute: true,
336        // PostgreSQL owns declarative partitioning (PARTITION BY / PARTITION OF / ATTACH …).
337        declarative_partitioning: true,
338        // PostgreSQL owns the legacy `INHERITS (parents)` clause and the `(LIKE src …)` element.
339        table_inheritance: true,
340        like_source_table: true,
341        // No statement-level `CREATE TABLE t LIKE src`: PostgreSQL rejects the bare form at raw
342        // parse and reads `(LIKE src …)` only as the element above (MySQL's distinct surface).
343        statement_level_table_like: false,
344        unlogged_tables: true,
345        table_access_method: true,
346        without_oids: true,
347        typed_tables: true,
348    };
349}
350
351impl ColumnDefinitionSyntax {
352    /// The `POSTGRES` preset for column definition syntax.
353    pub const POSTGRES: Self = Self {
354        // PostgreSQL's generated columns require the `GENERATED ALWAYS` keywords, and it
355        // has none of the SQLite `CREATE TABLE` decorations, so both are rejected.
356        generated_column_shorthand: false,
357        // PostgreSQL has no SQLite column-level `ON CONFLICT <resolution>` clause; the
358        // trailing `ON` after a `UNIQUE`/`PRIMARY KEY`/… constraint is rejected.
359        column_conflict_resolution_clause: false,
360        // PostgreSQL requires a data type on every column; a typeless column is a SQLite
361        // extension it rejects.
362        typeless_column_definitions: false,
363        // PostgreSQL requires a data type even on a generated column (`GENERATED ALWAYS AS
364        // (expr) STORED` still names a type); DuckDB's type-optional generated column is a
365        // dialect extension, so this must clear rather than be inherited by the DuckDB spread.
366        typeless_generated_columns: false,
367        // PostgreSQL has no joined `AUTOINCREMENT` attribute (its auto-increment is `serial`
368        // types / `GENERATED … AS IDENTITY`); the trailing keyword is rejected. DuckDB inherits
369        // this `false` through its POSTGRES spread.
370        joined_autoincrement_attribute: false,
371        // PostgreSQL's inline `PRIMARY KEY` takes no `ASC`/`DESC` order qualifier (ordering is a
372        // per-index-column property, not an inline column constraint); the trailing keyword is
373        // rejected. DuckDB inherits this `false` through its POSTGRES spread.
374        inline_primary_key_ordering: false,
375        // PostgreSQL accepts a bare column `COLLATE` but rejects a `CONSTRAINT <name>` prefix on it
376        // (there `COLLATE any_name` is a constraint alternative parallel to the nameable one);
377        // engine-measured reject. DuckDB inherits this `false` through its POSTGRES spread.
378        named_column_collate_constraint: false,
379        identity_columns: true,
380        // PostgreSQL accepts a bare expression default and a `CONSTRAINT <name>` on any
381        // inline column constraint.
382        default_expression_requires_parens: false,
383        // PostgreSQL parses a column-constraint `DEFAULT` as the restricted `b_expr`.
384        column_default_requires_b_expr: true,
385        // The CREATE TABLE residue surfaces are all PostgreSQL-native: per-column COLLATE,
386        // UNLOGGED persistence, column STORAGE/COMPRESSION, the USING access method, legacy
387        // WITHOUT OIDS, and typed `OF <type>` tables.
388        column_collation: true,
389        column_storage: true,
390    };
391}
392
393impl ConstraintSyntax {
394    /// The `POSTGRES` preset for constraint syntax.
395    pub const POSTGRES: Self = Self {
396        deferrable_constraints: true,
397        named_inline_non_check_constraints: true,
398        // PostgreSQL requires a constraint element after `CONSTRAINT <name>` — a bodyless
399        // `CONSTRAINT <name>` is engine-measured rejected.
400        bare_constraint_name: false,
401        exclusion_constraints: true,
402        constraint_no_inherit_not_valid: true,
403        index_constraint_parameters: true,
404        // PostgreSQL's PRIMARY KEY / UNIQUE table constraint takes a bare column-name list;
405        // COLLATE / opclass / ASC / DESC live in its CREATE INDEX grammar, not here
406        // (engine-measured: `PRIMARY KEY (a COLLATE "C")` / `UNIQUE (a DESC)` are syntax errors).
407        constraint_column_collate_order: false,
408        referential_action_cascade_set: true,
409        check_constraint_subqueries: true,
410    };
411}
412
413impl IndexAlterSyntax {
414    /// The `POSTGRES` preset for index alter syntax.
415    pub const POSTGRES: Self = Self {
416        drop_behavior: true,
417        // PostgreSQL's `DROP INDEX` is the shared name-list drop, not the MySQL `ON <table>` form.
418        index_drop_on_table: false,
419        index_concurrently: true,
420        index_using_method: true,
421        partial_index: true,
422        index_if_not_exists: true,
423        index_nulls_order: true,
424        alter_table_extended: true,
425        alter_nested_column_paths: false,
426        alter_existence_guards: true,
427        alter_column_set_data_type: true,
428        routine_arg_types: true,
429        routine_arg_defaults: true,
430        routine_arg_modes: true,
431        // PostgreSQL's `LANGUAGE` operand is `NonReservedWord_or_Sconst`: `LANGUAGE 'sql'`,
432        // `E'sql'`, and `$$sql$$` are accepted alongside the bare word (pg_query-measured).
433        routine_language_string: true,
434        alter_table_multiple_actions: true,
435    };
436}
437
438impl ExistenceGuards {
439    /// The `POSTGRES` preset for existence guards.
440    pub const POSTGRES: Self = Self {
441        if_exists: true,
442        view_if_not_exists: false,
443        create_database_if_not_exists: false,
444    };
445}
446
447impl SelectSyntax {
448    /// The `POSTGRES` preset for select syntax.
449    pub const POSTGRES: Self = Self {
450        distinct_on: true,
451        select_into: true,
452        // libpg_query's raw grammar accepts an empty target list (`SELECT`,
453        // `SELECT FROM t`); parse-level parity accepts it under the PostgreSQL preset.
454        empty_target_list: true,
455        // PostgreSQL has no `QUALIFY` clause (a DuckDB extension); `qualify` stays a
456        // free identifier there.
457        qualify: false,
458        // PostgreSQL requires an identifier alias; a string literal there is a syntax error.
459        alias_string_literals: false,
460        bare_alias_string_literals: false,
461        // `UNION [ALL] BY NAME` is a DuckDB extension; PostgreSQL has no name-matched
462        // set operation, so `BY` after a set operator is a syntax error there.
463        union_by_name: false,
464        wildcard_modifiers: false,
465        // PostgreSQL parses `t.*` as an ordinary columnref, so it takes the standard
466        // `[AS] alias` projection alias (`SELECT t.* x` / `SELECT t.* AS x`); libpg_query-measured.
467        qualified_wildcard_alias: true,
468        // FROM-first SELECT is a DuckDB extension; PostgreSQL rejects a statement-position
469        // `FROM` (`FROM t SELECT x` is a syntax error there — a required over-accept guard).
470        from_first: false,
471        parenthesized_query_operands: true,
472        // libpg_query's raw grammar accepts a ragged VALUES constructor (`VALUES (1,2),(3)`);
473        // PostgreSQL defers the equal-length check to parse-analysis, past parse-level
474        // parity, so the preset accepts it at parse (measured: `pg_query::parse` accepts).
475        values_rows_require_equal_arity: false,
476        // PostgreSQL's query-position VALUES constructor uses bare-parenthesized rows.
477        values_row_constructor: true,
478        // PostgreSQL's projection `AS` alias is a `ColLabel` admitting reserved words.
479        as_alias_rejects_reserved: false,
480        // A trailing comma in a list is a DuckDB tolerance; PostgreSQL rejects it.
481        trailing_comma: false,
482        // The prefix colon alias is a DuckDB extension (DuckDB overrides this to `true`);
483        // a `:` at a select-item / table-factor head is a parse error in PostgreSQL.
484        prefix_colon_alias: false,
485        // Hive/Spark `LATERAL VIEW` is not PostgreSQL (DuckDB inherits this value);
486        // PostgreSQL's LATERAL is the derived-table factor, and a post-FROM `LATERAL`
487        // is a parse error.
488        lateral_view_clause: false,
489        // The Oracle-style `START WITH`/`CONNECT BY` hierarchical query clause is not
490        // PostgreSQL (DuckDB inherits this value); PostgreSQL uses recursive CTEs, so a
491        // post-WHERE `CONNECT BY`/`START WITH` is a parse error.
492        connect_by_clause: false,
493    };
494}
495
496impl QueryTailSyntax {
497    /// The `POSTGRES` preset for query tail syntax.
498    pub const POSTGRES: Self = Self {
499        fetch_first: true,
500        limit_offset_comma: false,
501        // PostgreSQL's `FOR UPDATE/SHARE [OF …] [NOWAIT|SKIP LOCKED]` locking clause.
502        locking_clauses: true,
503        // PostgreSQL's `FOR NO KEY UPDATE` / `FOR KEY SHARE` strengths and its stacked
504        // clauses (`FOR UPDATE OF a FOR SHARE OF b`) — engine-verified accepts
505        // (pg-locking-clause-strengths-and-stacking); multi-table `OF` is the shared
506        // core the `locking_clauses` gate already reaches.
507        key_lock_strengths: true,
508        stacked_locking_clauses: true,
509        using_sample: false,
510        leading_offset: true,
511        limit_expressions: true,
512        limit_percent: false,
513        // PostgreSQL's `gram.y` `WITH TIES` guards (requires `ORDER BY`, excludes
514        // `SKIP LOCKED`), raised during raw parsing.
515        with_ties_requires_order_by: true,
516        // BigQuery/ZetaSQL `|>` pipe syntax is not PostgreSQL; off here (DuckDB inherits
517        // this value). A `|>` after a query is a parse error, and the token never lexes.
518        pipe_syntax: false,
519        // ClickHouse `LIMIT n BY …` is not PostgreSQL (DuckDB inherits this value); a
520        // `BY` after `LIMIT` is a parse error.
521        limit_by_clause: false,
522        // ClickHouse `SETTINGS …` is not PostgreSQL (DuckDB inherits this value); a
523        // trailing `SETTINGS` is a parse error.
524        settings_clause: false,
525        // ClickHouse `FORMAT …` is not PostgreSQL (DuckDB inherits this value); a trailing
526        // `FORMAT` is a parse error.
527        format_clause: false,
528        // MSSQL `FOR XML`/`FOR JSON` is not PostgreSQL (DuckDB inherits this value); a
529        // trailing `FOR XML`/`FOR JSON` is a parse error.
530        for_xml_json_clause: false,
531    };
532}
533
534impl GroupingSyntax {
535    /// The `POSTGRES` preset for grouping syntax.
536    pub const POSTGRES: Self = Self {
537        grouping_sets: true,
538        // The trailing `WITH ROLLUP` is a MySQL-only spelling; PostgreSQL writes the
539        // super-aggregate as the `ROLLUP (…)` grouping set above.
540        with_rollup: false,
541        // PostgreSQL's operator-driven `ORDER BY <expr> USING <operator>` sort form.
542        order_by_using: true,
543        // `GROUP BY ALL` / `ORDER BY ALL` are DuckDB clause modes; PostgreSQL reserves
544        // `ALL`, so either spelling is a syntax error there.
545        group_by_all: false,
546        // But PostgreSQL DOES admit `GROUP BY {DISTINCT | ALL} <items>` — the SQL:2016
547        // grouping-set quantifier, a modifier on a non-empty item list (not a mode).
548        group_by_set_quantifier: true,
549        order_by_all: false,
550    };
551}
552
553impl UtilitySyntax {
554    /// The `POSTGRES` preset for utility syntax.
555    pub const POSTGRES: Self = Self {
556        copy: true,
557        // PostgreSQL's `COPY` is the `{FROM | TO}` transfer (the `copy` gate above);
558        // Snowflake's `COPY INTO` load/unload is a different statement PostgreSQL has no
559        // form of, so this stays off (a `COPY INTO` surfaces as an unknown statement).
560        copy_into: false,
561        stage_references: false,
562        comment_on: true,
563        pragma: false,
564        attach: false,
565        // `KILL` and the MySQL `DESCRIBE`/`DESC` synonyms + table-metadata overload are
566        // MySQL-only, so off here — `EXPLAIN` keeps its plain query-plan grammar.
567        kill: false,
568        // MySQL's `HANDLER` cursor family is not a PostgreSQL statement.
569        handler_statements: false,
570        // MySQL's `INSTALL`/`UNINSTALL` `PLUGIN`/`COMPONENT` family is not a PostgreSQL statement.
571        plugin_component_statements: false,
572        // MySQL's server-administration families are not PostgreSQL statements.
573        shutdown: false,
574        restart: false,
575        clone: false,
576        import_table: false,
577        help_statement: false,
578        binlog: false,
579        // MySQL's `CACHE INDEX` / `LOAD INDEX INTO CACHE` key-cache pair is MyISAM-specific,
580        // not a PostgreSQL statement.
581        key_cache_statements: false,
582        // DuckDB (PostgreSQL-derived) is the one dialect with a `USE` statement; the
583        // PostgreSQL base itself has none, so its leading `USE` stays undispatched.
584        use_statement: false,
585        // Moot: `use_statement` is off, so the name-arity refinement is unreachable.
586        use_qualified_name: false,
587        // PostgreSQL has `PREPARE`/`EXECUTE`/`DEALLOCATE` too (`CALL` is a distinct
588        // statement PostgreSQL also has, but tracked by its own `call` flag below, off
589        // here — a separate, unfitted grammar ticket).
590        prepared_statements: true,
591        // PostgreSQL's own `PREPARE name ( <type> [, ...] ) AS ...` parenthesized
592        // parameter-type list — a widening of the `prepared_statements` name position,
593        // full type names including parameterized (`numeric(10,2)`) and arrayed
594        // (`int[]`) forms, at least one, an empty `()` rejected (`planner-parity-
595        // prepare-typed-parameters`, pg_query 6.1.1-verified).
596        prepare_typed_parameters: true,
597        // MySQL's `PREPARE ... FROM {'text' | @var}` lifecycle is a different grammar on
598        // the same keywords; PostgreSQL keeps its typed-`AS` `prepared_statements` above
599        // (`PREPARE p FROM 'x'` is a pg_query syntax error), so this stays off.
600        prepared_statements_from: false,
601        call: false,
602        // `call` is off here (a separate, unfitted grammar ticket), so its MySQL bare-name
603        // widening is moot and off too.
604        call_bare_name: false,
605        load_extension: true,
606        load_bare_name: false,
607        load_data: false,
608        reset_scope: false,
609        detach_if_exists: false,
610        // PostgreSQL's `DO [LANGUAGE <lang>] '<body>'` anonymous code block (pg_query
611        // PG-17 accepts). No other shipped fitted preset has it.
612        do_statement: true,
613        // MySQL's `DO <expr-list>` is a different behaviour on the `DO` keyword; PostgreSQL
614        // keeps its anonymous-code-block reading, so this stays off.
615        do_expression_list: false,
616        // PostgreSQL's own `LOCK [TABLE] <rel>, … [IN <mode> MODE]` is a *different
617        // behaviour* on the `LOCK` keyword (a statement-level mode list, not per-table lock
618        // kinds) and is not yet modelled — when it is, it takes its own gate; MySQL's
619        // reading stays off here. The backup-lock pair is MySQL-only.
620        lock_tables: false,
621        lock_instance: false,
622        // PostgreSQL's own `BEGIN` modifier vocabulary is `ISOLATION LEVEL …`/`READ
623        // ONLY|WRITE`/`[NOT] DEFERRABLE` (the existing `TransactionMode` list), not
624        // SQLite's `DEFERRED`/`IMMEDIATE`/`EXCLUSIVE` keywords (pg_query PG-17 rejects all
625        // three), so this stays off.
626        begin_transaction_mode: false,
627        // MySQL's `XA` distributed-transaction family is MySQL-only; PostgreSQL has no `XA`
628        // statement, so the leading `XA` keyword is not dispatched.
629        xa_transactions: false,
630        // The standalone `RENAME TABLE`/`RENAME USER` statements are MySQL-only; PostgreSQL
631        // renames objects via `ALTER … RENAME TO`, so the leading `RENAME` is not dispatched.
632        rename_statement: false,
633        signal_diagnostics: false,
634        // `EXPORT`/`IMPORT DATABASE` are DuckDB-specific; PostgreSQL has no such statements,
635        // so the leading keywords stay undispatched. DuckDB turns the pair on over its
636        // PostgreSQL base.
637        export_import_database: false,
638        // `UPDATE EXTENSIONS` is DuckDB extension management; PostgreSQL has no such statement,
639        // so the `EXTENSIONS` lookahead is never taken and every `UPDATE` reaches the DML
640        // parser. DuckDB turns it on over its PostgreSQL base.
641        update_extensions: false,
642        // MySQL's `FLUSH` / `PURGE BINARY LOGS` server-administration statements — PostgreSQL
643        // (and DuckDB over its base) has neither, so both leading-keyword gates stay off.
644        flush: false,
645        purge_binary_logs: false,
646        replication_statements: false,
647    };
648}
649
650impl ShowSyntax {
651    /// The `POSTGRES` preset for show syntax.
652    pub const POSTGRES: Self = Self {
653        describe: false,
654        // DuckDB (PostgreSQL-derived) turns this on; PostgreSQL has no `DESCRIBE`/`SUMMARIZE`.
655        describe_summarize: false,
656        session_statements: true,
657        show_tables: false,
658        show_columns: false,
659        show_create_table: false,
660        show_functions: false,
661        show_routine_status: false,
662        show_verbose: false,
663        show_admin: false,
664    };
665}
666
667impl MaintenanceSyntax {
668    /// The `POSTGRES` preset for maintenance syntax.
669    pub const POSTGRES: Self = Self {
670        // PostgreSQL has its own (differently-shaped) `VACUUM`/`REINDEX`/`ANALYZE`
671        // maintenance statements, but only SQLite's forms are modelled, so they stay
672        // off here and a leading `VACUUM`/`REINDEX`/`ANALYZE` surfaces as unknown.
673        vacuum: false,
674        vacuum_analyze: false,
675        reindex: false,
676        analyze: false,
677        analyze_columns: false,
678        // PostgreSQL has the bare `CHECKPOINT` and the `LOAD '<library>'` statement (both
679        // pg_query PG-17 accepts). The DuckDB operand/argument/scope extensions are not
680        // PostgreSQL: `FORCE CHECKPOINT`/`CHECKPOINT db`, a bare-identifier `LOAD`, a
681        // `RESET` scope prefix, and `DETACH … IF EXISTS` are all pg_query parser errors.
682        checkpoint: true,
683        checkpoint_database: false,
684        // The MySQL admin-table verbs are MySQL-only; PostgreSQL's `ANALYZE` is its own
685        // (unmodelled) form and there is no `CHECK/CHECKSUM/OPTIMIZE/REPAIR TABLE`.
686        table_maintenance: false,
687    };
688}
689
690impl AccessControlSyntax {
691    /// The `POSTGRES` preset for access control syntax.
692    pub const POSTGRES: Self = Self {
693        access_control: true,
694        // PostgreSQL admits the schema-scoped grant objects (`ON SCHEMA`, `ON ALL … IN
695        // SCHEMA`) and the `{GRANT|ADMIN} OPTION FOR` `REVOKE` prefix.
696        access_control_extended_objects: true,
697        // PostgreSQL's `CREATE ROLE`/`CREATE USER` is its own (unmodelled) grammar, not the
698        // MySQL account-management family, so this MySQL-shaped surface is off.
699        user_role_management: false,
700        // PostgreSQL uses the typed-object/role-spec grant grammar, not MySQL's account-based route.
701        access_control_account_grants: false,
702    };
703}
704
705impl TypeNameSyntax {
706    /// The `POSTGRES` preset for type name syntax.
707    pub const POSTGRES: Self = Self {
708        signed_type_modifier: true,
709        ..Self::ANSI
710    };
711}
712
713impl ExpressionSyntax {
714    /// The `POSTGRES` preset for expression syntax.
715    pub const POSTGRES: Self = Self {
716        typecast_operator: true,
717        subscript: true,
718        // PostgreSQL slices are two-bound only; the three-bound `[lower:upper:step]` is
719        // DuckDB's, so a `base[a:b:c]` here is a clean parse error at the second `:`.
720        slice_step: false,
721        collate: true,
722        at_time_zone: true,
723        semi_structured_access: false,
724        array_constructor: true,
725        // PostgreSQL's multidimensional array literals — the bare-bracket sub-row inside
726        // `ARRAY[...]` (`ARRAY[[1,2],[3,4]]`). DuckDB overrides the POSTGRES spread to
727        // `false` (it reaches nested lists through `collection_literals` instead).
728        multidim_array_literals: true,
729        // The `[…]`/`{…}`/`MAP` collection literals are DuckDB's; PostgreSQL spells
730        // arrays with the `ARRAY` keyword and rows with `ROW(...)`.
731        collection_literals: false,
732        row_constructor: true,
733        // BigQuery's `STRUCT(...)` value constructor is a dialect extension; PostgreSQL
734        // keeps `struct(...)` an ordinary function call.
735        struct_constructor: false,
736        field_selection: true,
737        field_wildcard: true,
738        // PostgreSQL admits the temporal + generalized typed literals. DuckDB inherits
739        // these via its `..ExpressionSyntax::POSTGRES` spread.
740        typed_string_literals: true,
741        // The PostgreSQL prefix-typed interval literal (`INTERVAL '1' HOUR TO SECOND`,
742        // and the unit-less `INTERVAL '1'` whose fields default from the string). DuckDB
743        // and Lenient inherit `true`; MySQL overrides to `false` (no interval literal).
744        typed_interval_literal: true,
745        // The relaxed interval spellings are DuckDB's; PostgreSQL admits only the
746        // standard quoted `INTERVAL '1' DAY`. DuckDB overrides this to `true`.
747        relaxed_interval_syntax: false,
748        mysql_interval_operator: false,
749        // The `#n` positional column is DuckDB's; PostgreSQL spells `#` bitwise-XOR
750        // (`hash_bitwise_xor: true`), so it stays off here. DuckDB overrides it to `true`.
751        positional_column: false,
752        lambda_keyword: false,
753    };
754}
755
756impl OperatorSyntax {
757    /// The `POSTGRES` preset for operator syntax.
758    pub const POSTGRES: Self = Self {
759        operator_construct: true,
760        containment_operators: true,
761        json_arrow_operators: true,
762        // The `jsonb` existence/path/search operators `?`/`?|`/`?&`/`@?`/`@@`/`#>`/`#>>`/`#-`
763        // (planner-parity-pg-json-op-*). PostgreSQL enables the whole family; it has no `?`
764        // parameter, so the `?`-led members do not contend with a placeholder here.
765        jsonb_operators: true,
766        // SQLite-only equality surface; PostgreSQL has neither `==` nor general `IS`.
767        double_equals: false,
768        // DuckDB-only `//` spelling; PostgreSQL spells only `/`.
769        integer_divide_slash: false,
770        starts_with_operator: false,
771        is_general_equality: false,
772        // Truth-value tests are standard SQL (F571); libpg_query accepts all six forms.
773        truth_value_tests: true,
774        // `<=>` is MySQL-only.
775        null_safe_equals: false,
776        // The single-arrow lambda is DuckDB-only: PostgreSQL's `->` is always the
777        // JSON accessor, so `x -> x + 1` stays a `JsonGet` binary op here.
778        lambda_expressions: false,
779        // PostgreSQL accepts the bitwise `| & ~ << >>` operators (engine-measured via
780        // libpg_query). Bitwise XOR is its `#` spelling, carried by `hash_bitwise_xor` below.
781        bitwise_operators: true,
782        quantified_comparisons: true,
783        quantified_comparison_lists: true,
784        // PostgreSQL quantifies any operator except the boolean keywords `AND`/`OR`
785        // (engine-probed via libpg_query: `3 * ANY('{1,2,3}')` parses).
786        quantified_arbitrary_operator: true,
787        // PostgreSQL's general symbolic-operator surface — regex `~`/`!~`/`~*`/`!~*`,
788        // geometric/network/text-search ops, negator spellings, and prefix/user-defined
789        // operators, all lexed by the maximal-munch `Op`-class rule (libpg_query-measured).
790        custom_operators: true,
791        null_test_postfix: true,
792        // PostgreSQL removed postfix operators in version 14 (`SELECT 10!` now errors), so the
793        // postfix reduction stays off — the general operator surface is infix/prefix only here.
794        postfix_operators: false,
795    };
796}
797
798impl CallSyntax {
799    /// The `POSTGRES` preset for call syntax.
800    pub const POSTGRES: Self = Self {
801        named_argument: true,
802        // The UTC_* niladic functions are MySQL-only.
803        utc_special_functions: false,
804        columns_expression: false,
805        extract_from_syntax: true,
806        try_cast: false,
807        // PostgreSQL `CAST` admits any type name as its target.
808        restricted_cast_targets: false,
809        // PostgreSQL admits a single-quoted string as the `EXTRACT(<field> FROM x)` field
810        // (`Sconst` in gram.y `extract_arg`), engine-verified against pg_query alongside its
811        // reject boundary (a non-string non-identifier field). The remaining DuckDB-specific
812        // call tails below stay off.
813        extract_string_field: true,
814        method_chaining: false,
815        // PostgreSQL's SQL/JSON constructors `JSON()`/`JSON_SCALAR()`/`JSON_SERIALIZE()`
816        // require their context-item argument (dedicated `gram.y` productions).
817        sqljson_constructors_require_argument: true,
818        // The SQL:2016 SQL/JSON expression functions (JSON_VALUE/JSON_QUERY/JSON_EXISTS,
819        // the JSON_OBJECT/JSON_ARRAY constructors + aggregates, JSON()/JSON_SCALAR()/
820        // JSON_SERIALIZE(), and the IS JSON predicate) are PostgreSQL grammar.
821        sqljson_expression_functions: true,
822        // The SQL:2006 SQL/XML expression functions (xmlelement/xmlforest/xmlconcat/
823        // xmlparse/xmlpi/xmlroot/xmlserialize/xmlexists) and the IS DOCUMENT predicate are
824        // PostgreSQL grammar (func_expr_common_subexpr).
825        xml_expression_functions: true,
826        variadic_argument: true,
827        // PostgreSQL's `merge_action()` MERGE-RETURNING support function (dedicated
828        // `MERGE_ACTION '(' ')'` production, raw-parse-accepted anywhere).
829        merge_action_function: true,
830        convert_function: false,
831    };
832}
833
834impl StringFuncForms {
835    /// The `POSTGRES` preset for string func forms.
836    pub const POSTGRES: Self = Self {
837        // The standard-SQL string special forms (the planner-parity-expr-substring/
838        // position/overlay/trim bundle), engine-verified against pg_query (PG 17):
839        // the full substring surface including the FOR-leading orders and the
840        // SIMILAR/ESCAPE regex form, symmetric b_expr POSITION operands, OVERLAY
841        // with its plain-call fallback (`overlay('a')` parse-accepts — arity is a
842        // catalog concern, so the plain-call arity floor stays off, and `substr`
843        // stays an ordinary catalog function with no keyword form), and the loose
844        // trim_list tails.
845        substring_from_for: true,
846        substring_leading_for: true,
847        substring_similar: true,
848        substring_plain_call_requires_2_or_3_args: false,
849        substr_from_for: false,
850        position_in: true,
851        position_asymmetric_operands: false,
852        overlay_placing: true,
853        overlay_requires_placing: false,
854        trim_from: true,
855        trim_list_syntax: true,
856        // PostgreSQL's `COLLATION FOR (<expr>)` common-subexpr (dedicated
857        // `COLLATION FOR '(' a_expr ')'` production).
858        collation_for_expression: true,
859        // PostgreSQL's `ceil`/`ceiling` are plain functions — no `TO <field>` grammar
860        // (`pg_query`-verified: `CEIL(x TO DAY)` is a syntax error at `TO`).
861        ceil_to_field: false,
862        // PostgreSQL's `floor` is a plain function — no `TO <field>` grammar
863        // (`pg_query`-verified: `FLOOR(x TO DAY)` is a syntax error at `TO`).
864        floor_to_field: false,
865        match_against: false,
866    };
867}
868
869impl AggregateCallSyntax {
870    /// The `POSTGRES` preset for aggregate call syntax.
871    pub const POSTGRES: Self = Self {
872        // The `GROUP_CONCAT(... SEPARATOR …)` delimiter is MySQL's; PostgreSQL spells the
873        // aggregate delimiter as an ordinary `string_agg(x, ',')` argument.
874        group_concat_separator: false,
875        within_group: true,
876        aggregate_filter: true,
877        // PostgreSQL requires `FILTER (WHERE …)` (engine-probed on PG-17); the keyword-less
878        // DuckDB body is not accepted.
879        filter_optional_where: false,
880        // PostgreSQL admits an aggregate's argument forms regardless of a space before the
881        // `(` — the significant-space rule is MySQL's `IGNORE_SPACE`-off tokenizer only.
882        aggregate_args_require_adjacent_paren: false,
883        null_treatment: false,
884        // MySQL-only built-in aggregate/window arity restrictions; PostgreSQL admits an
885        // empty aggregate call and `OVER` on any function.
886        aggregate_calls_reject_empty_arguments: false,
887        over_requires_windowable_function: false,
888        window_function_tail: false,
889        standalone_argument_order_by: false,
890    };
891}
892
893impl PredicateSyntax {
894    /// The `POSTGRES` preset for predicate syntax.
895    pub const POSTGRES: Self = Self {
896        like: true,
897        ilike: true,
898        similar_to: true,
899        // The SQL-standard `(s1, e1) OVERLAPS (s2, e2)` period predicate. PostgreSQL-only
900        // among the shipped engines (DuckDB overrides the `..Self::POSTGRES` spread to
901        // `false`, and MySQL/SQLite inherit ANSI's `false`).
902        overlaps_period_predicate: true,
903        // PostgreSQL requires the parentheses; `x IN y` is a syntax error there.
904        unparenthesized_in_list: false,
905        // `'foo' LIKE ANY (ARRAY['%a'])` / `ILIKE ALL (…)` — PostgreSQL's pattern-match
906        // ScalarArrayOpExpr (engine-probed via libpg_query; `SIMILAR TO ANY` rejects).
907        pattern_match_quantifier: true,
908        between_symmetric: true,
909        is_normalized: true,
910        // PostgreSQL requires a non-empty `IN` list; `x IN ()` is a syntax error there.
911        empty_in_list: false,
912        // PostgreSQL accepts the one-word `ISNULL`/`NOTNULL` postfix (see
913        // `OperatorSyntax::null_test_postfix`) but rejects the two-word `<expr> NOT NULL`
914        // (engine-measured via libpg_query): `SELECT 1 WHERE 1 NOT NULL` is a syntax error.
915        null_test_two_word_postfix: false,
916    };
917}
918
919impl FeatureSet {
920    /// PostgreSQL as dialect data.
921    pub const POSTGRES: Self = Self {
922        identifier_casing: Casing::Lower,
923        identifier_quotes: STANDARD_IDENTIFIER_QUOTES,
924        default_null_ordering: NullOrdering::NullsLast,
925        reserved_column_name: RESERVED_COLUMN_NAME,
926        reserved_function_name: RESERVED_FUNCTION_NAME,
927        reserved_type_name: RESERVED_TYPE_NAME,
928        reserved_bare_alias: RESERVED_BARE_ALIAS,
929        reserved_as_label: KeywordSet::EMPTY,
930        catalog_qualified_names: true,
931        // The shared M1 table plus the vertical tab (`0x0b`) as *ordinary* whitespace:
932        // PostgreSQL's flex `space` set is `[ \t\n\r\f\v]`, and the vertical tab is the one
933        // member Rust's `is_ascii_whitespace` (hence `STANDARD_BYTE_CLASSES`) omits. SQLite
934        // and DuckDB fold `0x0b` only position-dependently (their own tables — SQLite as a
935        // run continuation, DuckDB as statement-trim), so full whitespace-class membership
936        // rides only this table, not the shared one (see [`POSTGRES_BYTE_CLASSES`]). `$` is
937        // intentionally absent as an identifier byte:
938        // PostgreSQL admits `$` as an identifier-*continue* byte through
939        // `identifier_syntax.dollar_in_identifiers` (see [`IdentifierSyntax::POSTGRES`])
940        // instead, which keeps it out of the identifier-*start* class so a leading `$` stays
941        // a positional `$1` parameter.
942        byte_classes: POSTGRES_BYTE_CLASSES,
943        // PostgreSQL ranks `[NOT] BETWEEN`/`IN`/`LIKE`/`ILIKE`/`SIMILAR TO` one tier ABOVE
944        // the comparison operators (gram.y `%nonassoc BETWEEN IN_P LIKE …`), so
945        // `a = b BETWEEN c AND d` groups `a = (b BETWEEN c AND d)` — everything else stays
946        // the standard table.
947        binding_powers: BindingPowerTable {
948            range_predicate_override: Some(RANGE_PREDICATE_ABOVE_COMPARISON),
949            // The `IS`-family predicates (`IS NULL`, `IS DISTINCT FROM`, `IS TRUE`, …) rank one
950            // tier below comparison, so `a <> b IS NULL` groups `(a <> b) IS NULL`
951            // (`%nonassoc IS ISNULL NOTNULL`, engine-measured on PostgreSQL 16).
952            is_predicate_override: Some(IS_PREDICATE_BELOW_COMPARISON),
953            ..STANDARD_BINDING_POWERS
954        },
955        set_operation_powers: STANDARD_SET_OPERATION_BINDING_POWERS,
956        string_literals: StringLiteralSyntax::POSTGRES,
957        numeric_literals: NumericLiteralSyntax::POSTGRES,
958        parameters: ParameterSyntax::POSTGRES,
959        // PostgreSQL spells `@` as a text-search / custom operator, never a session
960        // variable, so the `@`/`@@` variable forms stay off (the ANSI baseline).
961        session_variables: SessionVariableSyntax::ANSI,
962        identifier_syntax: IdentifierSyntax::POSTGRES,
963        table_expressions: TableExpressionSyntax::POSTGRES,
964        join_syntax: JoinSyntax::POSTGRES,
965        table_factor_syntax: TableFactorSyntax::POSTGRES,
966        expression_syntax: ExpressionSyntax::POSTGRES,
967        operator_syntax: OperatorSyntax::POSTGRES,
968        call_syntax: CallSyntax::POSTGRES,
969        string_func_forms: StringFuncForms::POSTGRES,
970        aggregate_call_syntax: AggregateCallSyntax::POSTGRES,
971        predicate_syntax: PredicateSyntax::POSTGRES,
972        pipe_operator: PipeOperator::StringConcat,
973        double_ampersand: DoubleAmpersand::Unsupported,
974        keyword_operators: KeywordOperators::Unsupported,
975        // PostgreSQL's `^` is exponentiation, its own precedence row (tighter than `*`).
976        caret_operator: CaretOperator::Exponent,
977        // PostgreSQL spells bitwise XOR `#` (engine-measured: `2 # 3` parses, `#` binds at
978        // the "any other operator" rank), not `^`.
979        hash_bitwise_xor: true,
980        // A `--`/`#` line comment ends at `\r` as well as `\n` here (flex `non_newline`
981        // is `[^\n\r]`) — the one comment-shape difference from the ANSI baseline.
982        comment_syntax: CommentSyntax::POSTGRES,
983        mutation_syntax: MutationSyntax::POSTGRES,
984        statement_ddl_gates: StatementDdlGates::POSTGRES,
985        create_table_clause_syntax: CreateTableClauseSyntax::POSTGRES,
986        column_definition_syntax: ColumnDefinitionSyntax::POSTGRES,
987        constraint_syntax: ConstraintSyntax::POSTGRES,
988        index_alter_syntax: IndexAlterSyntax::POSTGRES,
989        existence_guards: ExistenceGuards::POSTGRES,
990        select_syntax: SelectSyntax::POSTGRES,
991        query_tail_syntax: QueryTailSyntax::POSTGRES,
992        grouping_syntax: GroupingSyntax::POSTGRES,
993        utility_syntax: UtilitySyntax::POSTGRES,
994        show_syntax: ShowSyntax::POSTGRES,
995        maintenance_syntax: MaintenanceSyntax::POSTGRES,
996        access_control_syntax: AccessControlSyntax::POSTGRES,
997        type_name_syntax: TypeNameSyntax::POSTGRES,
998        // Render PostgreSQL's canonical type spellings (`NUMERIC`, `TIMESTAMPTZ`, …) —
999        // the one preset whose target spelling diverges from the ANSI baseline.
1000        target_spelling: TargetSpelling::Postgres,
1001    };
1002}
1003
1004/// Prefer [`FeatureSet::POSTGRES`] for struct update.
1005pub const POSTGRES: FeatureSet = FeatureSet::POSTGRES;
1006
1007// Compile-time proof the PostgreSQL preset claims no shared tokenizer trigger twice —
1008// notably that `subscript` and `containment_operators` (both on here) never meet a
1009// contending `:name`/`@name` form. The ratchet fails the build if a future edit adds
1010// one, rather than silently shadowing a meaning (uniform with `LENIENT`'s assert).
1011const _: () = assert!(FeatureSet::POSTGRES.is_lexically_consistent());
1012// The two sibling self-consistency registries are ratcheted the same way, so the
1013// parse-entry `debug_assert!` folds all three to dead code for this preset: every
1014// refinement flag (the locking strengths, the MERGE and extended-`ALTER TABLE` actions,
1015// `prepare_typed_parameters`) rides its enabled base, and no two features contend for one
1016// parser-position head.
1017const _: () = assert!(FeatureSet::POSTGRES.has_satisfied_feature_dependencies());
1018const _: () = assert!(FeatureSet::POSTGRES.has_no_grammar_conflict());