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