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