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());