spg_sql/ast.rs
1//! AST for the PG-dialect subset SPG accepts in v0.2.
2//!
3//! `Display` is implemented so that for any AST `a` produced by [`crate::parser`],
4//! re-parsing `format!("{a}")` yields a structurally equal AST. Binary and
5//! unary operators always emit parentheses to remove any precedence
6//! ambiguity — round-trip safety wins over prettiness.
7
8use alloc::boxed::Box;
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use core::fmt;
13
14/// `COPY … TO STDOUT` output format. `text` is PG's default
15/// (tab-separated, `\N` nulls, backslash escapes); `csv` follows
16/// RFC-4180-style quoting.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum CopyFormat {
19 #[default]
20 Text,
21 Csv,
22}
23
24/// Options for `COPY … TO STDOUT [WITH] (…)`. Defaults reproduce the
25/// bare `COPY … TO STDOUT` text-format behaviour, so an empty option
26/// list is a no-op. `delimiter` / `null_str` / `quote` fall back to the
27/// per-format defaults (text: `\t` / `\N`; csv: `,` / `` / `"`) when
28/// unset.
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct CopyOptions {
31 pub format: CopyFormat,
32 pub header: bool,
33 pub delimiter: Option<char>,
34 pub null_str: Option<String>,
35 pub quote: Option<char>,
36 /// v7.39 (round 247) — CSV `ESCAPE`: the character that precedes a
37 /// quote (or itself) inside a quoted cell. Defaults to the quote
38 /// character (PG's doubling behavior).
39 pub escape: Option<char>,
40 /// v7.39 (round 247) — CSV `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`:
41 /// columns whose non-NULL cells always quote. `Some(vec![])` is the
42 /// `*` spelling (every column).
43 pub force_quote: Option<Vec<String>>,
44 /// v7.39 (round 265) — CSV `FORCE_NOT_NULL (col, …)`: for these
45 /// columns an UNQUOTED empty field reads as the empty string rather
46 /// than NULL (probed). COPY FROM only.
47 pub force_not_null: Option<Vec<String>>,
48 /// v7.39 (round 265) — CSV `FORCE_NULL (col, …)`: for these columns
49 /// a QUOTED empty field (`""`) also reads as NULL (probed). COPY
50 /// FROM only.
51 pub force_null: Option<Vec<String>>,
52}
53
54/// v7.39 (round 218) — FETCH / MOVE cursor direction. PG grammar: single-row
55/// forms (NEXT / PRIOR / FIRST / LAST / ABSOLUTE n / RELATIVE n) return at
56/// most one row; multi-row forms (bare n / ALL / FORWARD [n|ALL] /
57/// BACKWARD [n|ALL]) stream a run. A negative bare/FORWARD count means
58/// BACKWARD (normalized at execution).
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum CursorDirection {
61 Next,
62 Prior,
63 First,
64 Last,
65 Absolute(i64),
66 Relative(i64),
67 /// Bare `FETCH n` / `FORWARD n` (negative = backward n).
68 Count(i64),
69 /// `ALL` / `FORWARD ALL`.
70 All,
71 Backward(i64),
72 BackwardAll,
73}
74
75impl fmt::Display for CursorDirection {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self {
78 Self::Next => f.write_str("NEXT"),
79 Self::Prior => f.write_str("PRIOR"),
80 Self::First => f.write_str("FIRST"),
81 Self::Last => f.write_str("LAST"),
82 Self::Absolute(n) => write!(f, "ABSOLUTE {n}"),
83 Self::Relative(n) => write!(f, "RELATIVE {n}"),
84 Self::Count(n) => write!(f, "FORWARD {n}"),
85 Self::All => f.write_str("ALL"),
86 Self::Backward(n) => write!(f, "BACKWARD {n}"),
87 Self::BackwardAll => f.write_str("BACKWARD ALL"),
88 }
89 }
90}
91
92/// v7.39 (round 320, V53) — what a `DISCARD` throws away.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum DiscardTarget {
95 All,
96 Plans,
97 Sequences,
98 Temp,
99}
100
101impl fmt::Display for DiscardTarget {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.write_str(match self {
104 Self::All => "ALL",
105 Self::Plans => "PLANS",
106 Self::Sequences => "SEQUENCES",
107 Self::Temp => "TEMP",
108 })
109 }
110}
111
112/// v7.39 (round 535) — which maintenance statement, and therefore what
113/// its target names. Measured on PG18: INDEX / TABLE / CLUSTER name a
114/// relation, SCHEMA names a schema, and SYSTEM / DATABASE name neither
115/// in a way SPG can refuse.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum MaintainKind {
118 ReindexRelation,
119 ReindexSchema,
120 /// `REINDEX SYSTEM` / `REINDEX DATABASE`, and a bare `CLUSTER`.
121 Whole,
122 ClusterRelation,
123}
124
125/// v7.39 (round 547) — see [`Statement::SetDbRoleSetting`]. Boxed in the
126/// enum so the variant costs one pointer.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SetDbRoleSettingStatement {
129 pub database: Option<String>,
130 pub role: Option<String>,
131 pub param: Option<String>,
132 pub value: Option<String>,
133}
134
135/// v7.39 (round 696) — which operand a [`Statement::ValidateOnly`] names,
136/// and therefore which catalog answers whether it exists.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ValidateOnlyKind {
139 /// `LOCK TABLE <t> [, …]` — the relation must exist.
140 LockTable,
141 /// Every role named must exist: `DROP OWNED BY <r> [, …]`,
142 /// `REASSIGN OWNED BY <r> [, …] TO <r>`, and (round 697)
143 /// `SET SESSION AUTHORIZATION <r>`.
144 RoleName,
145 /// `SECURITY LABEL …` — PG refuses unconditionally, because no label
146 /// provider is loaded. SPG has none either.
147 SecurityLabel,
148 /// v7.39 (round 697) — `CREATE EXTENSION <e>`: the extension must be
149 /// AVAILABLE (PG: `extension "x" is not available`).
150 ExtensionAvailable,
151 /// v7.39 (round 708) — `ALTER TYPE <t> <any no-op form>`: the TYPE must
152 /// exist (PG: `type "x" does not exist`); the action itself stays a
153 /// no-op (PG genuinely renames; that residual is recorded).
154 TypeName,
155 /// v7.39 (round 708) — `ALTER AGGREGATE name(args) …`: names[0] is the
156 /// aggregate, the rest its argument type names (`*` = the `(*)` form).
157 /// Existence only; the action no-ops (PG really renames built-ins —
158 /// measured — and SPG does not model that).
159 AggregateName,
160 /// v7.39 (round 708) — `DROP CONVERSION <c>`: SPG ships no conversions,
161 /// so every name answers PG's `conversion "x" does not exist`.
162 ConversionName,
163 /// v7.39 (round 708) — `DROP LANGUAGE <l>`: an unknown language does
164 /// not exist; a shipped one is required (PG's two wordings, measured).
165 LanguageName,
166 /// v7.39 (round 709) — a collation name: performable or PG's
167 /// `collation "x" for encoding "UTF8" does not exist`.
168 CollationName,
169 /// v7.39 (round 709) — a text search configuration name.
170 TsConfigName,
171 /// v7.39 (round 709) — an event trigger name. SPG has none, so the
172 /// not-found answer is total.
173 EventTriggerName,
174 /// v7.39 (round 709) — a tablespace name. SPG has none beyond PG's two
175 /// built-ins, whose drop PG refuses with `permission denied` (measured).
176 TablespaceName,
177 /// v7.39 (round 709) — a large-object oid (names[0], decimal). The
178 /// registry is real (round 287), so the check is a lookup.
179 LargeObjectOid,
180 /// v7.39 (round 706) — `CREATE SERVER` / `CREATE FOREIGN TABLE` /
181 /// `CREATE FOREIGN DATA WRAPPER`. SPG has no foreign-data
182 /// infrastructure at all, so PG's refusals (`foreign-data wrapper "x"
183 /// does not exist`, `server "x" does not exist`) cannot be copied —
184 /// PG can refuse because the missing piece is installable there.
185 /// Accepted with a WARNING, the extension resolution (round 697):
186 /// refusing turns a dump that restores today into one that needs
187 /// editing, and silent acceptance was the actual defect.
188 ForeignInfra,
189 /// v7.39 (round 697) — `DROP EXTENSION <e>`: it must be installed
190 /// (PG: `extension "x" does not exist`).
191 ExtensionInstalled,
192}
193
194#[derive(Debug, Clone, PartialEq)]
195#[allow(clippy::large_enum_variant)] // Statement::Select dominates; Boxing would touch every match site
196pub enum Statement {
197 /// v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET <name>`.
198 ///
199 /// It used to be swallowed with the rest of the ALTER no-ops, which meant
200 /// `ALTER SYSTEM SET nosuch_guc = 1` was ACCEPTED where PG18 answers
201 /// `unrecognized configuration parameter`. SPG still applies nothing —
202 /// there is no postgresql.auto.conf to write — but a name it does not
203 /// know is now refused rather than swallowed.
204 ///
205 /// `None` is `RESET ALL`, which names no parameter.
206 AlterSystem {
207 parameter: Option<String>,
208 },
209 /// `DROP DATABASE [IF EXISTS] <name>`. SPG is single-database, so
210 /// this never succeeds; the name and the flag are carried so the
211 /// engine can answer with PG's wording for the two cases PG itself
212 /// has — an unknown name, or the database you are connected to.
213 DropDatabase {
214 name: String,
215 if_exists: bool,
216 },
217 /// A statement SPG accepts as a no-op but PG refuses inside a
218 /// transaction block — today `CREATE DATABASE` / `DROP DATABASE`,
219 /// which are no-ops here because SPG is single-database.
220 ///
221 /// The no-op path they used to share (`Statement::Empty`) also
222 /// carries CREATE ROLE, CREATE CAST and a dozen others that PG is
223 /// happy to run inside a transaction, so the object has to be named
224 /// to refuse the right ones.
225 NoOpPreventedInTransaction {
226 what: String,
227 /// v7.38.18 — `CREATE DATABASE … LC_COLLATE 'de_DE.utf8'` is in
228 /// every PostgreSQL bootstrap script there is, and SPG threw the
229 /// whole statement away. Being single-database makes the NAME a
230 /// no-op; it does not make the collation one, and a database
231 /// that quietly sorts by the container's `LANG` instead of the
232 /// one the script asked for is a silent difference in every
233 /// `ORDER BY` it will ever run.
234 ///
235 /// `LOCALE` and `LC_COLLATE` both land here; `LC_CTYPE` does
236 /// not, because SPG has no separate ctype.
237 collation: Option<String>,
238 },
239 /// v7.39 (round 696) — statements SPG performs nothing for, but whose
240 /// OPERAND PG validates before performing nothing either.
241 ///
242 /// All four used to be consumed whole by `is_dump_noise_statement`,
243 /// which meant `LOCK TABLE nosuch` and `DROP OWNED BY nosuchrole` were
244 /// ACCEPTED where PG18 errors. Accepting a statement that names
245 /// something that does not exist is the F29 shape: the caller is told
246 /// their intent was understood when the object it referred to is not
247 /// there.
248 ///
249 /// They share one variant because they share one rule — resolve the
250 /// name, refuse if absent, otherwise no-op — and four variants would be
251 /// four places for that rule to drift.
252 /// v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS] name(argtypes)[, …]`.
253 /// Consumed whole by the dump-noise list before, so `DROP AGGREGATE
254 /// nosuch(int)` reported success. PG validates every named aggregate's
255 /// EXISTENCE first (measured: a list with one unknown fails on the
256 /// unknown even when an earlier entry exists), renders the signature
257 /// with canonical type names (`int` → `integer`), and refuses to drop a
258 /// built-in (`cannot drop function sum(integer) because it is required
259 /// by the database system`). Every SPG aggregate is a built-in, so the
260 /// outcome is one of those two errors — or the IF EXISTS no-op.
261 ///
262 /// `args` holds the argument type names as written; `None` is the
263 /// `(*)` spelling.
264 DropAggregate {
265 if_exists: bool,
266 items: Vec<(String, Option<Vec<String>>)>,
267 },
268 /// v7.39 (round 750) — `ALTER ROLE|USER <name> … PASSWORD 'x' |
269 /// PASSWORD NULL`. The one attribute of the no-op family with a
270 /// SECURITY consequence: it was silently dropped (ledgered r710),
271 /// so a rotated credential never rotated. `None` = PASSWORD NULL
272 /// (the role keeps existing but can no longer password-auth).
273 AlterRolePassword {
274 name: String,
275 password: Option<String>,
276 },
277 ValidateOnly {
278 kind: ValidateOnlyKind,
279 /// The names the statement referred to. Empty means the form names
280 /// nothing (`SECURITY LABEL`, whose refusal is unconditional).
281 names: Vec<String>,
282 },
283
284 /// v7.39 (round 547) — `ALTER ROLE … SET/RESET` and
285 /// `ALTER DATABASE … SET/RESET`: the GUC defaults a session picks up
286 /// when it starts. Both used to land in the pg_dump no-op tail, so
287 /// the statement reported success and changed nothing.
288 ///
289 /// `database` / `role` are `None` for PG's oid 0 — `ALTER ROLE ALL`
290 /// sets both to None. `param` is `None` for RESET ALL. `value` is
291 /// `None` for RESET of one parameter.
292 SetDbRoleSetting(Box<SetDbRoleSettingStatement>),
293 /// v7.39 (round 288) — `SET CONSTRAINTS { ALL | <name>… }
294 /// { DEFERRED | IMMEDIATE }`. `deferred` carries the timing; the
295 /// name list is not yet honoured (ALL is what pg_dump emits and
296 /// what a circular-FK restore needs), so a named form applies to
297 /// all deferrable constraints too rather than silently doing
298 /// nothing.
299 /// v7.39 (round 308) — `SET CONSTRAINTS { ALL | name [, …] }
300 /// { DEFERRED | IMMEDIATE }`. An empty `names` is the ALL form;
301 /// otherwise the timing applies only to the constraints listed.
302 SetConstraints {
303 names: Vec<String>,
304 deferred: bool,
305 },
306
307 /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
308 /// [CASCADE | RESTRICT]`. Engine removes the matching tables
309 /// (each one) from the catalog; IF EXISTS makes the drop
310 /// idempotent. CASCADE / RESTRICT trailers parsed silently
311 /// (SPG always cascades index drops on table drop).
312 DropTable {
313 names: Vec<String>,
314 if_exists: bool,
315 },
316 /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
317 /// matching index across whichever table holds it.
318 DropIndex {
319 name: String,
320 if_exists: bool,
321 },
322 /// v7.14.0 — empty / comment-only statement. The lexer strips
323 /// `--` line comments and `/* … */` block comments (including
324 /// the MySQL conditional `/*!NNNNN … */` form) before the
325 /// parser ever sees them; a SQL chunk that contains nothing
326 /// else lands here. Engine returns CommandOk no-op so
327 /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
328 /// wrapped in conditional comments, etc.) load cleanly.
329 /// v7.39 (round 277) — SQL-level `PREPARE <name> [(type, …)] AS
330 /// <stmt>`. Session-scoped; the body keeps its `$N` placeholders
331 /// and is substituted at EXECUTE time.
332 Prepare {
333 name: String,
334 /// Declared parameter type names, in order. Empty when the
335 /// `(type, …)` list was omitted (PG infers them).
336 param_types: Vec<String>,
337 body: alloc::boxed::Box<Statement>,
338 /// The statement's own source text, which
339 /// `pg_prepared_statements.statement` reports verbatim.
340 source: String,
341 },
342 /// v7.39 (round 277) — `EXECUTE <name> [(arg, …)]`.
343 Execute {
344 name: String,
345 args: Vec<Expr>,
346 },
347 /// v7.39 (round 277) — `DEALLOCATE {<name> | ALL}`. `None` = ALL.
348 Deallocate(Option<String>),
349 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
350 /// [(kind, …)] ON <col>, … FROM <table>`. SPG records the object so
351 /// dumps restore and reflection is honest; the planner does not
352 /// consult it yet.
353 CreateStatistics {
354 name: String,
355 if_not_exists: bool,
356 /// Requested kinds as PG's single letters (`d` ndistinct,
357 /// `f` dependencies, `m` mcv). Empty = PG's default set.
358 kinds: Vec<String>,
359 columns: Vec<String>,
360 table: String,
361 },
362 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
363 DropStatistics {
364 name: String,
365 if_exists: bool,
366 },
367 /// v7.39 (round 278) — `CALL <proc>(…)`. Parses; the engine
368 /// reports that the procedure does not exist, because SPG has no
369 /// procedure catalog. Carried as a statement rather than raised at
370 /// parse time so the failure is a missing OBJECT (42883), not a
371 /// syntax error.
372 Call(String),
373 /// v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'`. Same shape:
374 /// 2PC is unavailable, which PG itself reports when
375 /// `max_prepared_transactions` is 0.
376 PrepareTransaction(String),
377 Empty,
378 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE]
379 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. The
380 /// canonical driver path for streaming large result sets (psycopg2
381 /// named cursors, JDBC setFetchSize).
382 DeclareCursor {
383 name: String,
384 /// `None` = neither keyword (PG default: backward allowed when the
385 /// plan supports it — always, for SPG's materialized cursors);
386 /// `Some(true)` = SCROLL; `Some(false)` = NO SCROLL (backward
387 /// fetch errors 55000).
388 scroll: Option<bool>,
389 /// `WITH HOLD` — survives the creating transaction's COMMIT.
390 hold: bool,
391 query: Box<Statement>,
392 },
393 /// v7.39 (round 218) — `FETCH [<direction>] [FROM|IN] <name>`.
394 FetchCursor {
395 name: String,
396 direction: CursorDirection,
397 },
398 /// v7.39 (round 218) — `MOVE [<direction>] [FROM|IN] <name>`: FETCH
399 /// without returning rows; the command tag carries the move count.
400 MoveCursor {
401 name: String,
402 direction: CursorDirection,
403 },
404 /// v7.39 (round 218) — `CLOSE <name>` / `CLOSE ALL` (`None` = ALL).
405 CloseCursor {
406 name: Option<String>,
407 },
408 /// v7.39 (round 222) — `LISTEN <channel>`: subscribe this session to
409 /// async notifications on the channel.
410 Listen(String),
411 /// v7.39 (round 222) — `NOTIFY <channel> [, '<payload>']`. Delivered at
412 /// COMMIT (PG semantics: transactional, deduplicated within the tx);
413 /// immediately under autocommit.
414 Notify {
415 channel: String,
416 payload: Option<String>,
417 },
418 /// v7.39 (round 222) — `UNLISTEN <channel>` / `UNLISTEN *` (`None` = *).
419 Unlisten(Option<String>),
420 /// `COPY table [(cols)] TO STDOUT` — the engine renders the
421 /// visible rows in COPY text format (tab-separated, `\N`
422 /// nulls, backslash escapes) as a single-text-column result
423 /// set; the wire layer streams CopyData from it.
424 CopyTo {
425 table: String,
426 columns: Option<Vec<String>>,
427 /// v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT`: an
428 /// arbitrary SELECT/VALUES/CTE (a whole [`Statement`], so set-ops and
429 /// VALUES ride through unchanged) whose result set is streamed in COPY
430 /// format. `Some` overrides `table`/`columns` (which are empty then);
431 /// `None` is the classic `COPY <table> …` shape.
432 query: Option<Box<Statement>>,
433 /// v7.37.x — `WITH (FORMAT csv, HEADER, DELIMITER, NULL, QUOTE)`
434 /// and the legacy `WITH CSV HEADER …` spelling. Default =
435 /// text format, no header (bare `COPY … TO STDOUT`).
436 options: CopyOptions,
437 },
438 /// v7.39 (round 249) — `COPY table [(cols)] FROM '<path>' [(opts)]`.
439 /// The engine is no_std and cannot read the file itself: the host
440 /// (embedded / server / tooling) reads the path and hands the bytes to
441 /// `Engine::copy_from_buffer`. Dispatching this statement straight to
442 /// the engine reports that contract.
443 CopyFromFile {
444 table: String,
445 columns: Option<Vec<String>>,
446 path: String,
447 options: CopyOptions,
448 },
449 /// v7.39 (round 249/252) — `COPY <table> [(cols)] TO '<file>'` (and
450 /// the `COPY (<query>) TO '<file>'` form). The engine is no_std and
451 /// cannot write the file itself: the host renders the payload via
452 /// `Engine::copy_to_buffer` and writes the path.
453 CopyToFile {
454 table: String,
455 columns: Option<Vec<String>>,
456 query: Option<Box<Statement>>,
457 path: String,
458 options: CopyOptions,
459 },
460 Select(SelectStatement),
461 CreateTable(CreateTableStatement),
462 /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
463 /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
464 /// no-op so PG dumps that include extension declarations
465 /// (notably `pgvector`) load against SPG without splitting
466 /// init scripts. mailrs migration follow-up F3.
467 CreateExtension(String),
468 /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
469 /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
470 /// the engine executes it at top level (mailrs round-10
471 /// A.2). Pre-v7.16.2 the parser discarded the body and the
472 /// engine returned CommandOk — a SEV-1 silent no-op that
473 /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
474 /// $$` idempotent migrations into invisible no-ops.
475 DoBlock(PlPgSqlBlock),
476 CreateIndex(CreateIndexStatement),
477 Insert(InsertStatement),
478 /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
479 Update(UpdateStatement),
480 /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
481 Delete(DeleteStatement),
482 /// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ `MERGE` statement.
483 /// `MERGE INTO target [alias] USING source [alias] ON cond
484 /// WHEN MATCHED [AND cond] THEN { UPDATE SET … | DELETE | DO NOTHING }
485 /// WHEN NOT MATCHED [AND cond] THEN { INSERT (cols) VALUES (vals) | DO NOTHING }
486 /// [WHEN …]`. SPG v7.17 supports table-based source (subquery
487 /// source is a follow-up); BY SOURCE / BY TARGET and RETURNING
488 /// are also follow-ups.
489 Merge(MergeStatement),
490 /// v7.39 (round 169) — `VACUUM [(opts)] [FULL|FREEZE|VERBOSE|ANALYZE]
491 /// [<table>]`. Was a parse-time no-op from the pre-MVCC era; with the
492 /// in-place MVCC gate ON, tombstoned versions are REAL bloat and a
493 /// customer's manual VACUUM must actually reclaim. `analyze` mirrors
494 /// the `VACUUM ANALYZE` spelling.
495 Vacuum {
496 table: Option<String>,
497 analyze: bool,
498 },
499 /// `BEGIN` / `START TRANSACTION` — with an optional explicit
500 /// `ISOLATION LEVEL …` mode (`None` = use the session default). PG
501 /// applies the level for the duration of this transaction only.
502 Begin(Option<IsolationLevel>),
503 Commit,
504 Rollback,
505 /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
506 /// stack so a later `ROLLBACK TO <name>` can undo just the work
507 /// since this point.
508 Savepoint(String),
509 /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
510 /// named savepoint and discard later savepoints. Does not end the
511 /// transaction.
512 RollbackToSavepoint(String),
513 /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
514 /// rolling back. Keeps the work done since then.
515 ReleaseSavepoint(String),
516 /// `SHOW TABLES` — return the list of tables in the catalog.
517 ShowTables,
518 /// v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES` /
519 /// `SHOW SCHEMAS`. SPG is single-database; the executor
520 /// returns the canonical MySQL set so the mysql / MariaDB
521 /// client populates its database selector.
522 ShowDatabases,
523 /// v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE TABLE <t>`
524 /// returns a 2-column row `(Table, "Create Table")` carrying
525 /// the synthesized DDL. mysqldump emits this for every
526 /// table at scrape time.
527 ShowCreateTable(String),
528 /// v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES FROM <t>`
529 /// (also `SHOW INDEX`, `SHOW KEYS`).
530 ShowIndexes(String),
531 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS`.
532 ShowStatus,
533 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW VARIABLES`.
534 ShowVariables,
535 /// r1067 — MySQL `SHOW VARIABLES LIKE 'pattern'` (sysbench-tpcc
536 /// probes isolation with it at connect).
537 ShowVariablesLike(String),
538 /// v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
539 ShowProcesslist,
540 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
541 /// pgbouncer sends `DISCARD ALL` between pooled client sessions to make
542 /// the connection look brand new to the next client; it used to be
543 /// swallowed as dump noise, so nothing was discarded.
544 Discard(DiscardTarget),
545 /// v7.39 (round 318, V51) — MySQL `KILL [CONNECTION | QUERY] <expr>`.
546 /// The id is an expression because MariaDB accepts one
547 /// (`KILL connection_id()` is the documented way to drop your own
548 /// connection). `query_only` is the `QUERY` form: stop the target's
549 /// running statement but leave it connected.
550 Kill {
551 query_only: bool,
552 id: Box<Expr>,
553 },
554 /// `SHOW COLUMNS FROM <table>` — return one row per column with
555 /// its declared name / type / nullability.
556 ShowColumns(String),
557 /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
558 /// Role is optional; defaults to `readonly` when omitted.
559 CreateUser(CreateUserStatement),
560 /// `DROP USER 'name'` (v4.1). v7.39 (read01 round 58) — `IF EXISTS` is
561 /// carried through: PG skips with a NOTICE rather than erroring.
562 DropUser {
563 name: String,
564 if_exists: bool,
565 },
566 /// v7.39 (RLS) — `SET ROLE { name | NONE | DEFAULT }` / `RESET ROLE`.
567 /// `Some(name)` switches the session's effective role (drives
568 /// `current_user` and RLS enforcement); `None` resets to the login
569 /// identity (the Admin superuser).
570 SetRole(Option<String>),
571 /// v7.39 (read01 round 57) — `GRANT <privs> ON <object> TO <roles>`.
572 Grant(GrantStatement),
573 /// v7.39 (read01 round 57) — `REVOKE [GRANT OPTION FOR] <privs> ON
574 /// <object> FROM <roles>`.
575 Revoke(GrantStatement),
576 /// v7.39 (RLS) — `CREATE POLICY name ON table …`.
577 CreatePolicy(CreatePolicyStatement),
578 /// v7.39 (RLS) — `ALTER POLICY name ON table …`.
579 AlterPolicy(AlterPolicyStatement),
580 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
581 DropPolicy(DropPolicyStatement),
582 /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
583 ShowUsers,
584 /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
585 /// single-column text table describing the rewritten plan tree
586 /// for `inner`. `analyze` triggers an actual exec to attach
587 /// observed row counts and elapsed micros to each node.
588 Explain(ExplainStatement),
589 /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
590 /// Synchronous rebuild of an NSW index. With the optional
591 /// encoding clause, every stored cell at the indexed column is
592 /// also re-encoded through `coerce_value` before the new graph
593 /// builds.
594 AlterIndex(AlterIndexStatement),
595 /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
596 /// The only setting in v6.7.2 is `hot_tier_bytes`, which
597 /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
598 /// for the named table.
599 AlterTable(AlterTableStatement),
600 /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
601 /// The catalog row lives in `spg_publications`. Publisher-side
602 /// WAL filtering arrives in v6.1.5.
603 CreatePublication(CreatePublicationStatement),
604 /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
605 /// no-op when the publication does not exist.
606 DropPublication {
607 name: String,
608 /// v7.39 (round 754, F31-B4) — `IF EXISTS` quietly skips a
609 /// missing publication; the bare form refuses with PG's
610 /// sentence (PG18-measured — the old "silent no-op" note on
611 /// the executor was wrong).
612 if_exists: bool,
613 },
614 /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
615 /// publication ordered by name with `(name, scope_summary,
616 /// table_count)` columns. The scope summary is the human-
617 /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
618 /// TABLES EXCEPT …`; `table_count` is `NULL` for the
619 /// `AllTables` scope and the table-list length otherwise.
620 ShowPublications,
621 /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
622 /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
623 /// in `spg_subscriptions`; when the subscription is
624 /// `enabled = true` (default) the server spawns a
625 /// background worker that connects to `conn` and drains the
626 /// requested publication(s) into the local engine.
627 CreateSubscription(CreateSubscriptionStatement),
628 /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
629 /// PUBLICATION, silent no-op when absent. Stops the
630 /// associated worker thread before removing the row.
631 DropSubscription {
632 name: String,
633 /// v7.39 (round 754, F31-B4) — same contract as
634 /// [`Statement::DropPublication`].
635 if_exists: bool,
636 },
637 /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
638 /// subscription ordered by name with `(name, conn_str,
639 /// publications, enabled, last_received_pos)`.
640 ShowSubscriptions,
641 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
642 /// Blocks until the local server's apply position reaches
643 /// `<pos>` or `<ms>` elapses. Server-layer command: the
644 /// engine refuses it (`EngineError::Unsupported`) since
645 /// `lag_state` lives in `spg-server`'s `ServerState`.
646 WaitForWalPosition {
647 pos: u64,
648 /// `None` → wait forever; `Some(ms)` → return after `ms`
649 /// milliseconds even if the target isn't reached.
650 timeout_ms: Option<u64>,
651 },
652 /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
653 /// table; `ANALYZE <name>` re-stats just one. Populates
654 /// `spg_statistic` with per-column null_frac + n_distinct +
655 /// 100-bucket equi-depth histogram.
656 Analyze(Option<String>),
657 /// v7.39 (round 535) — `REINDEX { INDEX | TABLE | SCHEMA | DATABASE
658 /// | SYSTEM } [CONCURRENTLY] <name>` and `CLUSTER [VERBOSE]
659 /// [<table> [USING <index>]]`.
660 ///
661 /// SPG has neither index bloat nor a clustering order to rebuild, so
662 /// the work is a no-op — but PG VALIDATES the target, and both were
663 /// swallowed at parse time, so `REINDEX TABLE typo` reported success.
664 /// The name is carried now so the engine can say what PG says.
665 Maintain {
666 kind: MaintainKind,
667 /// `REINDEX … CONCURRENTLY`. Carried for the same reason as
668 /// [`CreateIndexStatement::concurrently`]: PG bars the
669 /// CONCURRENTLY form inside a transaction block and allows the
670 /// plain one.
671 concurrently: bool,
672 /// `None` for the whole-database forms, which name nothing.
673 target: Option<String>,
674 },
675 /// v7.37.17 (17.6 sibling) — `TRUNCATE [TABLE] [ONLY] <name>
676 /// [, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE |
677 /// RESTRICT]`. Clears every row from each named table. SPG's
678 /// SEQUENCE identity is per-table; RESTART IDENTITY reinitializes
679 /// the associated sequence to its starting value. CASCADE
680 /// currently walks direct FK-referring tables and truncates
681 /// them too (PG's semantics). The ONLY modifier (skip partitions)
682 /// and RESTRICT (default) are accepted with no effect since
683 /// SPG's declarative partitions are always truncated together.
684 Truncate {
685 tables: Vec<String>,
686 restart_identity: bool,
687 cascade: bool,
688 /// v7.39 (round 647) — `TRUNCATE ONLY t`. Absorbed as a no-op
689 /// since v7.14 on the reasoning that SPG's children are separate
690 /// relations a truncate does not descend into. Same reasoning
691 /// round 621 applied to `FROM ONLY`, and it stopped being true
692 /// for the same reason: measured, `TRUNCATE <inheritance parent>`
693 /// leaves the children's rows where PG empties them, and
694 /// `TRUNCATE ONLY <partitioned parent>` is silently accepted
695 /// where PG refuses it outright.
696 only: bool,
697 },
698 /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
699 /// BTree-cold indices and merges small cold-tier segments
700 /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
701 /// 4 MiB) into a single larger segment per (table, index).
702 /// `WHERE` predicate filtering on which tables to compact is
703 /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
704 /// v6.7.3 only supports the bare form.
705 CompactColdSegments,
706 /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
707 /// parameter on the engine; v7.12.1 honours
708 /// `default_text_search_config` (consumed by `to_tsvector` /
709 /// `plainto_tsquery` family when called without an explicit
710 /// config arg). All other names are accepted as a no-op so PG
711 /// dumps with `SET client_encoding`, `SET search_path` etc.
712 /// load cleanly.
713 SetParameter {
714 name: String,
715 value: SetValue,
716 /// v7.38 (read01 P3.19) — `SET LOCAL` scopes the change to the
717 /// current transaction; the engine saves the prior value and
718 /// restores it at COMMIT / ROLLBACK. Plain `SET` (and `SET
719 /// SESSION`) leave this false and persist for the session.
720 local: bool,
721 },
722 /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
723 /// multi-assignment (mysqldump preamble uses
724 /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
725 /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
726 /// source order. Pairs whose LHS is a MySQL session/user
727 /// variable (`@VAR` / `@@VAR`) are recorded with the raw
728 /// name so the engine can ignore them; pairs whose LHS is
729 /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
730 /// go through the regular `set_session_param` path.
731 SetParameterList(Vec<(String, SetValue)>),
732 /// v7.39 (round 430) — MySQL's USER-defined variables:
733 /// `SET @x = 5, @s := CONCAT('a','b')`. Distinct from
734 /// [`Self::SetParameter`] (a `@@`-style engine/session setting) in
735 /// every way that matters: the value is an arbitrary EXPRESSION, the
736 /// name lives in its own per-session namespace, and reading an unset
737 /// one answers NULL rather than raising. `:=` and `=` are the same
738 /// assignment here.
739 ///
740 /// Before this the parser stripped every `@`, so `@x` and `@@x` were
741 /// the same node: `SET @x = 5` silently landed in the session-parameter
742 /// store where nothing could read it back, and `SELECT @x` failed with
743 /// "Unknown system variable".
744 /// v7.39 (round 554) — `SET @a = …, SETTING = …`.
745 ///
746 /// `settings` is the trailing half a mysqldump preamble writes:
747 /// `SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'`
748 /// saves a value and changes it in one statement. The parser used
749 /// to refuse the mixture outright, so no mysqldump could be
750 /// restored past its preamble.
751 SetUserVars(Vec<(String, Expr)>, Vec<(String, Expr)>),
752 /// v7.38 轴 4 — `SET [SESSION] TRANSACTION ISOLATION LEVEL …`
753 /// (plus optional READ ONLY / READ WRITE / DEFERRABLE clauses
754 /// silently accepted). PG-standard surface for picking an
755 /// isolation level. Engine tracks the value on
756 /// `Engine::current_isolation_level()`; actual MVCC / SSI
757 /// semantics implementation lands separately. PG itself maps
758 /// READ UNCOMMITTED to READ COMMITTED; SPG mirrors that —
759 /// effectively every level reads as READ COMMITTED in v7.37.8.
760 SetTransaction {
761 isolation: IsolationLevel,
762 },
763 /// v7.38 轴 4 — `SHOW <param>` returns a 1-column 1-row result
764 /// with the parameter's current value as TEXT. Today the only
765 /// recognised param is `transaction_isolation`; further
766 /// surfaces (`search_path`, `application_name`, …) land as the
767 /// session-parameter inventory grows.
768 ShowParameter(String),
769 /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
770 /// to its default. No-op for parameters SPG does not track.
771 ResetParameter(Option<String>),
772 /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
773 /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
774 /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
775 /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
776 /// languages parse but error at exec time with a clear
777 /// unsupported message.
778 CreateFunction(CreateFunctionStatement),
779 /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
780 /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
781 /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
782 /// triggers and column-list / WHEN clauses are out of scope
783 /// for v7.12.4.
784 CreateTrigger(CreateTriggerStatement),
785 /// v7.39 (round 139) — `CREATE RULE name AS ON event TO table [WHERE cond]
786 /// DO [ALSO|INSTEAD] { NOTHING | command }` query-rewrite rule.
787 CreateRule(CreateRuleStatement),
788 /// v7.39 (round 139) — `DROP RULE [IF EXISTS] name ON table`.
789 DropRule {
790 name: String,
791 table: String,
792 if_exists: bool,
793 },
794 /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
795 /// no-op when missing if `IF EXISTS` is set.
796 DropTrigger {
797 name: String,
798 table: String,
799 if_exists: bool,
800 },
801 /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
802 /// DROP TRIGGER but global (no table scope).
803 DropFunction {
804 name: String,
805 /// v7.39 (read01 round 62) — the argument TYPES, when the statement gave
806 /// them: `DROP FUNCTION f(int)` drops that overload only. `None` = no
807 /// argument list, which PG accepts only when the name is unambiguous.
808 args: Option<Vec<String>>,
809 if_exists: bool,
810 },
811 /// v7.17.0 — `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] name
812 /// [AS data_type]
813 /// [INCREMENT [BY] n]
814 /// [MINVALUE n | NO MINVALUE]
815 /// [MAXVALUE n | NO MAXVALUE]
816 /// [START [WITH] n]
817 /// [CACHE n]
818 /// [[NO] CYCLE]
819 /// [OWNED BY {table.col | NONE}]`.
820 /// Closes the round-7+ silent-no-op SEQUENCE story so pg_dump
821 /// emits + nextval/currval/setval downstream all work.
822 CreateSequence(CreateSequenceStatement),
823 /// v7.17.0 — `ALTER SEQUENCE [IF EXISTS] name <options>` with
824 /// the same option grammar as CREATE SEQUENCE, plus
825 /// `RESTART [WITH n]` and `OWNED BY ...` re-attach.
826 AlterSequence(AlterSequenceStatement),
827 /// v7.17.0 — `DROP SEQUENCE [IF EXISTS] name [, name…]
828 /// [CASCADE | RESTRICT]`. CASCADE / RESTRICT trailers parsed
829 /// silently (no FK on sequences).
830 DropSequence {
831 names: Vec<String>,
832 if_exists: bool,
833 },
834 /// v7.17.0 Phase 1.2 — `CREATE [OR REPLACE] [TEMPORARY] VIEW
835 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …>`. Closes the
836 /// silent-no-op VIEW story from the v7.17 customer-readiness
837 /// audit: pre-v7.17 SPG parsed CREATE VIEW as Statement::Empty
838 /// so any downstream `SELECT FROM v` errored with table-not-
839 /// found. The view body is stored verbatim; SELECT FROM <v>
840 /// rewrites at exec-time by prepending the view body as a
841 /// synthetic CTE.
842 CreateView(CreateViewStatement),
843 /// v7.17.0 Phase 1.2 — `DROP VIEW [IF EXISTS] name [, name…]
844 /// [CASCADE | RESTRICT]`. Removes the matching view from the
845 /// catalog; CASCADE/RESTRICT parsed silently.
846 DropView {
847 names: Vec<String>,
848 if_exists: bool,
849 },
850 /// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW [IF NOT
851 /// EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
852 /// Closes the silent-no-op MATERIALIZED VIEW story. Storage
853 /// model: the materialised result lives as a regular table
854 /// with the matching name + a parallel
855 /// `materialized_views` registry mapping name → body source
856 /// (used by REFRESH).
857 CreateMaterializedView(CreateMaterializedViewStatement),
858 /// v7.17.0 Phase 1.3 — `REFRESH MATERIALIZED VIEW name [WITH
859 /// [NO] DATA]`. Re-runs the stored body and replaces the
860 /// cached rows. `WITH NO DATA` truncates without re-running.
861 RefreshMaterializedView {
862 name: String,
863 with_data: bool,
864 },
865 /// v7.17.0 Phase 1.3 — `DROP MATERIALIZED VIEW [IF EXISTS]
866 /// name [, name…] [CASCADE | RESTRICT]`. Drops both the
867 /// backing table and the source registry entry.
868 DropMaterializedView {
869 names: Vec<String>,
870 if_exists: bool,
871 },
872 /// v7.17.0 Phase 1.4 — `CREATE TYPE name AS ENUM ('a', 'b',
873 /// …)`. Closes the silent-no-op CREATE TYPE story so PG
874 /// dumps that declare enum types load with real constraints
875 /// instead of becoming free-form TEXT. Future kinds
876 /// (composite / range / domain) extend the inner `kind`
877 /// enum.
878 CreateType(CreateTypeStatement),
879 /// v7.37 D.55 — `ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
880 /// [{BEFORE | AFTER} 'existing']`. Extends an enum's label list so
881 /// enum evolution stops being a silent no-op. `position` is
882 /// `Some((is_before, anchor))`.
883 /// v7.39 (read01 round 49) — `ALTER TYPE t RENAME VALUE 'old' TO 'new'`.
884 /// Used to be swallowed by the ALTER TYPE no-op tail, so the rename was
885 /// accepted and silently ignored.
886 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
887 /// Used to be swallowed as dump noise, so a comment was accepted and lost
888 /// (and obj_description / col_description always returned NULL).
889 /// `kind` is lowercase ("table" / "column" / "index" / …); for a column
890 /// `name` is the dotted `table.column`. `comment: None` = `IS NULL` = remove.
891 CommentOn {
892 kind: String,
893 name: String,
894 comment: Option<String>,
895 },
896 AlterTypeRenameValue {
897 type_name: String,
898 old: String,
899 new: String,
900 },
901 AlterTypeAddValue {
902 type_name: String,
903 label: String,
904 if_not_exists: bool,
905 position: Option<(bool, String)>,
906 },
907 /// v7.17.0 Phase 1.4 — `DROP TYPE [IF EXISTS] name [, name…]
908 /// [CASCADE | RESTRICT]`. Removes the matching enum/domain
909 /// from the catalog.
910 DropType {
911 names: Vec<String>,
912 if_exists: bool,
913 },
914 /// v7.17.0 Phase 1.5 — `CREATE DOMAIN name AS base_type
915 /// [DEFAULT expr] [NOT NULL | NULL] [CHECK (expr)]*`.
916 /// A DOMAIN is a named CHECK-constrained alias over a built-
917 /// in type. The CHECK + NOT NULL + DEFAULT clauses apply to
918 /// every column declared with the domain. Closes the
919 /// silent-no-op CREATE DOMAIN story so PG dumps that ship
920 /// validated identifier types (email, positive_int, …) keep
921 /// their guarantees.
922 CreateDomain(CreateDomainStatement),
923 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`. Every form was
924 /// previously swallowed by the catch-all DDL arm: the statement
925 /// reported success and did nothing, so a migration that dropped a
926 /// constraint kept rejecting the data it had just been told to
927 /// accept.
928 AlterDomain {
929 name: String,
930 action: AlterDomainAction,
931 },
932 /// v7.17.0 Phase 1.5 — `DROP DOMAIN [IF EXISTS] name
933 /// [, name…] [CASCADE | RESTRICT]`. Removes the matching
934 /// domain from the catalog.
935 DropDomain {
936 names: Vec<String>,
937 if_exists: bool,
938 },
939 /// v7.17.0 Phase 1.6 — `CREATE SCHEMA [IF NOT EXISTS]
940 /// name [AUTHORIZATION user]`. SPG is single-database;
941 /// schemas are tracked as a namespace registry so pg_dump
942 /// multi-schema declarations land cleanly and `SELECT *
943 /// FROM information_schema.schemata` returns real entries.
944 /// Schema-qualified `schema.table` references still strip
945 /// the prefix at lookup time per PG (schemas are not
946 /// isolation boundaries in v7.17 — see project-next-docket
947 /// for the v7.18+ isolation tracking).
948 CreateSchema {
949 name: String,
950 if_not_exists: bool,
951 },
952 /// v7.17.0 Phase 1.6 — `DROP SCHEMA [IF EXISTS] name
953 /// [, name…] [CASCADE | RESTRICT]`. Removes the schema
954 /// from the registry; built-in `public` / `pg_catalog` /
955 /// `information_schema` cannot be dropped.
956 DropSchema {
957 names: Vec<String>,
958 if_exists: bool,
959 },
960}
961
962/// v7.39 (round 260) — the `ALTER DOMAIN` actions SPG implements.
963#[derive(Debug, Clone, PartialEq)]
964pub enum AlterDomainAction {
965 AddConstraint { name: Option<String>, check: Expr },
966 DropConstraint { name: String, if_exists: bool },
967 SetDefault(Expr),
968 DropDefault,
969 SetNotNull,
970 DropNotNull,
971 RenameTo(String),
972}
973
974/// v7.17.0 Phase 1.5 — `CREATE DOMAIN` AST.
975#[derive(Debug, Clone, PartialEq)]
976pub struct CreateDomainStatement {
977 pub name: String,
978 /// Base type for the domain (one of the built-in
979 /// `ColumnTypeName` variants).
980 pub base_type: ColumnTypeName,
981 /// v7.39 (round 259) — `CREATE DOMAIN child AS parent …` where
982 /// `parent` is itself a DOMAIN. The parser already captured the
983 /// unknown type name; it just was not carried here, so the parent's
984 /// CHECK constraints were invisible and a value violating them was
985 /// silently accepted. `base_type` still holds the ultimate scalar
986 /// type, which is what the storage tier stores.
987 pub base_domain: Option<String>,
988 /// Optional `DEFAULT <expr>`. Resolved at engine-side
989 /// CREATE TABLE time when a column is bound to this domain.
990 pub default: Option<Expr>,
991 /// `NOT NULL` from the domain definition. Engine ORs this
992 /// with the column-level nullability so the strictest of the
993 /// two wins (i.e. the column is non-nullable if either side
994 /// says so).
995 pub not_null: bool,
996 /// Zero-or-more `CHECK (expr)` predicates. Each one is
997 /// enforced as part of the column's CHECK list at INSERT /
998 /// UPDATE time, with `VALUE` substituted for the column's
999 /// current cell value.
1000 pub checks: Vec<Expr>,
1001}
1002
1003/// v7.17.0 Phase 1.4 — `CREATE TYPE` AST.
1004#[derive(Debug, Clone, PartialEq, Eq)]
1005pub struct CreateTypeStatement {
1006 pub name: String,
1007 pub kind: TypeKind,
1008}
1009
1010/// v7.17.0 Phase 1.4 — flavour of the new type. Only ENUM is
1011/// implemented; the variant set is open so Phase 1.5 (DOMAIN)
1012/// and later (COMPOSITE, RANGE) can land without an AST shape
1013/// migration.
1014///
1015/// v7.37.x (ζ-B Phase 1 composite accept) — added Composite for
1016/// `CREATE TYPE name AS (field_name field_type, …)`. Phase 1
1017/// stores the field list in the catalog so PG dumps that emit
1018/// `CREATE TYPE … AS (…)` don't error out; using a composite type
1019/// as a column type lands in Phase 2 (Value::Composite encoding +
1020/// ROW() literal + field-access syntax).
1021#[derive(Debug, Clone, PartialEq, Eq)]
1022pub enum TypeKind {
1023 /// `AS ENUM ('a', 'b', …)`. Order is preserved (PG enum
1024 /// labels are ordered).
1025 Enum { labels: Vec<String> },
1026 /// `AS (field_name field_type, …)`. Order matters; PG
1027 /// composite literals are positional.
1028 Composite {
1029 fields: Vec<(String, ColumnTypeName)>,
1030 /// v7.39 (round 264) — parallel to `fields`: the raw type NAME
1031 /// when a field's type is not a builtin (i.e. another composite).
1032 /// The parser already captures it; without carrying it here a
1033 /// nested composite field resolved to the Text placeholder and
1034 /// the inner record never became a record.
1035 field_user_types: Vec<Option<String>>,
1036 },
1037}
1038
1039/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
1040/// a string literal, an identifier (often a config name), an
1041/// integer/float, or the bare `DEFAULT` keyword.
1042#[derive(Debug, Clone, PartialEq)]
1043pub enum SetValue {
1044 String(String),
1045 Ident(String),
1046 Number(String),
1047 Default,
1048}
1049
1050/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1051/// at parse time and tracks the selected value on the engine. The
1052/// actual semantic differentiation (REPEATABLE READ snapshot,
1053/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1054/// today every level reads as effective READ COMMITTED (which is
1055/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1056/// READ COMMITTED). Default = `ReadCommitted`.
1057#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1058pub enum IsolationLevel {
1059 ReadUncommitted,
1060 #[default]
1061 ReadCommitted,
1062 RepeatableRead,
1063 Serializable,
1064}
1065
1066impl IsolationLevel {
1067 /// Canonical PG-style display name, as `SHOW transaction_isolation`
1068 /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1069 /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1070 /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1071 /// `read uncommitted`) and only BEHAVES as read committed; the old
1072 /// fold renamed the label too.
1073 pub fn as_pg_str(self) -> &'static str {
1074 match self {
1075 Self::ReadUncommitted => "read uncommitted",
1076 Self::ReadCommitted => "read committed",
1077 Self::RepeatableRead => "repeatable read",
1078 Self::Serializable => "serializable",
1079 }
1080 }
1081}
1082
1083impl core::fmt::Display for IsolationLevel {
1084 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1085 f.write_str(self.as_pg_str())
1086 }
1087}
1088
1089/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1090/// single fixed-shape DDL; the WITH-clause options PG supports
1091/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1092/// scope for v6.1.4 — `enabled` defaults to true and there are
1093/// no other knobs to set in v6.1.x.
1094#[derive(Debug, Clone, PartialEq, Eq)]
1095pub struct CreateSubscriptionStatement {
1096 pub name: String,
1097 /// Connection string in PG keyword=value form (e.g.
1098 /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1099 /// `host` and `port` fields; the rest is reserved for
1100 /// future v6.1.x options.
1101 pub conn_str: String,
1102 /// One or more publications on the remote side. Order is
1103 /// preserved verbatim from the DDL; the worker requests them
1104 /// in this order. v6.1.4 records the list; v6.1.5
1105 /// publisher-side filtering enforces it.
1106 pub publications: Vec<String>,
1107}
1108
1109/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1110#[derive(Debug, Clone, PartialEq, Eq)]
1111pub struct CreateSequenceStatement {
1112 pub name: String,
1113 pub if_not_exists: bool,
1114 pub temporary: bool,
1115 /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1116 pub data_type: Option<SequenceDataType>,
1117 pub options: SequenceOptions,
1118}
1119
1120/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1122pub enum SequenceDataType {
1123 SmallInt,
1124 Int,
1125 BigInt,
1126}
1127
1128/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1129/// All fields are optional. `min_value`/`max_value` carry
1130/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1131#[derive(Debug, Clone, Default, PartialEq, Eq)]
1132pub struct SequenceOptions {
1133 pub increment: Option<i64>,
1134 pub min_value: Option<SeqBound>,
1135 pub max_value: Option<SeqBound>,
1136 pub start: Option<i64>,
1137 /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1138 /// RESTART, `Some(Some(n))` = RESTART WITH n.
1139 pub restart: Option<Option<i64>>,
1140 pub cache: Option<i64>,
1141 pub cycle: Option<bool>,
1142 pub owned_by: Option<SequenceOwnedBy>,
1143}
1144
1145/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1147pub enum SeqBound {
1148 Value(i64),
1149 NoBound,
1150}
1151
1152/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1153#[derive(Debug, Clone, PartialEq, Eq)]
1154pub enum SequenceOwnedBy {
1155 None,
1156 Column { table: String, column: String },
1157}
1158
1159/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1160#[derive(Debug, Clone, PartialEq)]
1161pub struct CreateMaterializedViewStatement {
1162 pub name: String,
1163 pub if_not_exists: bool,
1164 /// Optional `(col, col, …)` rename list. Applies to the
1165 /// backing table at CREATE / REFRESH time.
1166 pub columns: Vec<String>,
1167 /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1168 /// the cached rows.
1169 pub body: SelectStatement,
1170 /// `WITH DATA` (default) = materialise the rows at CREATE
1171 /// time. `WITH NO DATA` = create an empty backing table;
1172 /// callers must REFRESH before SELECT returns rows.
1173 pub with_data: bool,
1174 /// v7.38 (read01 P6.49) — when true this node came from
1175 /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1176 /// executor creates a plain table and does NOT register it in the
1177 /// materialized-view registry (no REFRESH semantics).
1178 pub as_plain_table: bool,
1179 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1180 /// meaningful together with `as_plain_table`; the executor puts the
1181 /// resulting table in the creating session's namespace.
1182 pub temporary: bool,
1183}
1184
1185/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1186/// auto-updatable view. `Cascaded` is PG's default when the bare
1187/// `WITH CHECK OPTION` is written.
1188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1189pub enum ViewCheckOption {
1190 Local,
1191 Cascaded,
1192}
1193
1194/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1195#[derive(Debug, Clone, PartialEq)]
1196pub struct CreateViewStatement {
1197 pub name: String,
1198 pub or_replace: bool,
1199 pub if_not_exists: bool,
1200 pub temporary: bool,
1201 /// Optional `(col, col, …)` rename list. When non-empty,
1202 /// these override the body's projected column names per-
1203 /// position at SELECT-from-view time.
1204 pub columns: Vec<String>,
1205 /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1206 /// time to materialise the view as a synthetic CTE.
1207 pub body: SelectStatement,
1208 /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1209 /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1210 /// 44000). `None` = no check option.
1211 pub check_option: Option<ViewCheckOption>,
1212}
1213
1214/// v7.17.0 — `ALTER SEQUENCE` AST node.
1215#[derive(Debug, Clone, PartialEq, Eq)]
1216pub struct AlterSequenceStatement {
1217 pub name: String,
1218 pub if_exists: bool,
1219 pub options: SequenceOptions,
1220 /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1221 /// instead of `options`; the two forms are mutually exclusive in PG.
1222 pub rename_to: Option<String>,
1223}
1224
1225/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1226/// the [`PublicationScope`] shape. v6.1.2 only accepted
1227/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1228/// variants by flipping the parser gate (no AST migration).
1229#[derive(Debug, Clone, PartialEq, Eq)]
1230pub struct CreatePublicationStatement {
1231 pub name: String,
1232 pub scope: PublicationScope,
1233}
1234
1235/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1236/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1237/// variants — the on-disk shape, snapshot serialisation, and the
1238/// AST round-trip Display path were already in place in v6.1.2
1239/// so this is a parser-only widening.
1240#[derive(Debug, Clone, PartialEq, Eq)]
1241pub enum PublicationScope {
1242 AllTables,
1243 ForTables(Vec<String>),
1244 AllTablesExcept(Vec<String>),
1245 /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1246 /// (PG 15+). AST-only: the executor folds `public` to
1247 /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1248 /// and refuses any other schema with PG's sentence, so the
1249 /// catalog / serializer / replication filter never see it.
1250 TablesInSchema(String),
1251}
1252
1253#[derive(Debug, Clone, PartialEq, Eq)]
1254pub struct AlterIndexStatement {
1255 pub name: String,
1256 pub target: AlterIndexTarget,
1257}
1258
1259#[derive(Debug, Clone, PartialEq, Eq)]
1260pub enum AlterIndexTarget {
1261 /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1262 /// rebuilds the existing graph in place without touching the
1263 /// column encoding; `Some(enc)` re-encodes every cell first.
1264 Rebuild { encoding: Option<VecEncoding> },
1265 /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1266 /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1267 /// uses it to make the migration idempotent (re-running on a
1268 /// DB where the rename already happened is a no-op rather
1269 /// than an error).
1270 Rename { new: String, if_exists: bool },
1271 /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1272 /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1273 /// does not exist`), so the index is validated and the storage
1274 /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1275 /// SET/RESET arms already record).
1276 StorageParams,
1277}
1278
1279/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1280/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1281/// can add more SET subjects without changing the dispatch shape.
1282#[derive(Debug, Clone, PartialEq)]
1283pub struct AlterTableStatement {
1284 pub name: String,
1285 /// v7.13.2 — mailrs round-6 S1. One or more subactions
1286 /// separated by commas in the source SQL. PG-semantic apply
1287 /// is sequential; engine bails on first error (no
1288 /// transactional rollback of completed subactions in v7.13).
1289 /// Single-subaction shape stays a 1-element vec.
1290 pub targets: Vec<AlterTableTarget>,
1291}
1292
1293#[derive(Debug, Clone, PartialEq)]
1294#[allow(clippy::large_enum_variant)]
1295pub enum AlterTableTarget {
1296 /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1297 ///
1298 /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1299 /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1300 /// the reasoning went stale: `NO INHERIT` reported success while the
1301 /// child stayed attached, which is the worst kind of answer — the
1302 /// statement says it worked and the catalog disagrees.
1303 Inherit { parent: String, detach: bool },
1304 /// Per-table hot-tier byte budget override. The freezer
1305 /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1306 SetHotTierBytes(u64),
1307 /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1308 /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1309 /// Engine validates existing rows against the new constraint
1310 /// before installing it.
1311 AddForeignKey(ForeignKeyConstraint),
1312 /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1313 /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1314 /// no-op when no FK with that name exists; otherwise raises.
1315 DropForeignKey { name: String, if_exists: bool },
1316 /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1317 /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1318 /// as the standalone `DROP INDEX` statement.
1319 DropIndex { name: String, if_exists: bool },
1320 /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1321 /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1322 /// (20 migrate-*.sql hits). Engine appends the column to the
1323 /// schema and back-fills every existing row with the DEFAULT
1324 /// (or NULL when no DEFAULT and the column is nullable).
1325 AddColumn {
1326 column: ColumnDef,
1327 if_not_exists: bool,
1328 },
1329 /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1330 /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1331 /// existing row's column value by evaluating the optional
1332 /// USING expression (default `col::<ty>`) and re-coercing
1333 /// against the new column type.
1334 AlterColumnType {
1335 column: String,
1336 new_type: ColumnTypeName,
1337 using: Option<Expr>,
1338 /// v7.39 (round 713) — `COLLATE <name>` between the type and
1339 /// USING. PG re-collates the column, and an ABSENT clause RESETS
1340 /// the collation to the type default (measured round 713) — so
1341 /// `None` is not "leave it alone". The type parser consumed the
1342 /// clause all along and this surface dropped it on the floor:
1343 /// the statement succeeded and the ordering did not change, the
1344 /// silent-divergence shape. Folded variant + the name as written.
1345 collation: Option<(Collation, String)>,
1346 },
1347 /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1348 /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1349 /// every row's value at that position is removed; any index
1350 /// on the column is dropped. `if_exists` makes the drop a
1351 /// no-op when the column is missing. `cascade` removes
1352 /// dependents (FKs referencing the column, partial indexes
1353 /// whose predicate names the column); without it, the engine
1354 /// rejects when dependents exist.
1355 DropColumn {
1356 column: String,
1357 if_exists: bool,
1358 cascade: bool,
1359 },
1360 /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1361 /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1362 /// CONSTRAINT name CHECK (expr)` — table-level constraints
1363 /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1364 /// separate ALTER TABLE statement, so this surface lets the
1365 /// dump load straight through.
1366 AddTableConstraint(TableConstraint),
1367 /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1368 /// there is nothing to record; what PG does that SPG did not is
1369 /// REFUSE a role that does not exist. The name has to reach the
1370 /// engine for that, because only the engine knows the roles.
1371 OwnerTo { role: String },
1372 /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1373 /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1374 /// the hint is still a no-op; naming an index that does not exist is
1375 /// not.
1376 ClusterOn { index: Option<String> },
1377 /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1378 /// already in the table against a constraint added `NOT VALID` and,
1379 /// if they all pass, mark it validated. It used to be swallowed as a
1380 /// no-op on the theory that SPG validated at ADD time; SPG did not.
1381 ValidateConstraint { name: String },
1382 /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1383 /// Renames the column in the schema and propagates the rename
1384 /// to every stored source string that references it as a
1385 /// (potentially-qualified) column identifier: CHECK predicates,
1386 /// partial-index predicates, runtime DEFAULT expressions, and
1387 /// triggers' `UPDATE OF` column lists. Function bodies and
1388 /// trigger bodies are NOT auto-rewritten — they're loose
1389 /// source text and may contain references SPG can't statically
1390 /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1391 /// the column even if dependents exist; users renaming a
1392 /// column referenced by a function body update the function
1393 /// body separately.
1394 RenameColumn { old: String, new: String },
1395 /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1396 /// Reachable now that the schema stores user-supplied constraint names.
1397 RenameConstraint { old: String, new: String },
1398 /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1399 /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1400 /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1401 /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1402 /// (identity); both lower to this. SPG's auto-increment is
1403 /// max+1-scan based, so the dump's `setval(…)` calls stay
1404 /// no-ops without losing the sequence position.
1405 SetColumnAutoIncrement {
1406 column: String,
1407 /// The implicit sequence pg_dump names for an identity
1408 /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1409 /// nextval target for a serial default. The engine creates
1410 /// it if absent so the dump's later `setval(s, …)` lands.
1411 seq_name: Option<String>,
1412 },
1413 /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1414 /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1415 /// migrate-042 uses it). The engine moves the table entry
1416 /// in the catalog under the new name; child catalog state
1417 /// (FKs pointing at this table, triggers watching this
1418 /// table) tracks the rename through the storage layer.
1419 RenameTable { new: String },
1420 /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1421 /// { ALL | <name> }`. Toggles whether row-level triggers
1422 /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1423 /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1424 /// ENABLE epilogue around every table's data block so the
1425 /// rows already-computed in prod don't get re-rewritten
1426 /// (and so trigger-driven side effects like
1427 /// audit/queueing don't re-fire during a bulk reload).
1428 /// `which == TriggerSelector::All` toggles every trigger
1429 /// on the table; `Named(name)` toggles one trigger. The
1430 /// engine persists the disabled state on `TriggerDef.enabled`
1431 /// (catalog FILE_VERSION 25+) and the row-write paths skip
1432 /// the trigger when `!enabled`.
1433 SetTriggerEnabled {
1434 which: TriggerSelector,
1435 enabled: bool,
1436 },
1437 /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1438 /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1439 /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1440 /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1441 SetRowSecurity {
1442 enabled: Option<bool>,
1443 force: Option<bool>,
1444 },
1445 /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1446 /// <bounds>`. Promotes an existing table `child` to a partition
1447 /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1448 /// Engine validates that `child`'s columns are layout-compatible
1449 /// with `parent` and that every row in `child` satisfies the
1450 /// bound before installing the role.
1451 AttachPartition {
1452 child: String,
1453 bounds: PartitionOfBoundsAst,
1454 },
1455 /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1456 /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1457 /// to a standalone table (clears `partition_role`) and removes
1458 /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1459 /// is parser-accepted; engine performs the same atomic detach
1460 /// (single-engine, no replication lag — the PG semantics that
1461 /// require the two-phase split don't apply).
1462 DetachPartition {
1463 child: String,
1464 concurrently: bool,
1465 finalize: bool,
1466 },
1467 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1468 /// <expr>`. Engine re-parses + freezes the literal at this point,
1469 /// matching CREATE TABLE-side default semantics. Volatile shapes
1470 /// (`now()` / `nextval`) take the runtime-default path.
1471 AlterColumnSetDefault { column: String, default_expr: Expr },
1472 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1473 AlterColumnDropDefault { column: String },
1474 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1475 /// Engine validates that no existing row has NULL in that column
1476 /// before flipping the flag (PG semantics — partial NOT NULL
1477 /// would surface inconsistently).
1478 AlterColumnSetNotNull { column: String },
1479 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1480 AlterColumnDropNotNull { column: String },
1481 /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1482 /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1483 /// column's start value = 1). Engine records a next-value floor over
1484 /// SPG's max+1 identity allocation.
1485 AlterColumnRestart { column: String, with: Option<i64> },
1486 /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1487 /// EXPRESSION` turns a stored generated column into a plain column
1488 /// (its generation expression is removed; existing values are kept).
1489 AlterColumnDropExpression { column: String, if_exists: bool },
1490 /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1491 /// de-generate an identity column into a plain column.
1492 AlterColumnDropIdentity { column: String, if_exists: bool },
1493 /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1494 /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1495 /// expression and recomputes every existing row.
1496 AlterColumnSetExpression { column: String, expr: Expr },
1497 /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1498 /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1499 /// (PG: `type "x" does not exist`).
1500 OfType { type_name: String },
1501 /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1502 /// identity setting no-ops (SPG has no logical replication consumer);
1503 /// the INDEX must exist on this table (PG: `index "i" for table "t"
1504 /// does not exist`).
1505 ReplicaIdentityUsingIndex { index: String },
1506}
1507
1508/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1509/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1510/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1511/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1512/// shouldn't surface from a dump.
1513#[derive(Debug, Clone, PartialEq, Eq)]
1514pub enum TriggerSelector {
1515 /// Every trigger on the table.
1516 All,
1517 /// A specific trigger by name.
1518 Named(String),
1519}
1520
1521/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1522/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1523/// bitflags word or a nested options struct would only relocate the lint
1524/// while making the option each caller sets harder to read.
1525#[allow(clippy::struct_excessive_bools)]
1526#[derive(Debug, Clone, PartialEq)]
1527pub struct ExplainStatement {
1528 pub analyze: bool,
1529 /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1530 /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1531 /// `Insert on / Update on / Delete on` trees for them.
1532 pub inner: Box<Statement>,
1533 /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1534 /// advisor pass: after the regular plan tree, the engine
1535 /// emits one suggestion line per column referenced in the
1536 /// query's WHERE / JOIN that has no covering index on the
1537 /// owning table.
1538 pub suggest: bool,
1539 /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1540 /// `elapsed=…us` annotations from the Total line (and any
1541 /// future cost-bearing lines). PG-standard option used by
1542 /// regression suites and diff-friendly EXPLAIN output. When
1543 /// `true`, takes precedence over the per-session
1544 /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1545 pub costs_off: bool,
1546 /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1547 /// option that surfaces hot/cold/shared block counters. SPG's
1548 /// hot-tier scan path counts examined rows; the BUFFERS option
1549 /// makes that an explicit per-operator annotation.
1550 pub buffers: bool,
1551 /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1552 /// uses this to disable per-operator timing while still
1553 /// emitting actual-row counts (cheaper than ANALYZE). Default
1554 /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1555 /// timing portion of the Total line. Decoupled from `costs_off`:
1556 /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1557 /// measured wall-clock.
1558 pub timing_off: bool,
1559 /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1560 /// modified GUC values to the plan output. SPG emits the
1561 /// session params that diverge from default after the main
1562 /// plan body.
1563 pub settings: bool,
1564 /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1565 /// bytes / records / FPI emitted by the query. SPG's
1566 /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1567 /// ANALYZE) report against the engine WAL counter delta.
1568 pub wal: bool,
1569 /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1570 /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1571 /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1572 /// this is set.
1573 pub summary_off: bool,
1574 /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1575 /// PG's standard format selector. Default is text. JSON / XML
1576 /// / YAML emit a single-row TEXT result whose body wraps the
1577 /// existing line-per-operator text in the chosen container —
1578 /// PG-compatible just enough for dashboards that parse those
1579 /// container shapes (pgAdmin's JSON path picker, etc.).
1580 pub format: ExplainFormat,
1581}
1582
1583#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1584pub enum ExplainFormat {
1585 #[default]
1586 Text,
1587 Json,
1588 Xml,
1589 Yaml,
1590}
1591
1592/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1594pub enum PolicyCmd {
1595 All,
1596 Select,
1597 Insert,
1598 Update,
1599 Delete,
1600}
1601
1602/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1603/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1604#[derive(Debug, Clone, PartialEq)]
1605pub struct CreatePolicyStatement {
1606 pub name: String,
1607 pub table: String,
1608 /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1609 pub permissive: bool,
1610 pub cmd: PolicyCmd,
1611 /// Empty = PUBLIC.
1612 pub roles: Vec<String>,
1613 pub using: Option<Expr>,
1614 pub with_check: Option<Expr>,
1615}
1616
1617/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1618/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1619/// or the command (matches PG).
1620#[derive(Debug, Clone, PartialEq)]
1621pub struct AlterPolicyStatement {
1622 pub name: String,
1623 pub table: String,
1624 pub rename_to: Option<String>,
1625 pub roles: Option<Vec<String>>,
1626 pub using: Option<Expr>,
1627 pub with_check: Option<Expr>,
1628}
1629
1630/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1631#[derive(Debug, Clone, PartialEq, Eq)]
1632pub struct DropPolicyStatement {
1633 pub name: String,
1634 pub table: String,
1635 pub if_exists: bool,
1636}
1637
1638#[derive(Debug, Clone, PartialEq, Eq)]
1639pub struct CreateUserStatement {
1640 pub name: String,
1641 /// Empty when the statement carried no PASSWORD — legal for a bare
1642 /// `CREATE ROLE`, which cannot log in anyway.
1643 pub password: String,
1644 /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1645 /// the parser; the engine validates against `Role::parse` so a
1646 /// typo lands as a runtime error with a clear message rather than
1647 /// a parse failure.
1648 pub role: String,
1649 /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1650 /// statement did not say, so the default for its spelling applies:
1651 /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1652 /// both default to INHERIT and NOSUPERUSER.
1653 pub login: Option<bool>,
1654 pub inherit: Option<bool>,
1655 pub superuser: Option<bool>,
1656 /// `true` when spelled `CREATE USER` (LOGIN by default).
1657 pub is_user: bool,
1658}
1659
1660/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1661/// it tells the planner how far a call may be moved or folded. SPG records
1662/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1663/// yet exploit it for constant folding.
1664#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1665pub enum FunctionVolatility {
1666 Immutable,
1667 Stable,
1668 #[default]
1669 Volatile,
1670}
1671
1672impl FunctionVolatility {
1673 /// PG's one-character `pg_proc.provolatile` code.
1674 #[must_use]
1675 pub const fn as_pg_char(self) -> &'static str {
1676 match self {
1677 Self::Immutable => "i",
1678 Self::Stable => "s",
1679 Self::Volatile => "v",
1680 }
1681 }
1682
1683 #[must_use]
1684 pub const fn as_sql(self) -> &'static str {
1685 match self {
1686 Self::Immutable => "IMMUTABLE",
1687 Self::Stable => "STABLE",
1688 Self::Volatile => "VOLATILE",
1689 }
1690 }
1691}
1692
1693/// v7.39 (round 322, V46) — PG's parallel-safety class.
1694#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1695pub enum FunctionParallel {
1696 #[default]
1697 Unsafe,
1698 Restricted,
1699 Safe,
1700}
1701
1702impl FunctionParallel {
1703 /// PG's one-character `pg_proc.proparallel` code.
1704 #[must_use]
1705 pub const fn as_pg_char(self) -> &'static str {
1706 match self {
1707 Self::Unsafe => "u",
1708 Self::Restricted => "r",
1709 Self::Safe => "s",
1710 }
1711 }
1712
1713 #[must_use]
1714 pub const fn as_sql(self) -> &'static str {
1715 match self {
1716 Self::Unsafe => "PARALLEL UNSAFE",
1717 Self::Restricted => "PARALLEL RESTRICTED",
1718 Self::Safe => "PARALLEL SAFE",
1719 }
1720 }
1721}
1722
1723/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1724/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1725/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1726/// language's default cost / rows.
1727#[derive(Debug, Clone, Copy, PartialEq, Default)]
1728pub struct FunctionAttrs {
1729 pub volatility: FunctionVolatility,
1730 /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1731 /// argument returns NULL without running the body.
1732 pub strict: bool,
1733 pub security_definer: bool,
1734 pub leakproof: bool,
1735 pub parallel: FunctionParallel,
1736 /// `COST n` — `None` leaves PG's per-language default.
1737 pub cost: Option<f64>,
1738 /// `ROWS n` — set-returning functions only; `None` = default.
1739 pub rows: Option<f64>,
1740}
1741
1742impl FunctionAttrs {
1743 /// The attribute words `pg_get_functiondef` puts on their own line,
1744 /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1745 /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1746 /// at its default — PG then emits no such line at all.
1747 #[must_use]
1748 pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1749 let mut out = alloc::vec::Vec::new();
1750 if self.volatility != FunctionVolatility::Volatile {
1751 out.push(alloc::string::String::from(self.volatility.as_sql()));
1752 }
1753 if self.parallel != FunctionParallel::Unsafe {
1754 out.push(alloc::string::String::from(self.parallel.as_sql()));
1755 }
1756 if self.strict {
1757 out.push(alloc::string::String::from("STRICT"));
1758 }
1759 if self.security_definer {
1760 out.push(alloc::string::String::from("SECURITY DEFINER"));
1761 }
1762 if self.leakproof {
1763 out.push(alloc::string::String::from("LEAKPROOF"));
1764 }
1765 if let Some(c) = self.cost {
1766 out.push(alloc::format!("COST {}", render_attr_number(c)));
1767 }
1768 if let Some(r) = self.rows {
1769 out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1770 }
1771 out
1772 }
1773}
1774
1775/// PG prints a whole-numbered cost / rows without a decimal point.
1776fn render_attr_number(v: f64) -> alloc::string::String {
1777 // no_std: `f64::fract` lives in std, so compare against the truncation.
1778 let whole = v as i64;
1779 if v.abs() < 1e15 && (whole as f64) == v {
1780 alloc::format!("{whole}")
1781 } else {
1782 alloc::format!("{v}")
1783 }
1784}
1785
1786/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1787/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1788/// (the row-level trigger body the CREATE TRIGGER below references).
1789/// Non-trigger user-defined functions parse but error at execution
1790/// time with a clear unsupported message; that surface lands in
1791/// v7.12.5+.
1792#[derive(Debug, Clone, PartialEq)]
1793pub struct CreateFunctionStatement {
1794 pub name: String,
1795 /// `OR REPLACE` was present; an existing function with the
1796 /// same name is overwritten instead of erroring.
1797 pub or_replace: bool,
1798 /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1799 /// list `()` (sufficient for trigger functions). Other shapes
1800 /// parse and store the args but the executor refuses to call
1801 /// them.
1802 pub args: Vec<FunctionArg>,
1803 /// `RETURNS <type>` — `trigger` is the supported shape for
1804 /// v7.12.4; arbitrary return types parse to
1805 /// [`FunctionReturn::Other`].
1806 pub returns: FunctionReturn,
1807 /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1808 /// side of `AS $$...$$`; the parser canonicalises to one slot.
1809 /// `plpgsql` and `sql` are the two interesting values.
1810 pub language: String,
1811 /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1812 /// a structured AST; non-trigger / non-plpgsql bodies stay as
1813 /// the raw source text so the v7.12.5+ executor can pick them
1814 /// up without a parser rev.
1815 pub body: FunctionBody,
1816 /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1817 /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1818 /// on either side of the body; before this they were a parse error, so
1819 /// PG's own `pg_dump` output would not restore.
1820 pub attrs: FunctionAttrs,
1821}
1822
1823/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1824#[derive(Debug, Clone, PartialEq)]
1825pub struct FunctionArg {
1826 /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1827 /// (the default); `OUT` / `INOUT` parse but the executor
1828 /// refuses them.
1829 pub mode: FunctionArgMode,
1830 /// Optional arg name. Trigger functions traditionally don't
1831 /// name their args (they read NEW/OLD instead), so `None` is
1832 /// the common case.
1833 pub name: Option<String>,
1834 /// Declared type, normalised to the SPG `DataType` mapping
1835 /// where one exists. Unknown / extension types parse as a
1836 /// raw string under [`FunctionArgType::Raw`].
1837 pub ty: FunctionArgType,
1838}
1839
1840#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1841pub enum FunctionArgMode {
1842 In,
1843 Out,
1844 InOut,
1845}
1846
1847#[derive(Debug, Clone, PartialEq)]
1848pub enum FunctionArgType {
1849 Typed(ColumnTypeName),
1850 /// Unknown / extension types — kept as the parser-side raw
1851 /// identifier so error messages can name them precisely.
1852 Raw(String),
1853}
1854
1855#[derive(Debug, Clone, PartialEq)]
1856pub enum FunctionReturn {
1857 /// `RETURNS TRIGGER` — the row-level trigger function shape.
1858 /// v7.12.4 ships exactly this for execution.
1859 Trigger,
1860 /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
1861 /// the function is unused (since v7.12.4 doesn't ship scalar
1862 /// function invocation).
1863 Void,
1864 /// `RETURNS <type>` for any concrete data type. Reserved for
1865 /// v7.12.5+'s scalar UDF surface.
1866 Type(ColumnTypeName),
1867 /// `RETURNS <ident>` for types SPG doesn't know — extension
1868 /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
1869 Other(String),
1870}
1871
1872#[derive(Debug, Clone, PartialEq)]
1873pub enum FunctionBody {
1874 /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
1875 /// trigger-function executor walks this directly without
1876 /// re-parsing.
1877 PlPgSql(PlPgSqlBlock),
1878 /// Raw source text — parser couldn't (or didn't try to)
1879 /// structure-parse the body. Used for `LANGUAGE sql`
1880 /// functions and any PL/pgSQL body that contains v7.12.5+
1881 /// features the v7.12.4 parser doesn't yet recognise. The
1882 /// executor returns an unsupported error when invoked.
1883 Raw(String),
1884}
1885
1886/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
1887/// from assignment + return to a real-PL/pgSQL surface:
1888/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
1889/// control flow, `RAISE` diagnostics, and embedded SQL
1890/// statements that execute through the regular engine path.
1891/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
1892/// which mailrs's trigger doesn't need but other PG customers
1893/// may; deferred to a future minor release.
1894#[derive(Debug, Clone, PartialEq)]
1895pub struct PlPgSqlBlock {
1896 /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
1897 /// preceding `BEGIN`. Empty when the body opens directly with
1898 /// `BEGIN`. Declarations execute in order; each may reference
1899 /// earlier-declared locals in its init expression.
1900 pub declarations: Vec<PlPgSqlDeclare>,
1901 pub statements: Vec<PlPgSqlStmt>,
1902 /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
1903 /// <body>` handlers appended to the block. Empty when no
1904 /// EXCEPTION clause is present. When a body statement raises
1905 /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
1906 /// handlers are tried in order; the first matching condition
1907 /// runs its body and the block terminates cleanly. `OTHERS`
1908 /// matches any exception. Unhandled exceptions propagate.
1909 pub exception_handlers: Vec<ExceptionHandler>,
1910}
1911
1912/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
1913/// arm inside an EXCEPTION block.
1914#[derive(Debug, Clone, PartialEq)]
1915pub struct ExceptionHandler {
1916 /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
1917 /// conditions joined by `OR` share one handler body.
1918 pub conditions: Vec<String>,
1919 /// Statements to run when a matching exception is caught.
1920 pub body: Vec<PlPgSqlStmt>,
1921}
1922
1923/// v7.12.6 — single `DECLARE` entry: variable name + declared
1924/// type + optional initialiser. Variables default to SQL NULL
1925/// when no init is given (matches PG).
1926#[derive(Debug, Clone, PartialEq)]
1927pub struct PlPgSqlDeclare {
1928 pub name: String,
1929 /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
1930 /// knows it; raw text otherwise).
1931 pub ty: FunctionArgType,
1932 pub default: Option<Expr>,
1933}
1934
1935#[derive(Debug, Clone, PartialEq)]
1936pub enum PlPgSqlStmt {
1937 /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
1938 /// for clarity in error reporting (PG also forbids it) — the
1939 /// executor errors with a clear "OLD is read-only" message.
1940 Assign { target: AssignTarget, value: Expr },
1941 /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
1942 /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
1943 /// the SELECT statement with the INTO clause stripped; the
1944 /// engine runs it via `Engine::execute`, takes the first
1945 /// row's first column, and assigns to the local variable
1946 /// in the DECLARE scope. Single-column / single-row
1947 /// queries only at v7.16.2; multi-target (`INTO a, b`) is
1948 /// a v7.16.x follow-up.
1949 SelectInto {
1950 var: String,
1951 body: Box<SelectStatement>,
1952 },
1953 /// `RETURN <target>;` — trigger functions canonically return
1954 /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
1955 /// expression for forward compatibility with scalar UDFs.
1956 Return(ReturnTarget),
1957 /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
1958 /// set a SETOF function is building, and KEEP GOING. Not a return.
1959 ReturnNext(Expr),
1960 /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
1961 /// query yields, and keep going. It used to desugar to a side-effect
1962 /// statement whose result was DISCARDED — in a SETOF function that is the
1963 /// whole answer thrown away.
1964 ReturnQuery(Box<SelectStatement>),
1965 /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
1966 /// twin. Its rows go to the set too; it used to run and discard them.
1967 ReturnQueryExecute { sql: Expr },
1968 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
1969 /// [ELSE body] END IF;`. Branches are tried in order; first
1970 /// truthy condition wins; the optional ELSE runs when no
1971 /// condition matched.
1972 If {
1973 branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
1974 else_branch: Vec<PlPgSqlStmt>,
1975 },
1976 /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
1977 /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
1978 /// (logging — observable side effect only) or `EXCEPTION`
1979 /// (aborts the trigger and propagates as an error). v7.12.6
1980 /// supports the basic format-string substitution PG uses
1981 /// (`%` placeholders consumed positionally).
1982 Raise {
1983 level: RaiseLevel,
1984 message: String,
1985 args: Vec<Expr>,
1986 },
1987 /// v7.12.6 — embedded SQL statement inside the trigger body
1988 /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
1989 /// NEW.col / OLD.col references inside the embedded
1990 /// statement's expression tree are substituted with the
1991 /// current trigger context before the engine re-executes the
1992 /// statement. Recursion depth into nested triggers is
1993 /// bounded by the engine's existing trigger-fire guard.
1994 EmbeddedSql(Box<Statement>),
1995 /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
1996 /// the condition evaluates falsy the trigger / DO block aborts
1997 /// with the message (defaulting to a generic shape when none
1998 /// is provided). Same propagation shape as `RAISE EXCEPTION`
1999 /// — the error reaches the caller's query path. PG's behaviour
2000 /// is identical except for a `plpgsql.check_asserts` GUC that
2001 /// can disable the check globally; SPG always evaluates.
2002 Assert {
2003 condition: Expr,
2004 message: Option<Expr>,
2005 },
2006 /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2007 /// Iterate the body while condition evaluates truthy. Iteration
2008 /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2009 /// loops; the executor errors out when reached. EXIT / CONTINUE
2010 /// inside the body queue with 20.2.
2011 While {
2012 condition: Expr,
2013 body: Vec<PlPgSqlStmt>,
2014 },
2015 /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2016 /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2017 /// bounds inclusive on both sides. REVERSE walks backward.
2018 /// Iteration budget guards runaway.
2019 ForRange {
2020 var: String,
2021 start: Expr,
2022 end: Expr,
2023 reverse: bool,
2024 body: Vec<PlPgSqlStmt>,
2025 },
2026 /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2027 /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2028 /// budget guards runaway.
2029 Loop { body: Vec<PlPgSqlStmt> },
2030 /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2031 /// Unconditional (no WHEN) or conditional (only breaks when
2032 /// condition is truthy). Bubbles up as BodyOutcome::Break which
2033 /// the enclosing loop catches. Outside a loop it's a no-op.
2034 Exit { when: Option<Expr> },
2035 /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2036 /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2037 /// which the enclosing loop catches, skipping the remainder of
2038 /// the body and jumping to the next iteration.
2039 Continue { when: Option<Expr> },
2040 /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2041 /// computed SQL statement. The expression is evaluated to a
2042 /// text value, the resulting string is parsed and dispatched
2043 /// through the engine like an EmbeddedSql. USING <param_list>
2044 /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2045 ExecuteDynamic { sql: Expr },
2046 /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2047 /// END LOOP;`. Runs the SELECT once, iterates the resulting
2048 /// rows, binds the first column of each row to `var` as a
2049 /// scalar Value, then runs the body per iteration. EXIT /
2050 /// CONTINUE / ASSERT / RAISE etc. propagate through the
2051 /// enclosing loop's BodyOutcome discipline the same way
2052 /// FOR range and WHILE do. Full record-binding (var as
2053 /// composite carrying all columns) queues with v7.40 record
2054 /// type infrastructure.
2055 ForQuery {
2056 var: String,
2057 query: Box<SelectStatement>,
2058 body: Vec<PlPgSqlStmt>,
2059 },
2060 /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2061 /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2062 /// computed at runtime from a text expression, parsed on the
2063 /// fly, then iterated. Enables dynamic queries where the
2064 /// projection / FROM / WHERE clauses depend on runtime values.
2065 ForExecute {
2066 var: String,
2067 sql_expr: Expr,
2068 body: Vec<PlPgSqlStmt>,
2069 },
2070}
2071
2072#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2073pub enum RaiseLevel {
2074 /// `RAISE NOTICE` — diagnostic message, observable in the
2075 /// server log. Does not affect the trigger's outcome.
2076 Notice,
2077 /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2078 Warning,
2079 /// `RAISE INFO` — like NOTICE, slightly quieter.
2080 Info,
2081 /// `RAISE LOG` — like NOTICE, lower priority.
2082 Log,
2083 /// `RAISE DEBUG` — like NOTICE, lowest priority.
2084 Debug,
2085 /// `RAISE EXCEPTION` — aborts the trigger function with the
2086 /// given message, propagating up to the caller as a query-
2087 /// level error.
2088 Exception,
2089}
2090
2091#[derive(Debug, Clone, PartialEq)]
2092pub enum AssignTarget {
2093 NewColumn(String),
2094 OldColumn(String),
2095 /// Reserved for v7.12.5 DECLARE'd local variables.
2096 Local(String),
2097}
2098
2099#[derive(Debug, Clone, PartialEq)]
2100pub enum ReturnTarget {
2101 /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2102 /// actually gets written (possibly with NEW.col mutations
2103 /// applied). For AFTER triggers, the return value is ignored.
2104 New,
2105 /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2106 /// the delete proceed; for BEFORE UPDATE / INSERT it's
2107 /// equivalent to dropping the write.
2108 Old,
2109 /// `RETURN NULL;` — for BEFORE triggers, skips the write
2110 /// entirely. For AFTER, the return value is ignored.
2111 Null,
2112 /// `RETURN <expr>;` — non-row return shape; reserved for the
2113 /// scalar UDF surface in v7.12.5+. Executor errors when used
2114 /// inside a trigger function.
2115 Expr(Expr),
2116}
2117
2118/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2119/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2120/// but the executor refuses them. `WHEN (cond)` clauses are out
2121/// of scope; the trigger function can short-circuit on a leading
2122/// IF inside its body once v7.12.5 lands IF.
2123#[derive(Debug, Clone, PartialEq)]
2124pub struct CreateTriggerStatement {
2125 pub name: String,
2126 pub or_replace: bool,
2127 pub timing: TriggerTiming,
2128 /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2129 /// three entries in order.
2130 pub events: Vec<TriggerEvent>,
2131 pub table: String,
2132 /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2133 /// only `Row`; `Statement` parses but the executor refuses.
2134 pub for_each: TriggerForEach,
2135 /// Name of the function to invoke. The function must exist at
2136 /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2137 /// forward reference (`function no_such_fn() does not exist`), so
2138 /// requiring it IS the PG behaviour (the old note claimed the
2139 /// opposite).
2140 pub function: String,
2141 /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2142 /// (mailrs round-5 G7). Non-empty only when the events list
2143 /// contains UPDATE and the user wrote the column-list filter.
2144 /// PG fires the trigger only when at least one of these
2145 /// columns appears in the SET clause; SPG conservatively
2146 /// fires on any UPDATE matching the listed columns or
2147 /// rewriting them at the row level. Empty vec = no filter
2148 /// (fire on every UPDATE).
2149 pub update_columns: Vec<String>,
2150 /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2151 /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2152 /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2153 pub when_condition: Option<Expr>,
2154}
2155
2156/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2157#[derive(Debug, Clone, PartialEq)]
2158pub struct CreateRuleStatement {
2159 pub name: String,
2160 pub or_replace: bool,
2161 /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2162 pub event: String,
2163 pub table: String,
2164 /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2165 /// (run alongside; PG's default when neither keyword is written).
2166 pub instead: bool,
2167 /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2168 pub when_condition: Option<Expr>,
2169 /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2170 pub commands: Vec<Statement>,
2171}
2172
2173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2174pub enum TriggerTiming {
2175 /// Fires before the row is written; the trigger function's
2176 /// return value (NEW or NULL) decides the row content and
2177 /// whether the write proceeds at all.
2178 Before,
2179 /// Fires after the row is written; the return value is
2180 /// ignored.
2181 After,
2182 /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2183 /// v7.12.4 (SPG has no updatable-view surface).
2184 InsteadOf,
2185}
2186
2187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2188pub enum TriggerEvent {
2189 Insert,
2190 Update,
2191 Delete,
2192 /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2193 /// so the trigger never fires.
2194 Truncate,
2195}
2196
2197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2198pub enum TriggerForEach {
2199 Row,
2200 Statement,
2201}
2202
2203/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2204///
2205/// SPG's index does not scan in a direction, but `indexdef` reproduces
2206/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2207/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2208/// which case PG's default applies — LAST for ascending, FIRST for
2209/// descending, and neither is rendered.
2210#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2211pub struct IndexColumnOrder {
2212 pub descending: bool,
2213 pub nulls_first: Option<bool>,
2214}
2215
2216#[derive(Debug, Clone, PartialEq)]
2217pub struct CreateIndexStatement {
2218 pub name: String,
2219 /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2220 /// either way, so this changes nothing about how the index is made
2221 /// — it is carried because PG refuses the CONCURRENTLY form inside
2222 /// a transaction block and accepts the plain one, and the engine
2223 /// cannot tell them apart without it.
2224 pub concurrently: bool,
2225 /// v7.39 (round 537) — the leading key column's ordering clause,
2226 /// which is the column SPG indexes.
2227 pub key_order: IndexColumnOrder,
2228 /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2229 /// written. SPG orders text by bytes, so honouring it changes
2230 /// nothing; PG prints it, because an explicitly named collation and
2231 /// the one a column inherits are different objects.
2232 pub key_collation: Option<String>,
2233 pub table: String,
2234 pub column: String,
2235 /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2236 /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2237 /// any NULL in the key exempts the row from the uniqueness check.
2238 pub nulls_not_distinct: bool,
2239 /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2240 /// graph for vector kNN); unspecified is the default B-tree index.
2241 pub method: IndexMethod,
2242 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2243 /// index name already exists, instead of raising `DuplicateIndex`.
2244 pub if_not_exists: bool,
2245 /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2246 /// non-key columns the planner should treat as "covered" by
2247 /// this index when checking whether a query can run as an
2248 /// index-only scan. Empty when no `INCLUDE` clause was given.
2249 pub included_columns: Vec<String>,
2250 /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2251 /// for which `<expr>` evaluates truthy enter the index;
2252 /// queries whose `WHERE` clause's canonical Display form
2253 /// matches this expression's Display form can be served by the
2254 /// partial index. Stored as a parsed `Expr` so the engine
2255 /// re-uses the existing evaluation path; storage persists the
2256 /// Display form on the catalog snapshot.
2257 pub partial_predicate: Option<Expr>,
2258 /// v6.8.2 — expression-based index. When `Some(expr)`, the
2259 /// index key is the result of `expr` evaluated on each row
2260 /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2261 /// field still names the *primary* column the expression
2262 /// touches so existing planner shortcuts that resolve a
2263 /// column position stay valid. `None` = plain
2264 /// column-reference index (the legacy shape).
2265 pub expression: Option<Expr>,
2266 /// v7.9.14 — extra column names after the leading column in a
2267 /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2268 /// planner today still only uses the leading column for index
2269 /// seeks; the extras are tracked verbatim so the same DDL
2270 /// round-trips through WAL replay + catalog snapshot, and so
2271 /// the engine can emit a clear warning at INDEX CREATE time
2272 /// that only the leading column is currently honoured.
2273 /// Composite BTree index keys land in v7.10.
2274 pub extra_columns: Vec<String>,
2275 /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2276 /// enforces uniqueness on the indexed key (combined with the
2277 /// `partial_predicate` filter — only rows where the predicate
2278 /// evaluates truthy enter the uniqueness check). Standard SQL
2279 /// and PG's canonical way to express conditional uniqueness.
2280 /// mailrs K1.
2281 pub is_unique: bool,
2282 /// v7.15.0 — operator class on the leading column, when the
2283 /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2284 /// Lower-cased. Most opclasses are still informational; the
2285 /// engine routes on `gin_trgm_ops` specifically to build a
2286 /// trigram-shingle GIN over a TEXT column, and otherwise
2287 /// keeps the current "accepted and discarded" behaviour for
2288 /// pg_dump compatibility.
2289 pub opclass: Option<String>,
2290 /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2291 /// there was no `USING` clause.
2292 ///
2293 /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2294 /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2295 /// implementation for still load. That degradation is deliberate, but
2296 /// it loses the name — and the operator-class check needs it, both to
2297 /// look the class up under the AM the user actually named and to say
2298 /// which AM it was missing from, the way PG's message does.
2299 pub method_name: Option<String>,
2300}
2301
2302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2303pub enum IndexMethod {
2304 /// Default — B-tree over `IndexKey`. Used for equality / range
2305 /// lookups on scalar columns.
2306 BTree,
2307 /// `USING hnsw` — NSW graph for kNN over a vector column.
2308 Hnsw,
2309 /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2310 /// metadata that records (min_key, max_key) for each page in a
2311 /// cold-tier segment, on the indexed column. The optimizer
2312 /// can use these summaries to skip pages whose range does NOT
2313 /// overlap a query's WHERE predicate. BRIN indexes carry no
2314 /// in-memory data — the summaries live in the segment v2
2315 /// envelope's sidecar. Created via the standard
2316 /// `CREATE INDEX … USING brin (col)` syntax.
2317 Brin,
2318 /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2319 /// column. Posting lists map `lexeme word` → row locators; the
2320 /// planner uses them to narrow `WHERE col @@ tsquery` to the
2321 /// candidate rows whose vectors contain a matching term, then
2322 /// re-evaluates the full `@@` semantics on each candidate.
2323 /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2324 /// silently degraded to a full scan at query time.
2325 Gin,
2326}
2327
2328/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2329/// inside a CREATE TABLE column list.
2330///
2331/// The source table's shape can only be read from the catalog, so the
2332/// parser records the clause and the engine expands it. `at` is how many
2333/// explicit columns preceded it: PG keeps the written order, so
2334/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2335#[derive(Debug, Clone, PartialEq)]
2336pub struct LikeSpec {
2337 pub source: String,
2338 pub at: usize,
2339 pub options: LikeOptions,
2340}
2341
2342/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2343/// types and NOT NULL and nothing else — measured on PG18, where a
2344/// copied generated column becomes a plain one and a copied identity
2345/// column loses its identity.
2346#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2347pub struct LikeOptions {
2348 pub defaults: bool,
2349 pub constraints: bool,
2350 pub identity: bool,
2351 pub generated: bool,
2352 pub indexes: bool,
2353 pub comments: bool,
2354}
2355
2356#[derive(Debug, Clone, PartialEq)]
2357pub struct CreateTableStatement {
2358 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2359 /// creating session's own namespace: it shadows a permanent table of the
2360 /// same name, other sessions never see it, and it is dropped when the
2361 /// session ends. A `bool` here lands in the struct's existing padding.
2362 pub temporary: bool,
2363 pub name: String,
2364 pub columns: Vec<ColumnDef>,
2365 /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2366 /// the order written. Empty for a table that has none.
2367 pub like_specs: Vec<LikeSpec>,
2368 /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2369 /// Empty for a table that inherits from nothing. Order matters:
2370 /// the child takes each parent's columns in this order before its
2371 /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2372 pub inherits: Vec<String>,
2373 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2374 /// table name already exists, instead of raising `DuplicateTable`.
2375 pub if_not_exists: bool,
2376 /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2377 /// constraints. Column-level `REFERENCES` (single-column inline
2378 /// form) is normalised into this vec at parse time so the engine
2379 /// sees one uniform list.
2380 pub foreign_keys: Vec<ForeignKeyConstraint>,
2381 /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2382 /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2383 /// Engine resolves each into a BTree index named after the
2384 /// constraint's leading column at CREATE TABLE time; INSERT
2385 /// path enforces composite uniqueness via row scan on the
2386 /// leading column index.
2387 pub table_constraints: Vec<TableConstraint>,
2388 /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2389 /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2390 /// the engine creates a parent table whose own rows stay
2391 /// empty and routes INSERT/SELECT through children. Mutually
2392 /// exclusive with `partition_of` (parser enforces).
2393 pub partition_by: Option<PartitionBySpec>,
2394 /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2395 /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2396 /// the table inherits its column list from `parent` (the
2397 /// parser rejects an explicit column list when this is set);
2398 /// engine routes child rows back to the parent at INSERT.
2399 pub partition_of: Option<PartitionOfSpec>,
2400}
2401
2402/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2403/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2404/// future LIST / HASH without breaking the public AST shape.
2405#[derive(Debug, Clone, PartialEq)]
2406pub struct PartitionBySpec {
2407 pub kind: PartitionKindAst,
2408 /// One or more ident references into the parent's column list.
2409 /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2410 /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2411 /// shape PG-compatible.
2412 pub key_columns: Vec<String>,
2413}
2414
2415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2416pub enum PartitionKindAst {
2417 Range,
2418 /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2419 /// `FOR VALUES IN (lit, lit, …)`.
2420 List,
2421 /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2422 /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2423 Hash,
2424}
2425
2426/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2427/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2428/// or the catch-all `DEFAULT` partition.
2429#[derive(Debug, Clone, PartialEq)]
2430pub struct PartitionOfSpec {
2431 pub parent_name: String,
2432 pub bounds: PartitionOfBoundsAst,
2433}
2434
2435#[derive(Debug, Clone, PartialEq)]
2436pub enum PartitionOfBoundsAst {
2437 /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2438 /// (lits include vector bodies), so we box both bounds to keep
2439 /// the variant size in line with `Default` for clippy and to
2440 /// minimise per-statement footprint when the partition shape
2441 /// isn't in use.
2442 Range {
2443 lower: Box<Expr>,
2444 upper: Box<Expr>,
2445 },
2446 /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2447 /// expr resolves to a typed literal at child-create time.
2448 List {
2449 values: Vec<Expr>,
2450 },
2451 /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2452 /// PG enforces `0 ≤ r < m`; m must be positive.
2453 Hash {
2454 modulus: u32,
2455 remainder: u32,
2456 },
2457 Default,
2458}
2459
2460/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2461/// column list. Either a composite PRIMARY KEY or a UNIQUE
2462/// (single- or multi-column).
2463#[derive(Debug, Clone, PartialEq)]
2464pub enum TableConstraint {
2465 /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2466 /// referenced column. Engine builds a BTree index named
2467 /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2468 PrimaryKey {
2469 name: Option<String>,
2470 columns: Vec<String>,
2471 /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2472 /// Round 621 consumed the clauses; these carry them.
2473 deferrable: bool,
2474 initially_deferred: bool,
2475 },
2476 /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2477 /// named `<table>_<leading_col>_key` (single-column) or
2478 /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2479 /// uniqueness on INSERT.
2480 Unique {
2481 name: Option<String>,
2482 columns: Vec<String>,
2483 /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2484 /// G10). PG 15+ flips the NULL handling so any number of
2485 /// NULL rows collide on the constraint. Default is
2486 /// `false` (NULLS DISTINCT, standard SQL behaviour).
2487 nulls_not_distinct: bool,
2488 /// v7.39 (round 711) — see PrimaryKey.
2489 deferrable: bool,
2490 initially_deferred: bool,
2491 },
2492 /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2493 /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2494 /// this same variant at parse time. Engine evaluates the
2495 /// predicate against each INSERT/UPDATE candidate row; a
2496 /// false / NULL result rejects the mutation.
2497 /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2498 /// PG adds such a constraint without scanning the existing rows: new
2499 /// rows are checked, the ones already there are grandfathered in, and
2500 /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2501 /// scans and flips it. pg_dump emits the suffix for exactly those, so
2502 /// validating them on restore would refuse a dump PG itself produced.
2503 Check {
2504 name: Option<String>,
2505 expr: Expr,
2506 not_valid: bool,
2507 },
2508 /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2509 /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2510 /// every element (the booking/scheduling non-overlap constraint,
2511 /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2512 /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2513 /// enforcement doesn't build the index yet). Each element pairs a
2514 /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2515 Exclude {
2516 name: Option<String>,
2517 method: Option<String>,
2518 elements: Vec<(String, String)>,
2519 },
2520 /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2521 /// non-unique secondary-index declaration inline in CREATE
2522 /// TABLE. Engine builds a BTree index on the leading column
2523 /// (composite columns parse but only the leading column is
2524 /// honoured at v7.15 — matches the existing
2525 /// `CreateIndexStatement::extra_columns` semantics). Useful
2526 /// for `mysql/blog`-style schemas that lean on routine
2527 /// secondary indexes for ORM lookups.
2528 Index {
2529 name: Option<String>,
2530 columns: Vec<String>,
2531 },
2532 /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2533 /// (cols)` inline declaration. Pre-v7.17 the parser
2534 /// silently dropped these so MyISAM-imported FULLTEXT
2535 /// indexes vanished; v7.17 routes them through the
2536 /// existing tsvector-GIN engine path so MATCH AGAINST
2537 /// queries get a real inverted index instead of falling
2538 /// back to a full scan. Multi-column FULLTEXT KEYs build
2539 /// one GIN per column at v7.17 (per-column posting lists);
2540 /// the leading column drives query planning.
2541 FulltextIndex {
2542 name: Option<String>,
2543 columns: Vec<String>,
2544 },
2545}
2546
2547#[derive(Debug, Clone, PartialEq)]
2548#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2549pub struct ColumnDef {
2550 pub name: String,
2551 pub ty: ColumnTypeName,
2552 pub nullable: bool,
2553 /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2554 /// evaluates this once (with an empty row) and caches the resulting
2555 /// `Value` on the column schema.
2556 pub default: Option<Expr>,
2557 /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2558 /// per such column and fills the slot when INSERT leaves it
2559 /// unbound (omitted from a column-list INSERT or explicitly NULL).
2560 pub auto_increment: bool,
2561 /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2562 /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2563 /// an implicit BTree index named `<table>_pkey` over this
2564 /// column at CREATE TABLE time, satisfying the parent-side
2565 /// index requirement for any FOREIGN KEY pointing at it.
2566 pub is_primary_key: bool,
2567 /// v7.13.0 — inline `UNIQUE` column constraint
2568 /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2569 /// into a single-column `TableConstraint::Unique` so the
2570 /// engine path stays uniform with table-level UNIQUE.
2571 pub is_unique: bool,
2572 /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2573 /// inline column constraint: treat NULL keys as equal so only one NULL
2574 /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2575 /// `TableConstraint::Unique { nulls_not_distinct }`.
2576 pub unique_nulls_not_distinct: bool,
2577 /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2578 /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2579 /// since this round so the fold into the table-level constraint keeps it.
2580 pub constraint_deferrable: bool,
2581 pub constraint_initially_deferred: bool,
2582 /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2583 /// (mailrs round-5 G3). Stored alongside the column so the
2584 /// CREATE TABLE handler can fold these into table-level
2585 /// CHECK constraints. Multiple inline CHECKs on the same
2586 /// column are concatenated with AND at the table level.
2587 pub check: Option<Expr>,
2588 /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2589 /// parser sees an unknown column-type ident (anything not in
2590 /// the built-in `parse_column_type_name` table), it sets
2591 /// `ty = ColumnTypeName::Text` and records the original name
2592 /// here. The engine resolves at CREATE TABLE time: if a
2593 /// catalog enum/domain with this name exists, the column is
2594 /// bound to it (label-checked on INSERT for enums; CHECK-
2595 /// constrained for domains); otherwise the CREATE TABLE
2596 /// errors with "unknown type".
2597 pub user_type_ref: Option<String>,
2598 /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2599 /// CURRENT_TIMESTAMP` column attribute. When set, an
2600 /// UPDATE that does NOT explicitly bind this column
2601 /// overrides the new value with `now()` (engine clock).
2602 /// Pre-v7.17 SPG silently accepted the syntax and never
2603 /// fired the override — `updated_at` columns from mysqldump
2604 /// stayed pinned at their initial DEFAULT forever, an
2605 /// audit Tier-S silent-failure. Generalised as a stored
2606 /// expression source so future shapes (`ON UPDATE
2607 /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2608 /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2609 pub on_update_runtime: Option<Expr>,
2610 /// v7.17.0 Phase 2.5 — text collation derived from the
2611 /// post-fix `COLLATE <name>` clause (and / or the table-level
2612 /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2613 /// per column). Pre-2.5 SPG accepted the clause and
2614 /// discarded the name, leaving every column byte-compared
2615 /// — a Tier-S silent failure when the customer expected
2616 /// `_ci` / `case_insensitive` semantics. Parser normalises
2617 /// the raw collation name into the variants in `Collation`.
2618 /// Default `Binary` preserves the legacy compare path.
2619 pub collation: Collation,
2620 /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2621 /// explicit `COLLATE <name>` clause rather than the default. Under the
2622 /// MySQL dialect a text column with NO explicit clause takes the
2623 /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2624 /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2625 /// flag is the only thing that tells them apart.
2626 pub collation_explicit: bool,
2627 /// v7.39 (round 676) — the collation name AS WRITTEN, because
2628 /// `collation` above cannot carry it: `Collation` is a two-variant
2629 /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2630 /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2631 /// tell them apart.
2632 pub collation_name: Option<String>,
2633 /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2634 /// 4.4 SPG accepted and discarded the keyword, leaving
2635 /// negative values silently accepted on a column the
2636 /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2637 /// rejects negative INSERT / UPDATE values on UNSIGNED int
2638 /// columns. SPG widening to `u64`-shaped storage is out of
2639 /// v7.17 scope; the upper bound remains the signed-type max
2640 /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2641 /// exceeds what every mailrs / Rails app actually uses.
2642 pub is_unsigned: bool,
2643 /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2644 /// value list captured at parse time. When `Some`, the parser
2645 /// recognised `ENUM(...)` in the type slot; the engine
2646 /// validates INSERT cells against this list at
2647 /// column_def_to_schema time and persists the variants on
2648 /// `ColumnSchema.inline_enum_variants`. None for all
2649 /// non-ENUM columns.
2650 pub inline_enum_variants: Option<Vec<String>>,
2651 /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2652 /// value list. Distinct from ENUM (subset semantics rather
2653 /// than pick-one). None for all non-SET columns.
2654 pub inline_set_variants: Option<Vec<String>>,
2655 /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2656 /// STORED` computed-column source. When `Some`, the engine
2657 /// stores the Display-form of the parsed expression on
2658 /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2659 /// and re-evaluates the expression against every INSERT /
2660 /// UPDATE candidate row, overwriting whatever the caller
2661 /// supplied for this column. Boxed to keep `ColumnDef` from
2662 /// blowing past the `large_enum_variant` clippy ceiling
2663 /// (`Expr` widens with vector literals).
2664 pub generated_stored_expr: Option<Box<Expr>>,
2665 /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2666 /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2667 /// `auto_increment`; this additionally marks the ALWAYS one, whose
2668 /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2669 /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2670 /// VALUE`. Only meaningful when the column is also an identity column.
2671 pub identity_always: bool,
2672 /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2673 /// integer width (TINYINT / MEDIUMINT), captured before the type
2674 /// collapses to SmallInt / Int. The engine copies it to
2675 /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2676 /// path can enforce the real range. None for every other column and
2677 /// under the PG dialect.
2678 pub mysql_int_width: Option<MysqlIntWidth>,
2679 /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2680 /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2681 /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2682 /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2683 /// CREATE TABLE time so the write path can truncate and the render path
2684 /// can pad. None under the PG dialect, where temporal columns keep full
2685 /// microseconds.
2686 pub mysql_fsp: Option<u8>,
2687}
2688
2689/// v7.17.0 Phase 2.5 — text collation classification surfaced
2690/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2691/// engine bridges between the two at CREATE TABLE time.
2692///
2693/// Recognised collation-name patterns (case-insensitive):
2694/// * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase` → CaseInsensitive
2695/// * Everything else (`C`, `POSIX`, `default`,
2696/// `pg_catalog.default`, `*_cs`, `*_bin`, unknown names) → Binary
2697#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2698pub enum Collation {
2699 Binary,
2700 CaseInsensitive,
2701}
2702
2703/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2704/// integer width for a column whose `ColumnTypeName` is too wide to carry
2705/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2706/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2707/// TABLE time. Only recorded under the MySQL dialect.
2708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2709pub enum MysqlIntWidth {
2710 Tiny,
2711 Small,
2712 Medium,
2713 Int,
2714 /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2715 Big,
2716}
2717
2718#[allow(clippy::derivable_impls)]
2719impl Default for Collation {
2720 fn default() -> Self {
2721 Self::Binary
2722 }
2723}
2724
2725impl Collation {
2726 /// Classify a `COLLATE <name>` ident into one of the supported
2727 /// variants. Empty / unknown names fall back to `Binary` —
2728 /// matches the pre-2.5 silent-accept behaviour for snapshots
2729 /// that load through but don't actually depend on the
2730 /// collation semantics.
2731 #[must_use]
2732 pub fn from_collation_name(name: &str) -> Self {
2733 let lc = name.trim().to_ascii_lowercase();
2734 // Strip any quotes / schema-qualifier the parser left on
2735 // (e.g. `pg_catalog.default`).
2736 let bare = lc
2737 .trim_matches(|c: char| c == '"' || c == '\'')
2738 .rsplit('.')
2739 .next()
2740 .unwrap_or("");
2741 if bare.is_empty() {
2742 return Self::Binary;
2743 }
2744 if bare == "case_insensitive" || bare == "nocase" {
2745 return Self::CaseInsensitive;
2746 }
2747 // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2748 // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2749 if bare.ends_with("_ci") {
2750 return Self::CaseInsensitive;
2751 }
2752 Self::Binary
2753 }
2754}
2755
2756/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2757/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2758/// parse into this shape — the column-level form has a single-entry
2759/// `columns` / `parent_columns`.
2760#[derive(Debug, Clone, PartialEq)]
2761pub struct ForeignKeyConstraint {
2762 /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2763 /// today but parses + stores it so a future ALTER TABLE DROP
2764 /// CONSTRAINT can target by name (v7.6.8).
2765 pub name: Option<String>,
2766 /// Local columns participating in the FK (≥ 1).
2767 pub columns: Vec<String>,
2768 /// Referenced parent table.
2769 pub parent_table: String,
2770 /// Referenced parent columns. Must have the same arity as
2771 /// `columns`; engine validates parent has a PK / UNIQUE index
2772 /// on exactly this column set (v7.6.1).
2773 pub parent_columns: Vec<String>,
2774 /// `ON DELETE` action. Defaults to `Restrict` if absent.
2775 pub on_delete: FkAction,
2776 /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2777 pub on_update: FkAction,
2778 /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2779 pub match_type: MatchType,
2780 /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2781 /// dropped on the floor, so a constraint declared DEFERRABLE was
2782 /// enforced immediately and a circular-FK migration could not load.
2783 pub deferrable: bool,
2784 /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2785 /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2786 pub initially_deferred: bool,
2787}
2788
2789/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2790/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2791/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2792#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2793pub enum MatchType {
2794 #[default]
2795 Simple,
2796 Full,
2797}
2798
2799/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2801pub enum FkAction {
2802 /// Reject the parent mutation if any child row references it.
2803 /// SQL spec default; SPG default when no clause is given.
2804 Restrict,
2805 /// Recursively propagate the parent's delete / update to the
2806 /// child rows. Same TX.
2807 Cascade,
2808 /// Set the child FK column(s) to NULL. Requires the FK columns
2809 /// to be NULL-able.
2810 SetNull,
2811 /// Set the child FK column(s) to their declared DEFAULT.
2812 /// Requires the child column(s) to have DEFAULT.
2813 SetDefault,
2814 /// SQL spec `NO ACTION` (deferred check). SPG treats this as
2815 /// `Restrict` because the single-writer model has no deferred
2816 /// constraint window; the keyword is accepted for compatibility.
2817 NoAction,
2818}
2819
2820/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
2821/// optional `USING <encoding>` clause; omitting it keeps the
2822/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
2823/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
2824/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
2825/// binary16 (2× compression, ~3 decimal digits of precision).
2826#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2827pub enum VecEncoding {
2828 /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
2829 /// uncompressed `vector` type wire / storage layout.
2830 #[default]
2831 F32,
2832 /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
2833 /// `spg_storage::quantize::Sq8Vector` for the math + recall
2834 /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
2835 /// dim ≥ 32).
2836 Sq8,
2837 /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
2838 /// per-element. DDL keyword `HALF` (pgvector convention).
2839 /// Bit-exact dequantise to f32 at the storage layer; no
2840 /// rerank pass needed for kNN search.
2841 F16,
2842}
2843
2844impl fmt::Display for VecEncoding {
2845 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2846 match self {
2847 Self::F32 => f.write_str("F32"),
2848 Self::Sq8 => f.write_str("SQ8"),
2849 // pgvector convention: DDL keyword is `HALF`, not `F16`.
2850 Self::F16 => f.write_str("HALF"),
2851 }
2852 }
2853}
2854
2855/// SQL-level type names. The mapping to the storage runtime's `DataType`
2856/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
2857#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2858pub enum ColumnTypeName {
2859 /// v7.39 (round 291) — PG's `name`, the identifier type its
2860 /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
2861 /// answered `type "name" does not exist` to.
2862 Name,
2863 /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
2864 /// 32-bit wrapping counter the row header carries; `xid8` is the
2865 /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
2866 /// SPG answered `type "xid" does not exist` to.
2867 Xid,
2868 Xid8,
2869 /// v7.39 (round 667) — `OID`. `XID` was already a column type here
2870 /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
2871 /// `type "oid" does not exist` while `t(x XID)` built fine.
2872 Oid,
2873 SmallInt,
2874 Int,
2875 BigInt,
2876 Float,
2877 /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
2878 /// IEEE. It used to map to [`Self::Float`] on the theory that a
2879 /// wider float is harmless, but the width is observable: a `real`
2880 /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
2881 /// answered false where PG answers true.
2882 Real,
2883 Text,
2884 /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
2885 Varchar(u32),
2886 /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
2887 Char(u32),
2888 Bool,
2889 /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
2890 /// `USING <encoding>` clause; omitting it surfaces as
2891 /// `encoding = VecEncoding::F32` (the pre-v6 default).
2892 Vector {
2893 dim: u32,
2894 encoding: VecEncoding,
2895 },
2896 /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
2897 /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
2898 /// v7.39 (round 271) — scale widened to u16 alongside the value's.
2899 /// v7.39 (round 272) — precision too: PG's runs to 1000.
2900 /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
2901 /// a negative one rounds to tens / hundreds. A VALUE's display scale
2902 /// stays unsigned.
2903 Numeric(u16, i16),
2904 /// `DATE` — calendar day, no time-of-day component.
2905 Date,
2906 /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
2907 /// precision.
2908 Timestamp,
2909 /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
2910 /// stores all timestamps as UTC microseconds-since-epoch and
2911 /// does not carry per-row offset (PG's internal representation
2912 /// is the same — TZ is a display convention). The distinction
2913 /// from `TIMESTAMP` exists for the PG-wire layer to advertise
2914 /// OID 1184 so sqlx-style clients decode into
2915 /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
2916 Timestamptz,
2917 /// v4.9 `JSON` — text-backed JSON document. No parse-time
2918 /// validation; the engine round-trips the literal verbatim.
2919 /// PG OID 114 on the wire.
2920 Json,
2921 /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
2922 /// PG OID 3802 on the wire so sqlx-style binary-typed clients
2923 /// decode without a custom type registration.
2924 Jsonb,
2925 /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
2926 /// Literal forms (decoded by the engine at coercion time):
2927 /// - PG hex form: `'\xDEADBEEF'`
2928 /// - Escape form: `'foo\\000bar'` (backslash octal triples)
2929 Bytes,
2930 /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
2931 /// OID 1009. Literal forms accepted by the parser:
2932 /// - `ARRAY['a', 'b', NULL]`
2933 /// - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
2934 /// form at coerce time)
2935 TextArray,
2936 /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
2937 /// 1007. Same literal forms as TEXT[] (substituting integer
2938 /// elements).
2939 IntArray,
2940 /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
2941 /// OID 1016.
2942 BigIntArray,
2943 /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
2944 /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
2945 /// external form). G-CRIT-3.
2946 TsVector,
2947 /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
2948 /// wire OID 3615.
2949 TsQuery,
2950 /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
2951 /// Literal input accepts canonical hyphenated, unhyphenated,
2952 /// uppercase, and `{...}`-braced forms; display normalises to
2953 /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
2954 /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
2955 /// gen_random_uuid()`.
2956 Uuid,
2957 /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
2958 /// microseconds since 00:00:00. PG wire OID 1083. Literal
2959 /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
2960 /// (6-digit microsecond precision). Display normalises to
2961 /// the canonical `HH:MM:SS[.ffffff]`.
2962 Time,
2963 /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
2964 /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
2965 /// PG OID; advertised as INT4 on the wire. Display always
2966 /// 4 digits zero-padded.
2967 Year,
2968 /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
2969 /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
2970 /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
2971 /// Offset range: ±14 hours.
2972 TimeTz,
2973 /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
2974 /// (locale-independent storage). Wire OID 790. Literal input
2975 /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
2976 /// major units), optional leading `-`. Display: en_US locale.
2977 Money,
2978 /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
2979 /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
2980 /// — the engine bridges to `DataType::Range(RangeKind)`.
2981 Range(RangeKindAst),
2982 /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
2983 /// `text => text` map with NULL value support.
2984 Hstore,
2985 /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
2986 IntArray2D,
2987 BigIntArray2D,
2988 TextArray2D,
2989 /// v7.39 (read01 round 75) — `bool[][]`.
2990 BoolArray2D,
2991 /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
2992 /// three-field {months, days, micros} struct (PG-byte-equal),
2993 /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
2994 /// β-P2 `INTERVAL` was runtime-only — literal in expression
2995 /// position but rejected at CREATE TABLE.
2996 Interval,
2997 /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
2998 /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
2999 /// PG external form quotes each non-NULL element because
3000 /// interval text contains spaces / colons
3001 /// (`{"1 day","24:00:00",NULL}`).
3002 IntervalArray,
3003 /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3004 /// mirrors a scalar `ColumnTypeName` that already existed.
3005 BoolArray,
3006 SmallIntArray,
3007 FloatArray,
3008 NumericArray,
3009 DateArray,
3010 TimestampArray,
3011 TimestamptzArray,
3012 UuidArray,
3013 JsonArray,
3014 JsonbArray,
3015 BytesArray,
3016 VarcharArray,
3017 CharArray,
3018 /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3019 /// as `Range(RangeKindAst)` — one column type variant covers
3020 /// all six builtin multiranges, kind pins the element type.
3021 /// Wire OIDs in pgwire.
3022 Multirange(RangeKindAst),
3023 /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3024 /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3025 /// Wire OIDs in pgwire.
3026 Point,
3027 Lseg,
3028 Path,
3029 PgBox,
3030 Polygon,
3031 Line,
3032 Circle,
3033 /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3034 Inet,
3035 Cidr,
3036 Macaddr,
3037 Macaddr8,
3038 /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3039 Bit(u32),
3040 /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3041 BitVarying(u32),
3042 Xml,
3043 Char1,
3044 MoneyArray,
3045}
3046
3047/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3048/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3049/// crate doesn't depend on storage. Bridged at engine boundary.
3050#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3051pub enum RangeKindAst {
3052 Int4,
3053 Int8,
3054 Num,
3055 Ts,
3056 TsTz,
3057 Date,
3058}
3059
3060impl fmt::Display for ColumnTypeName {
3061 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3062 match self {
3063 Self::SmallInt => f.write_str("SMALLINT"),
3064 Self::Int => f.write_str("INT"),
3065 Self::BigInt => f.write_str("BIGINT"),
3066 Self::Float => f.write_str("FLOAT"),
3067 Self::Real => f.write_str("REAL"),
3068 Self::Text => f.write_str("TEXT"),
3069 Self::Name => f.write_str("name"),
3070 Self::Xid => f.write_str("xid"),
3071 Self::Xid8 => f.write_str("xid8"),
3072 Self::Oid => f.write_str("oid"),
3073 Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3074 Self::Char(n) => write!(f, "CHAR({n})"),
3075 Self::Bool => f.write_str("BOOL"),
3076 Self::Vector { dim, encoding } => match encoding {
3077 VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3078 VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3079 VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3080 },
3081 Self::Json => f.write_str("JSON"),
3082 Self::Jsonb => f.write_str("JSONB"),
3083 Self::Bytes => f.write_str("BYTEA"),
3084 Self::TextArray => f.write_str("TEXT[]"),
3085 Self::IntArray => f.write_str("INT[]"),
3086 Self::BigIntArray => f.write_str("BIGINT[]"),
3087 Self::TsVector => f.write_str("TSVECTOR"),
3088 Self::TsQuery => f.write_str("TSQUERY"),
3089 Self::Uuid => f.write_str("UUID"),
3090 Self::Numeric(p, s) => {
3091 if *s == 0 {
3092 write!(f, "NUMERIC({p})")
3093 } else {
3094 write!(f, "NUMERIC({p}, {s})")
3095 }
3096 }
3097 Self::Date => f.write_str("DATE"),
3098 Self::Timestamp => f.write_str("TIMESTAMP"),
3099 Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3100 Self::Time => f.write_str("TIME"),
3101 Self::Year => f.write_str("YEAR"),
3102 Self::TimeTz => f.write_str("TIMETZ"),
3103 Self::Money => f.write_str("MONEY"),
3104 Self::Range(k) => f.write_str(match k {
3105 RangeKindAst::Int4 => "INT4RANGE",
3106 RangeKindAst::Int8 => "INT8RANGE",
3107 RangeKindAst::Num => "NUMRANGE",
3108 RangeKindAst::Ts => "TSRANGE",
3109 RangeKindAst::TsTz => "TSTZRANGE",
3110 RangeKindAst::Date => "DATERANGE",
3111 }),
3112 Self::Hstore => f.write_str("HSTORE"),
3113 Self::Interval => f.write_str("INTERVAL"),
3114 Self::IntervalArray => f.write_str("INTERVAL[]"),
3115 Self::BoolArray => f.write_str("BOOL[]"),
3116 Self::SmallIntArray => f.write_str("SMALLINT[]"),
3117 Self::FloatArray => f.write_str("FLOAT[]"),
3118 Self::NumericArray => f.write_str("NUMERIC[]"),
3119 Self::DateArray => f.write_str("DATE[]"),
3120 Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3121 Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3122 Self::UuidArray => f.write_str("UUID[]"),
3123 Self::JsonArray => f.write_str("JSON[]"),
3124 Self::JsonbArray => f.write_str("JSONB[]"),
3125 Self::BytesArray => f.write_str("BYTEA[]"),
3126 Self::VarcharArray => f.write_str("VARCHAR[]"),
3127 Self::CharArray => f.write_str("CHAR[]"),
3128 Self::Multirange(k) => f.write_str(match k {
3129 RangeKindAst::Int4 => "INT4MULTIRANGE",
3130 RangeKindAst::Int8 => "INT8MULTIRANGE",
3131 RangeKindAst::Num => "NUMMULTIRANGE",
3132 RangeKindAst::Ts => "TSMULTIRANGE",
3133 RangeKindAst::TsTz => "TSTZMULTIRANGE",
3134 RangeKindAst::Date => "DATEMULTIRANGE",
3135 }),
3136 Self::Point => f.write_str("POINT"),
3137 Self::Lseg => f.write_str("LSEG"),
3138 Self::Path => f.write_str("PATH"),
3139 Self::PgBox => f.write_str("BOX"),
3140 Self::Polygon => f.write_str("POLYGON"),
3141 Self::Line => f.write_str("LINE"),
3142 Self::Circle => f.write_str("CIRCLE"),
3143 Self::Inet => f.write_str("INET"),
3144 Self::Cidr => f.write_str("CIDR"),
3145 Self::Macaddr => f.write_str("MACADDR"),
3146 Self::Macaddr8 => f.write_str("MACADDR8"),
3147 Self::Bit(0) => f.write_str("BIT"),
3148 Self::Bit(n) => write!(f, "BIT({n})"),
3149 Self::BitVarying(0) => f.write_str("VARBIT"),
3150 Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3151 Self::Xml => f.write_str("XML"),
3152 Self::Char1 => f.write_str("\"char\""),
3153 Self::MoneyArray => f.write_str("MONEY[]"),
3154 Self::IntArray2D => f.write_str("INT[][]"),
3155 Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3156 Self::TextArray2D => f.write_str("TEXT[][]"),
3157 Self::BoolArray2D => f.write_str("BOOL[][]"),
3158 }
3159 }
3160}
3161
3162/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3163/// engine evaluates `expr` per matched row in the table's row order
3164/// and rewrites cells in place. Indexed columns are dropped + re-
3165/// inserted into the affected B-tree on each row change.
3166/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3167/// tail on a DML statement. Boxed off the statement struct so the PG-only
3168/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3169/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3170/// the identical meaning, so both share this one payload rather than each
3171/// growing its own.
3172#[derive(Debug, Clone, PartialEq)]
3173pub struct DmlOrderLimit {
3174 pub order_by: Vec<OrderBy>,
3175 pub limit: Option<u32>,
3176}
3177
3178/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3179/// FROM, kept so the engine can finish the job.
3180///
3181/// The parser rewrites the statement onto correlated subqueries, and it
3182/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3183/// name belongs to the target or to a source needs their column lists,
3184/// which parse time does not have. Carrying the clause lets the engine
3185/// — which has the catalog — resolve the rest.
3186#[derive(Debug, Clone, PartialEq)]
3187pub struct UpdateFromSources {
3188 pub from: FromClause,
3189 pub sub_where: Option<Expr>,
3190}
3191
3192#[derive(Debug, Clone, PartialEq)]
3193pub struct UpdateStatement {
3194 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3195 /// level UPDATE. Empty for a plain UPDATE.
3196 pub ctes: Vec<Cte>,
3197 pub table: String,
3198 /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3199 /// to `t`'s own rows and not to anything that descends from it.
3200 ///
3201 /// Round 644 taught the FROM clause the keyword and left DML behind
3202 /// because it needed a field here, and this struct carries a warning
3203 /// that round 413 measured widening it in place overflowing the
3204 /// parser's nesting stack. That warning was about `from_sources`, a
3205 /// struct wide enough to need boxing; a `bool` lands in the padding
3206 /// already present — same as `CreateTableStatement::temporary`.
3207 ///
3208 /// It also earns its keep beyond the spelling: the inheritance
3209 /// fan-out needs a way to say "the parent's own rows" as a
3210 /// statement, or running one on the parent recurses forever.
3211 pub only: bool,
3212 /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3213 /// statement's expressions refer to the target row by. PG allows the
3214 /// bare spelling here (unlike INSERT, which requires AS).
3215 pub alias: Option<String>,
3216 pub assignments: Vec<(String, Expr)>,
3217 /// v7.39 (round 533) — boxed: round 413 measured that widening this
3218 /// struct in place overflows the parser's nesting stack.
3219 pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3220 pub where_: Option<Expr>,
3221 /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3222 /// mutate the first `limit` rows in the given order. PG has no such
3223 /// clause; the parser accepts it only under the MySQL dialect. Boxed
3224 /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3225 /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3226 /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3227 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3228 /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3229 /// clause (legacy CommandComplete path). Some = engine
3230 /// evaluates the projection over each mutated row and
3231 /// streams the result as a Rows QueryResult.
3232 pub returning: Option<Vec<SelectItem>>,
3233}
3234
3235/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3236/// from the active catalog and prunes them from every index.
3237#[derive(Debug, Clone, PartialEq)]
3238pub struct DeleteStatement {
3239 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3240 /// level DELETE. Empty for a plain DELETE.
3241 pub ctes: Vec<Cte>,
3242 pub table: String,
3243 /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3244 /// to `t`'s own rows and not to anything that descends from it.
3245 ///
3246 /// Round 644 taught the FROM clause the keyword and left DML behind
3247 /// because it needed a field here, and this struct carries a warning
3248 /// that round 413 measured widening it in place overflowing the
3249 /// parser's nesting stack. That warning was about `from_sources`, a
3250 /// struct wide enough to need boxing; a `bool` lands in the padding
3251 /// already present — same as `CreateTableStatement::temporary`.
3252 ///
3253 /// It also earns its keep beyond the spelling: the inheritance
3254 /// fan-out needs a way to say "the parent's own rows" as a
3255 /// statement, or running one on the parent recurses forever.
3256 pub only: bool,
3257 /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3258 /// the WHERE / RETURNING expressions refer to the target row by.
3259 pub alias: Option<String>,
3260 pub where_: Option<Expr>,
3261 /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3262 /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3263 /// form (round 413), so it shares that payload — and it is boxed for
3264 /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3265 /// statement tipped the parser's 512 KiB nesting stack.
3266 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3267 /// v7.9.4 — `RETURNING <projection>`.
3268 pub returning: Option<Vec<SelectItem>>,
3269}
3270
3271/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3272/// One WHEN clause fires per source row depending on whether the
3273/// `on` condition matched any target row(s); the executor walks
3274/// `clauses` in declaration order and fires the first whose
3275/// `matched` kind and optional `condition` are both satisfied.
3276#[derive(Debug, Clone, PartialEq)]
3277pub struct MergeStatement {
3278 /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3279 /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3280 /// in PG). Each CTE materialises before the merge runs and its alias
3281 /// resolves as a source relation.
3282 pub ctes: Vec<Cte>,
3283 pub target: String,
3284 pub target_alias: Option<String>,
3285 pub source: String,
3286 pub source_alias: Option<String>,
3287 /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3288 /// the engine materialises this SELECT for the source rows and `source`
3289 /// is empty; the alias (required by PG for a subquery source) is in
3290 /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3291 pub source_select: Option<Box<SelectStatement>>,
3292 /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3293 /// positional column-alias list after the source alias. Empty when
3294 /// the statement carries none; the engine renames the materialised
3295 /// source columns positionally (PG's rule).
3296 pub source_column_aliases: Vec<String>,
3297 pub on: Expr,
3298 pub clauses: Vec<MergeWhenClause>,
3299 /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3300 /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3301 /// target/source aliases. `None` = no RETURNING (the common form).
3302 pub returning: Option<Vec<SelectItem>>,
3303}
3304
3305#[derive(Debug, Clone, PartialEq)]
3306pub struct MergeWhenClause {
3307 pub matched: MergeMatched,
3308 /// Optional `AND <expr>` filter — when present, the clause
3309 /// only fires for the source rows whose match-pair satisfies
3310 /// the predicate.
3311 pub condition: Option<Expr>,
3312 pub action: MergeAction,
3313}
3314
3315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3316pub enum MergeMatched {
3317 Matched,
3318 /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3319 /// target row (the classic insert branch).
3320 NotMatched,
3321 /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3322 /// row no source row matches. Actions are UPDATE / DELETE / DO
3323 /// NOTHING only (INSERT is a syntax error, as in PG).
3324 NotMatchedBySource,
3325}
3326
3327#[derive(Debug, Clone, PartialEq)]
3328pub enum MergeAction {
3329 /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3330 /// explicit column list (the bare `INSERT VALUES (vals)`
3331 /// shape lands later).
3332 Insert {
3333 columns: Vec<String>,
3334 values: Vec<Expr>,
3335 },
3336 /// `UPDATE SET col = expr [, …]` — applied to every matched
3337 /// target row for the firing source row.
3338 Update { assignments: Vec<(String, Expr)> },
3339 /// `DELETE` — drop every matched target row.
3340 Delete,
3341 /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3342 /// the clause and SPG mirrors so a customer-side MERGE that
3343 /// uses it for branch-control doesn't error).
3344 DoNothing,
3345}
3346
3347#[derive(Debug, Clone, PartialEq)]
3348pub struct InsertStatement {
3349 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3350 /// level INSERT (writable CTE outer body). Empty for a plain
3351 /// INSERT. PG semantics: each CTE materialises before the
3352 /// outer INSERT runs, sharing the same transaction.
3353 pub ctes: Vec<Cte>,
3354 pub table: String,
3355 /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3356 /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3357 /// row by. PG requires the AS keyword in this position.
3358 pub alias: Option<String>,
3359 /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3360 /// `None`, every tuple is positional and must match the table arity.
3361 /// When `Some`, the engine maps each tuple slot to the named column and
3362 /// fills the rest with NULL (must be nullable).
3363 pub columns: Option<Vec<String>>,
3364 /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3365 /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3366 /// `select_source` is `Some` (the engine builds rows from the
3367 /// inner SELECT result set instead).
3368 pub rows: Vec<Vec<Expr>>,
3369 /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3370 /// round-5 G4). When present, `rows` is empty and the engine
3371 /// materialises the SELECT result, coerces each output tuple to
3372 /// the target column types, and inserts as a single batch.
3373 pub select_source: Option<Box<SelectStatement>>,
3374 /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3375 /// upsert clause. None = legacy INSERT (conflict raises a
3376 /// DuplicateKey error). mailrs migration blocker #2.
3377 pub on_conflict: Option<OnConflictClause>,
3378 /// v7.9.4 — `RETURNING <projection>`.
3379 pub returning: Option<Vec<SelectItem>>,
3380 /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3381 /// between the column list and VALUES. Governs how explicitly-supplied
3382 /// values interact with `GENERATED … AS IDENTITY` columns:
3383 /// * `None` — default. A `GENERATED ALWAYS` identity column rejects
3384 /// an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3385 /// * `System` — override the ALWAYS restriction: the explicit value
3386 /// is used verbatim, as for a `BY DEFAULT` column.
3387 /// * `User` — ignore any explicit value on a `BY DEFAULT` identity
3388 /// column and generate from the sequence instead (no effect on
3389 /// non-identity columns).
3390 pub overriding: Overriding,
3391 /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3392 /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3393 /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3394 /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3395 /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3396 /// into a NOT NULL column becomes the type's default), and the engine
3397 /// cannot recover that intent from the conflict clause alone. A plain
3398 /// `bool` lands in this struct's existing padding, so the AST does not
3399 /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3400 pub mysql_ignore: bool,
3401}
3402
3403/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3404#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3405pub enum Overriding {
3406 /// No `OVERRIDING` clause.
3407 #[default]
3408 None,
3409 /// `OVERRIDING SYSTEM VALUE`.
3410 System,
3411 /// `OVERRIDING USER VALUE`.
3412 User,
3413}
3414
3415/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3416#[derive(Debug, Clone, PartialEq)]
3417pub struct OnConflictClause {
3418 /// Local columns that identify the conflict (must match a
3419 /// UNIQUE / PRIMARY KEY index on the target table). Empty
3420 /// list means the user wrote `ON CONFLICT DO …` without a
3421 /// target — the engine arbitrates on every unique constraint
3422 /// (round 240).
3423 pub target_columns: Vec<String>,
3424 /// v7.39 (round 240) — the index predicate after the target list
3425 /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3426 /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3427 /// which satisfy any predicate, so it is parsed and carried but not
3428 /// consulted (recorded residual: partial-unique-index arbiters).
3429 pub index_where: Option<Expr>,
3430 /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3431 /// <name>`: the pg_dump conflict-target form. The engine
3432 /// resolves the name to the constraint's columns.
3433 pub constraint_name: Option<String>,
3434 /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3435 /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3436 /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3437 /// `ON CONFLICT DO UPDATE` is refused (42601).
3438 pub mysql_lowered: bool,
3439 /// The action on conflict.
3440 pub action: OnConflictAction,
3441}
3442
3443/// v7.9.7 — action on conflict.
3444#[derive(Debug, Clone, PartialEq)]
3445pub enum OnConflictAction {
3446 /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3447 /// silently skips conflicting ones.
3448 Nothing,
3449 /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3450 /// may reference `EXCLUDED.col` to read the incoming row's
3451 /// value (engine wires `EXCLUDED` as a virtual table).
3452 Update {
3453 assignments: Vec<(String, Expr)>,
3454 where_: Option<Expr>,
3455 },
3456}
3457
3458/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3459///
3460/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3461/// policies are spelled again here and mapped at the engine boundary.
3462#[derive(Debug, Clone, PartialEq, Eq)]
3463pub struct LockingClause {
3464 pub strength: LockStrength,
3465 /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3466 pub of_tables: Vec<String>,
3467 pub policy: LockWait,
3468}
3469
3470/// PG's four tuple-lock strengths, weakest first.
3471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3472pub enum LockStrength {
3473 KeyShare,
3474 Share,
3475 NoKeyUpdate,
3476 Update,
3477}
3478
3479/// What to do when the row is already locked.
3480#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3481pub enum LockWait {
3482 /// Block until it is free — PG's default.
3483 #[default]
3484 Wait,
3485 /// `NOWAIT` — fail the statement with 55P03.
3486 NoWait,
3487 /// `SKIP LOCKED` — leave the row out of the result.
3488 SkipLocked,
3489}
3490
3491#[derive(Debug, Clone, PartialEq, Default)]
3492pub struct SelectStatement {
3493 /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3494 /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3495 /// whole syntax and locked nothing: two workers running the classic
3496 /// `SKIP LOCKED` queue take both took the same row.
3497 /// v7.39 (round 305) — boxed. A locking clause appears on a
3498 /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3499 /// cost every `SelectStatement` 32 bytes, and this struct sits in
3500 /// recursive evaluation frames where the engine already runs close to
3501 /// its stack budget (a 512 KB depth guard is the canary).
3502 pub locking: Option<alloc::boxed::Box<LockingClause>>,
3503 /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3504 /// expressions, materialised once at query start before the
3505 /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3506 /// only — no `WITH RECURSIVE` for v4.x.
3507 pub ctes: Vec<Cte>,
3508 pub distinct: bool,
3509 /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3510 /// keep the first row (per ORDER BY) of each group the
3511 /// expressions define. Empty = no DISTINCT ON.
3512 pub distinct_on: Vec<Expr>,
3513 pub items: Vec<SelectItem>,
3514 pub from: Option<FromClause>,
3515 pub where_: Option<Expr>,
3516 pub group_by: Option<Vec<Expr>>,
3517 /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3518 /// expands `group_by` to every non-aggregate SELECT-list item
3519 /// before the executor runs. Mutually exclusive with an
3520 /// explicit `group_by` list (the parser sets exactly one).
3521 pub group_by_all: bool,
3522 /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3523 /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3524 /// aggregate executor resolves them through the same synthetic
3525 /// schema used for the SELECT items.
3526 pub having: Option<Expr>,
3527 /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3528 /// itself a `SelectStatement` with `order_by = None` and `limit =
3529 /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3530 /// top of the chain).
3531 pub unions: Vec<(UnionKind, SelectStatement)>,
3532 /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3533 /// Keys are matched left-to-right: first key decides, ties break
3534 /// to the second, etc.
3535 pub order_by: Vec<OrderBy>,
3536 /// `LIMIT <n>` — bound on row output. `n` is an integer
3537 /// literal **or** (v7.9.24) a placeholder `$N` resolved
3538 /// against the prepared-statement Bind values. mailrs
3539 /// migration follow-up H2.
3540 pub limit: Option<LimitExpr>,
3541 /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3542 /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3543 pub offset: Option<LimitExpr>,
3544 /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3545 /// (SQL:2008). When true and an ORDER BY is present, the
3546 /// executor extends past the LIMIT-truncated tail to include
3547 /// every row whose ORDER BY key equals the last-kept row's
3548 /// key. Requires an ORDER BY; the executor errors otherwise
3549 /// (matching PG's `WITH TIES` rule). The parser was already
3550 /// accepting `WITH TIES` since Phase 5.1; this field captures
3551 /// the choice so the executor can act on it.
3552 pub limit_with_ties: bool,
3553 /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3554 /// that NOTHING referenced. PG analyses every definition whether
3555 /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3556 /// and silently succeeded here — the referenced ones get their columns
3557 /// resolved through the WindowFunction nodes they were inlined into,
3558 /// and the unreferenced ones used to be dropped at parse, unexamined.
3559 /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3560 ///
3561 /// Not part of `Display`: an unreferenced definition has no effect on
3562 /// the result, so a deparsed body (a stored view) omits it.
3563 pub window_check_exprs: Vec<Expr>,
3564}
3565
3566impl Expr {
3567 /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3568 /// directly inside this expression to `f`. `f` receives each nested
3569 /// statement once; descending further (into that statement's own
3570 /// clauses) is the caller's job, which keeps this walk finite and
3571 /// lets the caller order the recursion.
3572 ///
3573 /// The match is deliberately **wildcard-free**: a new `Expr` variant
3574 /// does not compile until it says whether it can carry a subquery.
3575 /// The row-count resolution pass is built on this, and a shape it
3576 /// silently failed to visit would leave a `LimitExpr::Expr` behind —
3577 /// which every row-count reader would take as "no limit", i.e. the
3578 /// whole table. Compile-time exhaustiveness is what rules that out.
3579 /// Iterative on purpose. Expression trees here get deep (long
3580 /// boolean chains, big IN lists), and this walk is on the path of
3581 /// every statement; recursing would add a frame per node to a stack
3582 /// budget the engine already runs close to — a depth guard that runs
3583 /// on a deliberately small stack caught exactly that. Depth costs
3584 /// heap here instead.
3585 pub fn for_each_subquery_mut<E>(
3586 &mut self,
3587 f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
3588 ) -> Result<(), E> {
3589 let mut stack: Vec<&mut Self> = alloc::vec![self];
3590 while let Some(e) = stack.pop() {
3591 match e {
3592 Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
3593 Self::NamedArg { expr, .. }
3594 | Self::Variadic(expr)
3595 | Self::Unary { expr, .. }
3596 | Self::Cast { expr, .. }
3597 | Self::FieldAccess { base: expr, .. }
3598 | Self::IsNull { expr, .. }
3599 | Self::BoolTest { expr, .. }
3600 | Self::Extract { source: expr, .. } => stack.push(expr),
3601 Self::Binary { lhs, rhs, .. } => {
3602 stack.push(lhs);
3603 stack.push(rhs);
3604 }
3605 Self::Like { expr, pattern, .. } => {
3606 stack.push(expr);
3607 stack.push(pattern);
3608 }
3609 Self::ArraySubscript { target, index } => {
3610 stack.push(target);
3611 stack.push(index);
3612 }
3613 Self::ArraySlice { target, lo, hi } => {
3614 stack.push(target);
3615 stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
3616 }
3617 Self::AnyAll { expr, array, .. } => {
3618 stack.push(expr);
3619 stack.push(array);
3620 }
3621 Self::FunctionCall { args, .. } | Self::Array(args) => {
3622 stack.extend(args.iter_mut());
3623 }
3624 Self::AggregateOrdered {
3625 call,
3626 order_by,
3627 filter,
3628 ..
3629 } => {
3630 stack.push(call);
3631 stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
3632 stack.extend(filter.iter_mut().map(|b| &mut **b));
3633 }
3634 Self::WindowFunction {
3635 args,
3636 partition_by,
3637 order_by,
3638 filter,
3639 ..
3640 } => {
3641 // `frame` bounds hold folded numbers / interval
3642 // parts, never expressions — nothing to visit there.
3643 stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
3644 stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
3645 stack.extend(filter.iter_mut().map(|b| &mut **b));
3646 }
3647 Self::InList { expr, list, .. } => {
3648 stack.push(expr);
3649 stack.extend(list.iter_mut());
3650 }
3651 Self::Case {
3652 operand,
3653 branches,
3654 else_branch,
3655 } => {
3656 stack.extend(
3657 operand
3658 .iter_mut()
3659 .chain(else_branch.iter_mut())
3660 .map(|b| &mut **b),
3661 );
3662 for (when, then) in branches.iter_mut() {
3663 stack.push(when);
3664 stack.push(then);
3665 }
3666 }
3667 Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
3668 Self::InSubquery { expr, subquery, .. } => {
3669 stack.push(expr);
3670 f(subquery)?;
3671 }
3672 Self::RowInSubquery { row, subquery, .. }
3673 | Self::RowCmpSubquery { row, subquery, .. } => {
3674 stack.extend(row.iter_mut());
3675 f(subquery)?;
3676 }
3677 }
3678 }
3679 Ok(())
3680 }
3681}
3682
3683/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
3684/// time or a placeholder `$N` resolved during extended-query
3685/// Bind. mailrs migration follow-up H2.
3686///
3687/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
3688/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
3689/// made the compiler point at every site that used to duplicate a
3690/// row-count out of the AST, which is exactly the set that must not
3691/// bypass the resolution pre-pass.
3692#[derive(Debug, Clone, PartialEq)]
3693pub enum LimitExpr {
3694 /// `LIMIT 10` — value known at parse time.
3695 Literal(u32),
3696 /// `LIMIT $N` — the 1-based parameter index, resolved against
3697 /// the bind values when the prepared statement executes.
3698 Placeholder(u16),
3699 /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
3700 /// greatest(2,3)`: a row-count expression that isn't constant, so
3701 /// it can't be folded at parse time. Evaluated once, before
3702 /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
3703 /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
3704 /// "no limit"). **No execution path may see this variant** —
3705 /// `as_literal` would report `None`, which every row-count reader
3706 /// takes to mean "unlimited", i.e. the whole table.
3707 Expr(alloc::boxed::Box<Expr>),
3708}
3709
3710impl fmt::Display for LimitExpr {
3711 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3712 match self {
3713 Self::Literal(n) => write!(f, "{n}"),
3714 Self::Placeholder(n) => write!(f, "${n}"),
3715 // Parenthesised so the round-trip text re-parses as one
3716 // row-count expression (`LIMIT (SELECT 4)`), which is also
3717 // the only spelling `FETCH FIRST` accepts.
3718 Self::Expr(e) => write!(f, "({e})"),
3719 }
3720 }
3721}
3722
3723impl LimitExpr {
3724 /// Convenience for the simple-query path where no placeholders
3725 /// can possibly exist. Returns the literal value or `None` if
3726 /// this is a placeholder (caller must surface as Unsupported).
3727 ///
3728 /// v7.39 (round 305) — `None` is read by every row-count consumer as
3729 /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
3730 /// therefore silently return the whole table, so the engine's
3731 /// `resolve_limit_exprs` pre-pass rewrites the variant away before
3732 /// dispatch. The assertion makes a missed nesting site fail loudly
3733 /// in every test build rather than quietly widening a result set.
3734 #[must_use]
3735 pub fn as_literal(&self) -> Option<u32> {
3736 match self {
3737 Self::Literal(n) => Some(*n),
3738 Self::Placeholder(_) => None,
3739 Self::Expr(_) => {
3740 debug_assert!(
3741 false,
3742 "LimitExpr::Expr reached execution — resolve_limit_exprs \
3743 missed a nesting site; treating it as `no limit` would \
3744 return every row"
3745 );
3746 None
3747 }
3748 }
3749 }
3750}
3751
3752/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
3753/// the engine's `substitute_placeholders` pass these are
3754/// always Literal; in the simple-query path a Placeholder
3755/// shape returns None (executor surfaces as
3756/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
3757impl SelectStatement {
3758 #[must_use]
3759 pub fn limit_literal(&self) -> Option<u32> {
3760 self.limit.as_ref().and_then(LimitExpr::as_literal)
3761 }
3762 #[must_use]
3763 pub fn offset_literal(&self) -> Option<u32> {
3764 self.offset.as_ref().and_then(LimitExpr::as_literal)
3765 }
3766}
3767
3768#[derive(Debug, Clone, PartialEq)]
3769pub struct Cte {
3770 pub name: String,
3771 /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
3772 /// classical case) or a data-modifying statement
3773 /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
3774 /// CTE semantics. The modifying body's RETURNING projection
3775 /// becomes the materialised CTE table the outer query can
3776 /// reference; the modifying statement runs once before the
3777 /// outer query, within the same transaction.
3778 pub body: CteBody,
3779 /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
3780 /// RECURSIVE keyword. Applies to every CTE in the clause per
3781 /// PG semantics. A non-recursive body in a RECURSIVE WITH is
3782 /// allowed; the engine just runs it once.
3783 pub recursive: bool,
3784 /// v4.22: optional `WITH name(a, b, c)` column-name list. When
3785 /// non-empty, these override the body's output column names
3786 /// position-by-position; the engine errors out if the count
3787 /// doesn't match the body's projection width.
3788 pub column_overrides: Vec<String>,
3789 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
3790 /// SET seqcol` on a recursive CTE. Desugared at parse time into an
3791 /// extra ordering column on the body (see `rewrite_search_and_cycle`).
3792 pub search: Option<SearchClause>,
3793 /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
3794 /// USING pathcol` cycle detection, desugared at parse time.
3795 pub cycle: Option<CycleClause>,
3796}
3797
3798/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
3799#[derive(Debug, Clone, PartialEq)]
3800pub struct SearchClause {
3801 /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
3802 pub depth_first: bool,
3803 /// The CTE output columns the search orders by.
3804 pub by_columns: Vec<String>,
3805 /// The new column holding the ordering key (a row-array for depth,
3806 /// a `(depth, keys…)` row for breadth).
3807 pub set_column: String,
3808}
3809
3810/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
3811#[derive(Debug, Clone, PartialEq)]
3812pub struct CycleClause {
3813 /// Columns whose repetition along a path marks a cycle.
3814 pub columns: Vec<String>,
3815 /// The new boolean-ish column set to `mark_value` on a cycle.
3816 pub mark_column: String,
3817 /// Value written to `mark_column` when a cycle is detected (default
3818 /// `true`); `default_value` otherwise. PG allows any type; SPG carries
3819 /// them as literals.
3820 pub mark_value: Option<Literal>,
3821 pub default_value: Option<Literal>,
3822 /// The new column accumulating the visited-row path array.
3823 pub path_column: String,
3824}
3825
3826/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
3827/// (Insert / Update / Delete with optional RETURNING). The
3828/// data-modifying variants must carry a RETURNING projection for the
3829/// outer query to reference the CTE alias by; an empty RETURNING is
3830/// only valid if no outer reference materialises (rare — typically
3831/// caught at planning).
3832#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
3833#[derive(Debug, Clone, PartialEq)]
3834pub enum CteBody {
3835 Select(SelectStatement),
3836 Insert(Box<InsertStatement>),
3837 Update(Box<UpdateStatement>),
3838 Delete(Box<DeleteStatement>),
3839 /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
3840 /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
3841 Merge(Box<MergeStatement>),
3842}
3843
3844impl CteBody {
3845 /// Convenience accessor used by classical (read-only) CTE
3846 /// callsites that still expect a SELECT body. Returns None for
3847 /// data-modifying CTEs; callers must explicitly route those
3848 /// through `exec_with_ctes`'s modifying branch.
3849 #[must_use]
3850 pub fn as_select(&self) -> Option<&SelectStatement> {
3851 match self {
3852 Self::Select(s) => Some(s),
3853 _ => None,
3854 }
3855 }
3856
3857 #[must_use]
3858 pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
3859 match self {
3860 Self::Select(s) => Some(s),
3861 _ => None,
3862 }
3863 }
3864
3865 #[must_use]
3866 pub fn is_modifying(&self) -> bool {
3867 !matches!(self, Self::Select(_))
3868 }
3869}
3870
3871#[derive(Debug, Clone, PartialEq)]
3872pub struct OrderBy {
3873 pub expr: Expr,
3874 /// `false` = ASC (default), `true` = DESC.
3875 pub desc: bool,
3876 /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
3877 /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
3878 /// NULLS FIRST for DESC); the engine resolves the effective
3879 /// value via `nulls_first.unwrap_or(desc)`.
3880 pub nulls_first: Option<bool>,
3881 /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
3882 /// It lives here rather than in the expression for the same reason
3883 /// `desc` does: at an ORDER BY key a collation is ordering
3884 /// information, and nothing downstream of the sort needs it. A new
3885 /// `Expr` variant would instead put a new arm on `eval_expr`, which
3886 /// this repo has measured to overflow the debug stack.
3887 ///
3888 /// `None` means none was written, and the key falls back to whatever
3889 /// its COLUMN declares — which is every key that existed before this.
3890 pub collation: Option<String>,
3891}
3892
3893#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3894pub enum UnionKind {
3895 /// `UNION` — dedupes the combined set.
3896 Distinct,
3897 /// `UNION ALL` — concatenates without dedup.
3898 All,
3899 /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
3900 /// present on both sides.
3901 Intersect,
3902 /// `INTERSECT ALL` — multiset intersection (min per-row count).
3903 IntersectAll,
3904 /// `EXCEPT` — distinct left rows absent from the right.
3905 Except,
3906 /// `EXCEPT ALL` — multiset subtraction.
3907 ExceptAll,
3908}
3909
3910#[derive(Debug, Clone, PartialEq)]
3911pub enum SelectItem {
3912 Wildcard,
3913 /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
3914 /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
3915 /// `NEW` pseudo-relation).
3916 QualifiedWildcard(String),
3917 Expr {
3918 expr: Expr,
3919 alias: Option<String>,
3920 },
3921}
3922
3923#[derive(Debug, Clone, PartialEq)]
3924pub struct TableRef {
3925 pub name: String,
3926 pub alias: Option<String>,
3927 /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
3928 /// children.
3929 ///
3930 /// The keyword used to be absorbed at parse time, on the reasoning
3931 /// that SPG's inheritance children are separate relations a plain
3932 /// scan does not descend into — so ONLY already described what the
3933 /// scan did. That stopped being true when a partition parent
3934 /// started unioning its children: measured, `SELECT count(*) FROM
3935 /// ONLY <partitioned parent>` answered 2 where PG answers 0.
3936 pub only: bool,
3937 /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
3938 /// When `Some(id)`, the scan restricts to rows that live in
3939 /// segment `<id>` only — useful for forensic inspection of a
3940 /// specific freezer-emitted segment without exposing the hot
3941 /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
3942 /// is STABILITY carve-out for v6.10 — needs the freezer to
3943 /// stamp each segment with a wall-clock at creation time.
3944 pub as_of_segment: Option<u32>,
3945 /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
3946 /// source. When `Some`, `name` is the alias (defaulting to
3947 /// `"unnest"` when no `AS` is given) and the engine builds a
3948 /// synthetic single-column table by evaluating the expression
3949 /// once at SELECT entry. Each TEXT[] element becomes one row;
3950 /// NULL elements become NULL cells. v7.11 supported
3951 /// uncorrelated UNNEST only as the FROM primary; v7.13.2
3952 /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
3953 /// position (cross-join with regular tables).
3954 pub unnest_expr: Option<Box<Expr>>,
3955 /// v7.13.2 — mailrs round-6 S5. PG-standard
3956 /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
3957 /// when non-empty, the first entry overrides the projected
3958 /// column name for the unnested column. Empty = fall back to
3959 /// the table alias (pre-v7.13.2 behaviour).
3960 pub unnest_column_aliases: Vec<String>,
3961 /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
3962 /// row-stream gains a trailing BIGINT column counting rows
3963 /// from 1 in element order. PG names it `ordinality`; a second
3964 /// entry in the column-alias list renames it.
3965 pub with_ordinality: bool,
3966 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
3967 /// [, step])` set-returning source. When `Some`, the engine
3968 /// materialises a single-column virtual table by stepping
3969 /// `start` to `stop` inclusive. Args are the literal arg list
3970 /// (2 for default-step, 3 for explicit-step). Supports:
3971 /// * SmallInt / Int / BigInt with integer step (default = 1)
3972 /// * Timestamp with INTERVAL step (PG date-range pattern)
3973 /// Mutually exclusive with `unnest_expr` — both populate the
3974 /// same downstream dispatch slot. `name` defaults to
3975 /// `"generate_series"` when no alias is provided.
3976 pub generate_series_args: Option<Vec<Expr>>,
3977 /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
3978 /// table. When `Some`, the TableRef is a parenthesised SELECT
3979 /// that may reference columns from the preceding FROM items
3980 /// (correlated derived table). The executor materialises the
3981 /// subquery per left-row, substituting outer-column references
3982 /// against the current join row's values before running the
3983 /// inner SELECT, then cross-joins the result back.
3984 /// Mutually exclusive with `name` / `unnest_expr` /
3985 /// `generate_series_args`.
3986 pub lateral_subquery: Option<Box<SelectStatement>>,
3987 /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
3988 /// function as a FROM item. PG semantics: for each key/value
3989 /// pair in the JSONB object argument, emit one (key TEXT,
3990 /// value TEXT) row. When prefixed by `LATERAL` and joined via
3991 /// `CROSS JOIN LATERAL`, the argument may reference columns
3992 /// from a preceding FROM item, in which case the executor
3993 /// evaluates `<expr>` per outer row.
3994 /// Mutually exclusive with `unnest_expr` / `generate_series_args`
3995 /// / `lateral_subquery`. The optional `LATERAL` keyword does not
3996 /// require a separate flag — the executor evaluates per-row
3997 /// whenever the join sits in a JoinKind context.
3998 ///
3999 /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4000 /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4001 /// `json_each` / `json_each_text`) so the executor picks the
4002 /// value-column rendering (JSON text vs unwrapped text).
4003 pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4004 /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4005 /// function channel: `(lowercase fn name, args)`. Carries
4006 /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4007 /// dispatches by name.
4008 pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4009 /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4010 /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4011 /// reference to it yields the value, not a one-field composite
4012 /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4013 /// desugared shape is indistinguishable from a hand-written
4014 /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4015 /// only the parser knows which one it built, so it says so here.
4016 pub scalar_fn_item: bool,
4017 /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4018 /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4019 /// target-list SRFs follow — see round 67). The array-returning family keeps
4020 /// its own lowering; this channel carries the ones that have no array form
4021 /// (`generate_series`, a user `RETURNS SETOF` function).
4022 pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4023 /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4024 /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4025 /// tables (implicit LATERAL, like every SRF channel). Executed by
4026 /// walking the row path over the parsed doc, then each column's
4027 /// path per row-item; NESTED expands as a per-parent outer join.
4028 pub json_table: Option<Box<JsonTable>>,
4029}
4030
4031/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4032#[derive(Debug, Clone, PartialEq)]
4033pub struct JsonTable {
4034 /// The document expression (jsonb/json/text). May reference outer
4035 /// columns → implicit LATERAL.
4036 pub doc: Box<Expr>,
4037 /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4038 /// match is one row's context item.
4039 pub row_path: String,
4040 /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4041 pub columns: Vec<JsonTableColumn>,
4042 /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4043 pub passing: Vec<(String, Expr)>,
4044}
4045
4046/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4047#[derive(Debug, Clone, PartialEq)]
4048pub enum JsonTableColumn {
4049 /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4050 Ordinality { name: String },
4051 /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4052 /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4053 /// `<name> <type> EXISTS [PATH '<p>']`.
4054 Regular {
4055 name: String,
4056 ty: ColumnTypeName,
4057 /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4058 path: String,
4059 /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4060 exists: bool,
4061 /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4062 format_json: bool,
4063 /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4064 wrapper: bool,
4065 /// Behaviour when the path matches nothing (default NULL).
4066 on_empty: JsonTableOnBehavior,
4067 /// Behaviour when coercion fails (default NULL).
4068 on_error: JsonTableOnBehavior,
4069 },
4070 /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4071 /// row like a LEFT JOIN (a parent with no nested match still emits one
4072 /// row, nested cols NULL).
4073 Nested {
4074 path: String,
4075 columns: Vec<JsonTableColumn>,
4076 },
4077}
4078
4079/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4080#[derive(Debug, Clone, PartialEq)]
4081pub enum JsonTableOnBehavior {
4082 /// Default: the column value is NULL.
4083 Null,
4084 /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4085 Error,
4086 /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4087 Default(Box<Expr>),
4088}
4089
4090/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4091/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4092/// joins evaluate left-associatively in nested-loop order.
4093#[derive(Debug, Clone, PartialEq)]
4094pub struct FromClause {
4095 pub primary: TableRef,
4096 pub joins: Vec<FromJoin>,
4097}
4098
4099#[derive(Debug, Clone, PartialEq)]
4100pub struct FromJoin {
4101 pub kind: JoinKind,
4102 pub table: TableRef,
4103 /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4104 pub on: Option<Expr>,
4105 /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4106 /// USING column list so the executor can perform PG's column-merge
4107 /// (the join columns collapse to a single unqualified output column,
4108 /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4109 /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4110 /// USING into an equivalent `on` predicate so the join filter/count
4111 /// path works unchanged; `using_cols` drives only the output-shape
4112 /// rewrite. Empty/`None` for `ON` and CROSS joins.
4113 pub using_cols: Option<Vec<String>>,
4114 /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4115 /// column names are not known until the table schemas are available
4116 /// (parse time is schema-less), so the parser only sets this flag and
4117 /// leaves `on`/`using_cols` empty; the engine resolves the common
4118 /// columns at execution time, synthesises the `on` predicate + the
4119 /// USING column-merge, and clears the flag. If there are no common
4120 /// columns PG treats it as a CROSS join.
4121 pub natural: bool,
4122}
4123
4124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4125pub enum JoinKind {
4126 Inner,
4127 Left,
4128 Cross,
4129 /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4130 /// NULL-filling the left (drive) columns on unmatched right rows.
4131 /// The executor runs the LEFT algorithm's mirror: it tracks which
4132 /// peer rows matched and emits the unmatched ones with a NULL-left
4133 /// tuple after the probe loop. Output column order is unchanged
4134 /// (left-table cols then right-table cols).
4135 Right,
4136 /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4137 /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4138 FullOuter,
4139 /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4140 /// once, paired with the first peer row that satisfies the ON. Not
4141 /// reachable from SQL — the EXISTS pull-up emits it, which is what
4142 /// frees positive EXISTS from the round-721 uniqueness gate (an
4143 /// INNER join would multiply the outer rows; a semi join cannot).
4144 Semi,
4145}
4146
4147#[derive(Debug, Clone, PartialEq)]
4148pub enum Expr {
4149 Literal(Literal),
4150 Column(ColumnName),
4151 /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4152 /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4153 /// callee's declared parameter names, and a user function's live in the
4154 /// catalog — which the parser cannot see. So the name rides along in the
4155 /// tree and the evaluator, which has the catalog, does the reordering.
4156 /// Appears only inside a `FunctionCall`'s argument list.
4157 NamedArg {
4158 name: String,
4159 expr: Box<Expr>,
4160 },
4161 /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
4162 /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
4163 /// expression evaluates to an array whose elements the evaluator splices
4164 /// into the call as individual trailing arguments. Appears only inside a
4165 /// `FunctionCall`'s argument list.
4166 Variadic(Box<Expr>),
4167 /// v6.1.1 — `$N` parameter placeholder for the extended query
4168 /// protocol. The number is 1-based per PostgreSQL convention.
4169 /// Evaluation looks up `params[N-1]` from the prepared-statement
4170 /// bind buffer; out-of-range indices raise a runtime error
4171 /// (same shape as a column-not-found miss).
4172 Placeholder(u16),
4173 Binary {
4174 lhs: Box<Expr>,
4175 op: BinOp,
4176 rhs: Box<Expr>,
4177 },
4178 Unary {
4179 op: UnOp,
4180 expr: Box<Expr>,
4181 },
4182 /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
4183 /// TEXT, BOOL targets; engine coerces at evaluation time.
4184 Cast {
4185 expr: Box<Expr>,
4186 target: CastTarget,
4187 },
4188 /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
4189 /// evaluates to a composite/record value (an explicit `ROW(...)`, a
4190 /// whole-row reference, or a composite-returning function); `field` names
4191 /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
4192 /// column names for a whole-row). Only the parenthesised form reaches
4193 /// here — a bare `a.b` is parsed as a qualified column reference.
4194 FieldAccess {
4195 base: Box<Expr>,
4196 field: String,
4197 },
4198 /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
4199 IsNull {
4200 expr: Box<Expr>,
4201 negated: bool,
4202 },
4203 /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
4204 /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
4205 /// `Some(false)` for FALSE and `None` for UNKNOWN.
4206 ///
4207 /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
4208 /// The semantics were right, but the AST then had no way to say what
4209 /// the user wrote, so every renderer printed the lowering:
4210 /// `CHECK ((a > 1) IS TRUE)` came back as
4211 /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
4212 /// dumped view lost the form too.
4213 BoolTest {
4214 expr: Box<Expr>,
4215 value: Option<bool>,
4216 negated: bool,
4217 },
4218 /// Function call `name(args...)`. v1.4 supports a small built-in set
4219 /// (length, upper, lower, abs, coalesce); unknown names error at eval
4220 /// time so the parser stays open for v1.5 aggregates.
4221 FunctionCall {
4222 name: String,
4223 args: Vec<Expr>,
4224 },
4225 /// v7.24 (mailrs round-16 A) — an aggregate call with an
4226 /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
4227 /// Wraps the plain [`Expr::FunctionCall`] so every existing
4228 /// FunctionCall consumer stays untouched; only the aggregate
4229 /// executor (and the expression walkers) know the wrapper.
4230 /// Non-aggregate evaluation contexts reject it at eval time.
4231 AggregateOrdered {
4232 call: Box<Expr>,
4233 order_by: Vec<OrderBy>,
4234 /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
4235 /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
4236 /// aggregate modifier so plain FunctionCall stays untouched.
4237 distinct: bool,
4238 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
4239 /// Only the rows where `cond` is true contribute to this
4240 /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
4241 /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
4242 /// END)`, which is faithful for NULL-ignoring aggregates but
4243 /// WRONG for `array_agg` (it would collect a NULL per excluded
4244 /// row). The executor instead skips excluded rows before
4245 /// accumulation, which is correct for every aggregate.
4246 filter: Option<Box<Expr>>,
4247 },
4248 /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
4249 /// wildcards are `%` (any run) and `_` (one char), backslash escapes
4250 /// the next char (so `\%` matches a literal `%`).
4251 Like {
4252 expr: Box<Expr>,
4253 pattern: Box<Expr>,
4254 negated: bool,
4255 /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
4256 /// match. PG folds both operands.
4257 case_insensitive: bool,
4258 },
4259 /// v4.12 window function call: `name(args) OVER (PARTITION BY
4260 /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
4261 /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
4262 /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
4263 /// unordered windows and "from start of partition through
4264 /// current row" for ordered windows — no explicit ROWS /
4265 /// RANGE clause in v4.12 MVP.
4266 WindowFunction {
4267 name: String,
4268 args: Vec<Expr>,
4269 partition_by: Vec<Expr>,
4270 /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
4271 /// (None = PG default, same contract as [`OrderBy`]).
4272 order_by: Vec<(
4273 Expr,
4274 bool, /* desc */
4275 Option<bool>, /* nulls_first */
4276 )>,
4277 /// v4.20 explicit frame. `None` means "use the default":
4278 /// whole-partition when unordered, running aggregate from
4279 /// partition start through current row when ordered.
4280 frame: Option<WindowFrame>,
4281 /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
4282 /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
4283 /// `Respect` (PG / ANSI default — NULLs participate). Other
4284 /// window functions ignore this flag.
4285 null_treatment: NullTreatment,
4286 /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
4287 /// = no FILTER. Only aggregate window functions honor it; the
4288 /// predicate restricts which peer rows contribute within the frame.
4289 filter: Option<Box<Expr>>,
4290 },
4291 /// v4.10 scalar subquery — `(SELECT ...)` used in expression
4292 /// position. Must return exactly one row × one column at eval
4293 /// time; the engine errors out otherwise. Uncorrelated only —
4294 /// the inner SELECT cannot reference outer columns.
4295 ScalarSubquery(Box<SelectStatement>),
4296 /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
4297 /// projection is ignored; only row-count matters.
4298 Exists {
4299 subquery: Box<SelectStatement>,
4300 negated: bool,
4301 },
4302 /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
4303 /// project exactly one column; membership is tested by Eq
4304 /// against each row's value (NULL handling follows ANSI:
4305 /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
4306 InSubquery {
4307 expr: Box<Expr>,
4308 subquery: Box<SelectStatement>,
4309 negated: bool,
4310 },
4311 /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
4312 /// against a multi-column subquery. Row comparisons against a *list*
4313 /// decompose to OR-of-AND at parse time, but the subquery form can't
4314 /// (its rows are only known at runtime), so this survives as its own
4315 /// node evaluated with PG's row-comparison three-valued logic.
4316 RowInSubquery {
4317 row: Vec<Expr>,
4318 subquery: Box<SelectStatement>,
4319 negated: bool,
4320 },
4321 /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
4322 /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
4323 /// RowInSubquery, the literal-RHS form decomposes at parse time but the
4324 /// subquery form can't, so it survives as its own node. The subquery
4325 /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
4326 RowCmpSubquery {
4327 row: Vec<Expr>,
4328 op: BinOp,
4329 subquery: Box<SelectStatement>,
4330 },
4331 /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
4332 /// list. Both the parser's literal-list path and the engine's
4333 /// IN-subquery materialisation used to desugar into a left-deep
4334 /// OR-Eq chain, so expression depth scaled with the element count
4335 /// — a 24k-row subquery result overflowed the 2 MiB worker stack
4336 /// (recursive eval AND recursive Box drop) and aborted embedding
4337 /// host processes. The flat node keeps depth constant: eval is an
4338 /// iterative scan with PG three-valued logic, drop is a Vec drop.
4339 InList {
4340 expr: Box<Expr>,
4341 list: Vec<Expr>,
4342 negated: bool,
4343 },
4344 /// `EXTRACT(<field> FROM <source>)` — pull an integer component
4345 /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
4346 /// because the `FROM` keyword is what separates the two halves,
4347 /// not a comma.
4348 Extract {
4349 field: ExtractField,
4350 source: Box<Expr>,
4351 },
4352 /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
4353 /// element is evaluated independently; NULLs are allowed.
4354 /// v7.10 supports only single-dimension TEXT[] semantically;
4355 /// non-text elements coerce at engine evaluation time when
4356 /// the surrounding context (column type / cast) makes the
4357 /// target clear.
4358 Array(Vec<Expr>),
4359 /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
4360 /// engine returns NULL for out-of-range indices.
4361 ArraySubscript {
4362 target: Box<Expr>,
4363 index: Box<Expr>,
4364 },
4365 /// Array slice `arr[lo:hi]` — PG 1-based, both ends
4366 /// inclusive; a missing bound extends to that end of the
4367 /// array and out-of-range bounds clamp. Returns an array of
4368 /// the same element type.
4369 ArraySlice {
4370 target: Box<Expr>,
4371 lo: Option<Box<Expr>>,
4372 hi: Option<Box<Expr>>,
4373 },
4374 /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
4375 /// operator is the comparison binary op (Eq / Ne / Lt / …);
4376 /// the engine desugars: `ANY` returns true if any element
4377 /// satisfies; `ALL` returns true only if every element does.
4378 /// NULL handling follows PG's three-valued logic.
4379 AnyAll {
4380 expr: Box<Expr>,
4381 op: BinOp,
4382 array: Box<Expr>,
4383 /// `true` = ANY, `false` = ALL.
4384 is_any: bool,
4385 },
4386 /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
4387 /// (searched form, `operand` is None) and
4388 /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
4389 /// `operand` is the lead expression compared against each
4390 /// branch's match). Each `(when_expr, then_expr)` branch
4391 /// stays as written; engine short-circuits on the first match.
4392 /// `else_branch` is `None` when no ELSE; evaluates to NULL.
4393 /// mailrs round-5 G9.
4394 Case {
4395 operand: Option<Box<Expr>>,
4396 branches: Vec<(Expr, Expr)>,
4397 else_branch: Option<Box<Expr>>,
4398 },
4399}
4400
4401/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
4402/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
4403/// in the offset walk. `Ignore` causes the function to skip NULL
4404/// values in the argument expression, returning the next non-NULL.
4405#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4406pub enum NullTreatment {
4407 #[default]
4408 Respect,
4409 Ignore,
4410}
4411
4412/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
4413/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
4414/// where end implicitly = CURRENT ROW.
4415#[derive(Debug, Clone, PartialEq, Eq)]
4416pub struct WindowFrame {
4417 pub kind: FrameKind,
4418 pub start: FrameBound,
4419 pub end: Option<FrameBound>,
4420 /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
4421 /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
4422 /// no-op; CURRENT ROW drops the current row from the frame.
4423 pub exclude: FrameExclusion,
4424}
4425
4426#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4427pub enum FrameExclusion {
4428 /// Default — exclude nothing.
4429 #[default]
4430 NoOthers,
4431 /// Drop the current row from the frame.
4432 CurrentRow,
4433 /// Drop the current row's whole peer group.
4434 Group,
4435 /// Drop the current row's peers but keep the current row.
4436 Ties,
4437}
4438
4439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4440pub enum FrameKind {
4441 Rows,
4442 Range,
4443 /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
4444 /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
4445 /// bounds (no explicit integer offsets) GROUPS behaves identically
4446 /// to RANGE — both consult the peer-group of the current row.
4447 /// Integer offsets are not yet supported; the executor rejects
4448 /// them at run time.
4449 Groups,
4450}
4451
4452#[derive(Debug, Clone, PartialEq, Eq)]
4453pub enum FrameBound {
4454 UnboundedPreceding,
4455 OffsetPreceding(u64),
4456 CurrentRow,
4457 OffsetFollowing(u64),
4458 UnboundedFollowing,
4459 /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
4460 /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
4461 /// interval is folded to its (months, days, micros) components at
4462 /// parse time.
4463 IntervalPreceding {
4464 months: i32,
4465 days: i32,
4466 micros: i64,
4467 },
4468 IntervalFollowing {
4469 months: i32,
4470 days: i32,
4471 micros: i64,
4472 },
4473}
4474
4475impl fmt::Display for FrameBound {
4476 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4477 match self {
4478 Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
4479 Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
4480 Self::CurrentRow => f.write_str("CURRENT ROW"),
4481 Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
4482 Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
4483 Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
4484 Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
4485 }
4486 }
4487}
4488
4489#[derive(Debug, Clone, PartialEq, Eq)]
4490pub enum ExtractField {
4491 Year,
4492 Month,
4493 Day,
4494 Hour,
4495 Minute,
4496 Second,
4497 Microsecond,
4498 /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
4499 /// SPG keeps the integer convention — truncated seconds).
4500 Epoch,
4501 /// Day of week, 0 = Sunday … 6 = Saturday.
4502 Dow,
4503 /// ISO day of week, 1 = Monday … 7 = Sunday.
4504 Isodow,
4505 /// Day of year, 1-366.
4506 Doy,
4507 /// ISO 8601 week number, 1-53.
4508 Week,
4509 /// ISO 8601 week-numbering year (pairs with `Week`).
4510 Isoyear,
4511 /// Quarter, 1-4.
4512 Quarter,
4513 /// Year divided by 10 (floor).
4514 Decade,
4515 /// Century — 2001-2100 is century 21.
4516 Century,
4517 /// Millennium — 2001-3000 is millennium 3.
4518 Millennium,
4519 /// Julian day number (truncated for timestamps).
4520 Julian,
4521 /// Seconds and fraction in milliseconds (ss·1000 + frac).
4522 Millisecond,
4523 /// UTC offset in seconds — SPG sessions run UTC, so 0.
4524 Timezone,
4525 /// Hour component of the UTC offset — 0.
4526 TimezoneHour,
4527 /// Minute component of the UTC offset — 0.
4528 TimezoneMinute,
4529 /// v7.39 (round 253) — a field name the parser does not know. PG
4530 /// resolves EXTRACT fields at RUNTIME and reports them with the
4531 /// source type (`unit "nosuch" not recognized for type timestamp
4532 /// without time zone`, 22023), so the parser carries the raw name
4533 /// instead of rejecting.
4534 Other(String),
4535}
4536
4537impl fmt::Display for ExtractField {
4538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4539 f.write_str(match self {
4540 Self::Year => "YEAR",
4541 Self::Month => "MONTH",
4542 Self::Day => "DAY",
4543 Self::Hour => "HOUR",
4544 Self::Minute => "MINUTE",
4545 Self::Second => "SECOND",
4546 Self::Microsecond => "MICROSECOND",
4547 Self::Epoch => "EPOCH",
4548 Self::Dow => "DOW",
4549 Self::Isodow => "ISODOW",
4550 Self::Doy => "DOY",
4551 Self::Week => "WEEK",
4552 Self::Isoyear => "ISOYEAR",
4553 Self::Quarter => "QUARTER",
4554 Self::Decade => "DECADE",
4555 Self::Century => "CENTURY",
4556 Self::Millennium => "MILLENNIUM",
4557 Self::Julian => "JULIAN",
4558 Self::Millisecond => "MILLISECOND",
4559 Self::Timezone => "TIMEZONE",
4560 Self::TimezoneHour => "TIMEZONE_HOUR",
4561 Self::TimezoneMinute => "TIMEZONE_MINUTE",
4562 Self::Other(name) => return f.write_str(name),
4563 })
4564 }
4565}
4566
4567#[derive(Debug, Clone, PartialEq, Eq)]
4568pub enum CastTarget {
4569 Int,
4570 BigInt,
4571 Float,
4572 Text,
4573 Bool,
4574 Vector,
4575 Date,
4576 Timestamp,
4577 /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
4578 /// H3a. Engine reuses the existing runtime-interval / timestamp
4579 /// paths (parse the text input, return the matching Value).
4580 Interval,
4581 Timestamptz,
4582 /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
4583 /// types (v7.9.0); the cast just routes Text→Json with the
4584 /// requested OID for the wire layer.
4585 Json,
4586 Jsonb,
4587 /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
4588 /// compatibility; engine surfaces as Unsupported with a
4589 /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
4590 RegType,
4591 RegClass,
4592 /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
4593 /// the PG external array form `{a,b,NULL}`.
4594 TextArray,
4595 /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
4596 /// `{1,2,3}` or widens a `TextArray` whose elements are
4597 /// integer-shaped.
4598 IntArray,
4599 BigIntArray,
4600 /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
4601 /// external form text representation. Used by pg_dump output
4602 /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
4603 TsVector,
4604 TsQuery,
4605 /// v7.17.0 — `::uuid`. Decodes the LHS Text via
4606 /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
4607 /// unhyphenated, uppercase, and brace-wrapped forms); malformed
4608 /// input is a SQL error.
4609 Uuid,
4610 /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
4611 /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
4612 /// inputs pass through unchanged. Closes the mailrs D-pre #3
4613 /// reverse-acceptance gap — anywhere a PG schema writes
4614 /// `expr::bytea`, SPG now matches.
4615 Bytea,
4616 /// v7.37.5 ship triage — generic cast target for the long tail
4617 /// of PG type names the parser meets in `expr::TYPE` shapes that
4618 /// don't deserve their own enum variant. The engine routes these
4619 /// through `column_type_to_data_type` + the existing typed
4620 /// `coerce_value` dispatch, so adding a new PG type to SPG
4621 /// implicitly adds its cast-target form too — no parser change
4622 /// per type. The string carries the lowercase PG type ident
4623 /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
4624 /// a clear message when the type isn't known.
4625 Named(String),
4626}
4627
4628impl fmt::Display for CastTarget {
4629 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4630 f.write_str(match self {
4631 Self::Int => "int",
4632 Self::BigInt => "bigint",
4633 Self::Float => "float",
4634 Self::Text => "text",
4635 Self::Bool => "bool",
4636 Self::Vector => "vector",
4637 Self::Interval => "interval",
4638 Self::Timestamptz => "timestamptz",
4639 Self::Json => "json",
4640 Self::Jsonb => "jsonb",
4641 Self::RegType => "regtype",
4642 Self::RegClass => "regclass",
4643 Self::Date => "date",
4644 Self::Timestamp => "timestamp",
4645 Self::TextArray => "TEXT[]",
4646 Self::IntArray => "INT[]",
4647 Self::BigIntArray => "BIGINT[]",
4648 Self::TsVector => "tsvector",
4649 Self::TsQuery => "tsquery",
4650 Self::Uuid => "uuid",
4651 Self::Bytea => "bytea",
4652 // v7.37.5 — `Self::Named` carries its own canonical name.
4653 Self::Named(name) => return f.write_str(name),
4654 })
4655 }
4656}
4657
4658#[derive(Debug, Clone, PartialEq)]
4659pub enum Literal {
4660 Integer(i64),
4661 Float(f64),
4662 /// Exact decimal literal — a bare `12.34`-style token, kept as
4663 /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
4664 /// before it becomes a `Value::Numeric`. PG parses such literals as
4665 /// `numeric`, not `double precision`. (Scientific/huge literals stay
4666 /// `Float`.)
4667 Numeric {
4668 unscaled: i128,
4669 /// v7.39 (round 271) — widened to u16. At u8 a literal with more
4670 /// than 255 decimal places could not be represented, and the
4671 /// conversion's `.expect("lexer-validated decimal")` aborted the
4672 /// query with an internal error on SQL PG accepts.
4673 scale: u16,
4674 },
4675 /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
4676 /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
4677 /// `Value::NumericBig` at eval; previously such literals fell back to double.
4678 NumericBig(String),
4679 String(String),
4680 /// v7.38.8 — a temporal constant that has already been decoded.
4681 ///
4682 /// Without these the only way to carry one through the AST was as
4683 /// text, and a predicate comparing a `timestamp` column against a
4684 /// literal then coerced that text back into a timestamp ONCE PER
4685 /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
4686 /// profile. `constfold` produced text for the same reason: its exit
4687 /// had nothing else to hand back.
4688 ///
4689 /// `text` keeps the spelling so `Display` round-trips byte for byte,
4690 /// the way `Interval` already does and for the same reason: this
4691 /// node is printed in EXPLAIN, in dumps and in error messages, and
4692 /// none of those should change because the value stopped being
4693 /// carried as a string. The enum already holds a `String` and an
4694 /// `i128`, so neither variant widens it.
4695 Timestamp {
4696 micros: i64,
4697 text: String,
4698 },
4699 /// Days since the epoch `Value::Date` counts from. See
4700 /// [`Literal::Timestamp`].
4701 Date {
4702 days: i32,
4703 text: String,
4704 },
4705 Bool(bool),
4706 Null,
4707 /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
4708 Vector(Vec<f32>),
4709 /// TEXT[] value carried through the prepared-bind path
4710 /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
4711 /// text form, so the array rides the AST natively).
4712 TextArray(Vec<Option<String>>),
4713 /// INT[] value carried through the prepared-bind path.
4714 IntArray(Vec<Option<i32>>),
4715 /// BIGINT[] value carried through the prepared-bind path.
4716 BigIntArray(Vec<Option<i64>>),
4717 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
4718 /// Three independent dimensions: `months` (variable-length;
4719 /// year/month), `days` (fixed 86400 seconds at non-DST, but
4720 /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
4721 /// stays distinguishable), and `micros` (sub-day; can carry).
4722 /// `text` keeps the original spelling so Display round-trips
4723 /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
4724 Interval {
4725 months: i32,
4726 days: i32,
4727 micros: i64,
4728 text: String,
4729 },
4730}
4731
4732#[derive(Debug, Clone, PartialEq, Eq)]
4733pub struct ColumnName {
4734 pub qualifier: Option<String>,
4735 pub name: String,
4736}
4737
4738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4739pub enum BinOp {
4740 Or,
4741 And,
4742 Eq,
4743 NotEq,
4744 /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
4745 /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
4746 /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
4747 /// non-NULL behaviour matches `<>` / `=` exactly. Common in
4748 /// PG-style JOIN ON predicates and pg_dump output.
4749 IsDistinctFrom,
4750 IsNotDistinctFrom,
4751 /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
4752 /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
4753 /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
4754 /// is a real division (round 351).
4755 IntDiv,
4756 Lt,
4757 LtEq,
4758 Gt,
4759 GtEq,
4760 Add,
4761 Sub,
4762 Mul,
4763 Div,
4764 /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
4765 /// precedence as Mul/Div; result type follows left operand.
4766 Mod,
4767 /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
4768 /// operands of equal dimension; engine returns `Value::Float(d)`.
4769 L2Distance,
4770 /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
4771 GeomParallel,
4772 /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
4773 OverLeft,
4774 OverRight,
4775 /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
4776 GeomPerp,
4777 /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
4778 GeomSameAs,
4779 /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
4780 /// object to the left-hand one.
4781 ClosestPoint,
4782 /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
4783 GeomHoriz,
4784 /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
4785 /// more similar" remains true (matches pgvector's published convention).
4786 InnerProduct,
4787 /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
4788 CosineDistance,
4789 /// SQL string concatenation `||`. NULL propagates.
4790 Concat,
4791 /// Bitwise OR `|` on integers.
4792 BitOr,
4793 /// Bitwise AND `&` on integers.
4794 BitAnd,
4795 /// Bitwise XOR `#` on integers and equal-length bit strings.
4796 BitXor,
4797 /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
4798 /// sides as truth values and returns their exclusive-or (`1 XOR 0`
4799 /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
4800 /// MySQL dialect produces it; PG has no logical XOR. Its precedence
4801 /// sits between OR (loosest) and AND.
4802 LogicalXor,
4803 /// v4.14 `json -> key` — element access by string key (object)
4804 /// or integer index (array). Returns a JSON value.
4805 JsonGet,
4806 /// v4.14 `json ->> key` — same access, returns the result as
4807 /// TEXT (unwraps a top-level JSON string; renders other scalars
4808 /// as their canonical text).
4809 JsonGetText,
4810 /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
4811 /// text array literal like `'{a,0,b}'`. Returns JSON.
4812 JsonGetPath,
4813 /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
4814 JsonGetPathText,
4815 /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
4816 /// when every key/value in `sub_json` is structurally present in
4817 /// the left side. Matches PG semantics (top-level + recursive).
4818 JsonContains,
4819 /// `@?` — jsonb path existence (jsonb_path_exists).
4820 JsonPathExists,
4821 /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
4822 /// `a <@ b` is defined as `b @> a` (same semantics, swapped
4823 /// sides). Eval dispatch reuses `JsonContains` with swapped args.
4824 JsonContainedBy,
4825 /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
4826 /// returns BOOL. For an object, true if `key` is an existing
4827 /// member name; for an array, true if any element is the string
4828 /// `key` (PG semantics).
4829 JsonKeyExists,
4830 /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
4831 /// returns BOOL.
4832 JsonKeysAny,
4833 /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
4834 /// returns BOOL.
4835 JsonKeysAll,
4836 /// `jsonb #- path_text[]` — delete the value at a nested path.
4837 /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
4838 JsonDeletePath,
4839 /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
4840 /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
4841 /// tsvector` and engine eval normalises either ordering.
4842 TsMatch,
4843 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
4844 /// `<<`. LHS network is strictly inside RHS network (no equality).
4845 InetContainedBy,
4846 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
4847 /// `<<=`. LHS network ⊆ RHS network.
4848 InetContainedByEq,
4849 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
4850 /// LHS network strictly contains RHS network.
4851 InetContains,
4852 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
4853 /// LHS network ⊇ RHS network.
4854 InetContainsEq,
4855 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
4856 /// True iff either network contains any address of the other.
4857 InetOverlap,
4858 /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
4859 /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
4860 Intersects,
4861 /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
4862 /// (point, box).
4863 IsBelow,
4864 IsAbove,
4865 /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
4866 /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
4867 /// where `'A' < 'a'` is false under a non-C collation, which is the
4868 /// whole reason the operator family exists — it is what makes a LIKE
4869 /// prefix index-usable. pg_dump writes these into index definitions.
4870 PatternLt,
4871 PatternLtEq,
4872 PatternGt,
4873 PatternGtEq,
4874}
4875
4876#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4877pub enum UnOp {
4878 Not,
4879 Neg,
4880 /// Bitwise NOT `~` on integers.
4881 BitNot,
4882 /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
4883 /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
4884 /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
4885 /// while PG18 and MariaDB accept every one of them.
4886 ///
4887 /// It is not a no-op to drop at parse time — PG refuses it on
4888 /// non-numeric operands ("operator does not exist: + boolean"), so the
4889 /// operand's type has to be seen at eval.
4890 Plus,
4891}
4892
4893// --- Display impls (round-trip-safe) --------------------------------------
4894
4895impl Statement {
4896 /// v7.18 — classify whether the statement is read-only at
4897 /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
4898 /// route SELECT-shaped traffic through the fan-out
4899 /// `AsyncReadHandle` (no writer-lock contention) while
4900 /// keeping DML / DDL / TX-control on the single-writer path.
4901 ///
4902 /// The classification matches what
4903 /// `Engine::execute_readonly_with_cancel` accepts: anything
4904 /// that does NOT mutate catalog, statistics, session state,
4905 /// or transaction state. WaitForWalPosition is included
4906 /// (engine returns `Unsupported`, but the classification is
4907 /// semantically read-only — no mutation). Empty is excluded
4908 /// out of an abundance of caution — the no-op routes
4909 /// through the writer so any future side effect lands
4910 /// uniformly.
4911 ///
4912 /// **Not connection-state aware**. `SET LOCAL` / `RESET`
4913 /// affect session parameters and must run on the writer
4914 /// engine that owns the session state; they classify as
4915 /// writer-path here. Same for `BEGIN` / `COMMIT` /
4916 /// `ROLLBACK` / `SAVEPOINT` — transaction control is
4917 /// always writer-path.
4918 /// v7.39 (round 435) — does this statement implicitly COMMIT an open
4919 /// transaction under MySQL?
4920 ///
4921 /// PG runs DDL inside the transaction; MySQL commits before (and after)
4922 /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
4923 /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
4924 /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
4925 /// nested START TRANSACTION; and measured NOT to fire for `CREATE
4926 /// TEMPORARY TABLE`, `SET`, or a SELECT.
4927 ///
4928 /// A positive list, not "everything that is not DML": a statement
4929 /// wrongly listed here commits a client's data early, which is as bad as
4930 /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
4931 /// COMPACT) are left out — a MySQL session never sends them.
4932 #[must_use]
4933 pub fn mysql_implicit_commit(&self) -> bool {
4934 match self {
4935 // MySQL's documented exception, measured on MariaDB 11: a
4936 // TEMPORARY table is not DDL for this purpose and does not
4937 // commit. (Round 435 got this for free because the parser then
4938 // lowered that spelling to `Statement::Empty`; round 436 made it
4939 // a real CREATE TABLE, and the round-435 pin caught it.)
4940 Self::CreateTable(c) => !c.temporary,
4941 // MySQL commits the open transaction and opens a fresh one.
4942 Self::Begin { .. }
4943 | Self::DropTable { .. }
4944 | Self::DropIndex { .. }
4945 | Self::CreateIndex(_)
4946 | Self::AlterIndex { .. }
4947 | Self::AlterTable(_)
4948 | Self::Truncate { .. }
4949 | Self::Analyze { .. }
4950 | Self::CreateStatistics { .. }
4951 | Self::DropStatistics { .. }
4952 | Self::CreateView { .. }
4953 | Self::DropView { .. }
4954 | Self::CreateMaterializedView { .. }
4955 | Self::RefreshMaterializedView { .. }
4956 | Self::DropMaterializedView { .. }
4957 | Self::CreateSequence(_)
4958 | Self::AlterSequence { .. }
4959 | Self::DropSequence { .. }
4960 | Self::CreateFunction(_)
4961 | Self::DropFunction { .. }
4962 | Self::CreateTrigger(_)
4963 | Self::DropTrigger { .. }
4964 | Self::CreateRule(_)
4965 | Self::DropRule { .. }
4966 | Self::CreateType(_)
4967 | Self::DropType { .. }
4968 | Self::AlterTypeAddValue { .. }
4969 | Self::AlterTypeRenameValue { .. }
4970 | Self::CreateDomain(_)
4971 | Self::AlterDomain { .. }
4972 | Self::DropDomain { .. }
4973 | Self::CreateSchema { .. }
4974 | Self::DropSchema { .. }
4975 | Self::CreateUser { .. }
4976 | Self::DropUser { .. }
4977 | Self::Grant { .. }
4978 | Self::Revoke { .. }
4979 | Self::CreatePolicy(_)
4980 | Self::AlterPolicy(_)
4981 | Self::DropPolicy { .. }
4982 | Self::CommentOn { .. }
4983 | Self::CreateExtension { .. } => true,
4984 _ => false,
4985 }
4986 }
4987
4988 #[must_use]
4989 pub fn is_readonly(&self) -> bool {
4990 match self {
4991 // v7.39 (round 288) — SET CONSTRAINTS changes transaction
4992 // state, and IMMEDIATE can run the deferred checks there and
4993 // then; writer-path.
4994 Statement::SetConstraints { .. } => false,
4995 // v7.39 (round 695) — it writes nothing (SPG has no
4996 // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
4997 // writer and a read-only session refuses it there too.
4998 Statement::AlterSystem { .. } => false,
4999 // Same shape: a no-op here, a writer to PG, so a read-only
5000 // session refuses it as PG's would.
5001 Statement::NoOpPreventedInTransaction { .. } => false,
5002 Statement::DropDatabase { .. } => false,
5003 // v7.39 (round 696) — they perform nothing, so nothing is
5004 // written; PG classes LOCK and the OWNED BY pair as writers and
5005 // a read-only session refuses them there.
5006 Statement::ValidateOnly { .. } => false,
5007 // v7.39 (round 750) — a credential rotation persists.
5008 Statement::AlterRolePassword { .. } => true,
5009 Statement::DropAggregate { .. } => false,
5010 // v7.39 (round 547) — records a GUC default in the catalog.
5011 Statement::SetDbRoleSetting(_) => false,
5012 // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5013 // but they name a relation and PG refuses one that is not
5014 // there, so they are not read-only in the sense this asks.
5015 Statement::Maintain { .. } => false,
5016 // v7.39 (round 277) — the prepared-statement surface is
5017 // session state, like SET; writer-path so it lands on the
5018 // engine that owns the session. EXECUTE may also run a
5019 // write, and its body is only known at execution time.
5020 Statement::Prepare { .. }
5021 | Statement::Execute { .. }
5022 | Statement::Deallocate(_)
5023 | Statement::Call(_)
5024 | Statement::PrepareTransaction(_)
5025 | Statement::CreateStatistics { .. }
5026 | Statement::DropStatistics { .. }
5027 // v7.39 (round 318, V51) — KILL signals another connection;
5028 // it must run on the writer path that owns the registry hook.
5029 | Statement::Kill { .. }
5030 // v7.39 (round 320, V53) — DISCARD throws session state away;
5031 // writer path, like SET / RESET.
5032 | Statement::Discard(_) => false,
5033 // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5034 // locks MUTATES the lock table, so it is not a read. Left as
5035 // a read it went to the read-only executor and the locking
5036 // pre-pass never ran at all — the clause was honoured only
5037 // inside an explicit transaction, and silently ignored in
5038 // autocommit, which is where a queue worker runs it.
5039 Statement::Select(s) if s.locking.is_some() => false,
5040 Statement::Select(_)
5041 | Statement::CopyTo { .. }
5042 | Statement::CopyToFile { .. }
5043 | Statement::Explain(_)
5044 | Statement::ShowTables
5045 | Statement::ShowDatabases
5046 | Statement::ShowCreateTable(_)
5047 | Statement::ShowIndexes(_)
5048 | Statement::ShowStatus
5049 | Statement::ShowVariables
5050 | Statement::ShowVariablesLike(_)
5051 | Statement::ShowProcesslist
5052 | Statement::ShowColumns(_)
5053 | Statement::ShowUsers
5054 | Statement::ShowPublications
5055 | Statement::ShowSubscriptions
5056 | Statement::WaitForWalPosition { .. } => true,
5057 // Everything else mutates catalog, statistics,
5058 // session state, or transaction state — writer path.
5059 // Listed explicitly so a new Statement variant fails
5060 // the match exhaustiveness check and forces a
5061 // classification decision at add-site.
5062 Statement::Empty
5063 // v7.39 (round 169) — VACUUM mutates storage (reclaims
5064 // tombstoned versions): writer path.
5065 | Statement::Vacuum { .. }
5066 | Statement::DropTable { .. }
5067 | Statement::DropIndex { .. }
5068 | Statement::CreateTable(_)
5069 | Statement::CreateExtension(_)
5070 | Statement::DoBlock(_)
5071 | Statement::CreateIndex(_)
5072 | Statement::Insert(_)
5073 | Statement::Update(_)
5074 | Statement::Delete(_)
5075 | Statement::Merge(_)
5076 | Statement::Begin(_)
5077 | Statement::Commit
5078 | Statement::Rollback
5079 | Statement::Savepoint(_)
5080 | Statement::RollbackToSavepoint(_)
5081 | Statement::ReleaseSavepoint(_)
5082 | Statement::CreateUser(_)
5083 | Statement::DropUser { .. }
5084 | Statement::SetRole(_)
5085 | Statement::Grant(_)
5086 | Statement::Revoke(_)
5087 | Statement::CreatePolicy(_)
5088 | Statement::AlterPolicy(_)
5089 | Statement::DropPolicy(_)
5090 | Statement::AlterIndex(_)
5091 | Statement::AlterTable(_)
5092 | Statement::CreatePublication(_)
5093 | Statement::DropPublication { .. }
5094 | Statement::CreateSubscription(_)
5095 | Statement::DropSubscription { .. }
5096 | Statement::Analyze(_)
5097 | Statement::Truncate { .. }
5098 | Statement::CompactColdSegments
5099 | Statement::SetParameter { .. }
5100 | Statement::SetParameterList(_)
5101 | Statement::SetUserVars(..)
5102 | Statement::SetTransaction { .. }
5103 | Statement::ShowParameter(_)
5104 | Statement::ResetParameter(_)
5105 | Statement::CreateFunction(_)
5106 | Statement::CreateTrigger(_)
5107 | Statement::DropTrigger { .. }
5108 | Statement::CreateRule(_)
5109 | Statement::DropRule { .. }
5110 | Statement::DropFunction { .. }
5111 | Statement::CreateSequence(_)
5112 | Statement::AlterSequence(_)
5113 | Statement::DropSequence { .. }
5114 | Statement::CreateView(_)
5115 | Statement::DropView { .. }
5116 | Statement::CreateMaterializedView(_)
5117 | Statement::RefreshMaterializedView { .. }
5118 | Statement::DropMaterializedView { .. }
5119 | Statement::CreateType(_)
5120 | Statement::AlterTypeAddValue { .. }
5121 | Statement::AlterTypeRenameValue { .. }
5122 | Statement::CommentOn { .. }
5123 | Statement::DropType { .. }
5124 | Statement::CreateDomain(_)
5125 | Statement::DropDomain { .. }
5126 | Statement::CreateSchema { .. }
5127 | Statement::DropSchema { .. }
5128 // v7.39 (round 218) — cursors mutate per-session cursor state
5129 // (open/position/close) on the writer engine: writer path.
5130 | Statement::DeclareCursor { .. }
5131 | Statement::FetchCursor { .. }
5132 | Statement::MoveCursor { .. }
5133 | Statement::CloseCursor { .. }
5134 // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5135 // state / the notification queue: writer path.
5136 | Statement::Listen(_)
5137 | Statement::Notify { .. }
5138 | Statement::Unlisten(_)
5139 | Statement::CopyFromFile { .. }
5140 | Statement::AlterDomain { .. } => false,
5141 }
5142 }
5143}
5144
5145/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5146/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5147#[derive(Debug, Clone, PartialEq, Eq)]
5148pub struct GrantStatement {
5149 /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5150 /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5151 /// is why they keep the case the user typed.
5152 pub privileges: Vec<GrantPriv>,
5153 /// What the privileges are on.
5154 pub object: GrantObject,
5155 /// The roles granted to / revoked from. An empty string entry = PUBLIC.
5156 pub grantees: Vec<String>,
5157 /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
5158 /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
5159 /// privilege itself).
5160 pub grant_option: bool,
5161}
5162
5163/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
5164/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
5165/// An empty column list means the privilege is table-wide.
5166#[derive(Debug, Clone, PartialEq, Eq)]
5167pub struct GrantPriv {
5168 pub word: String,
5169 pub columns: Vec<String>,
5170}
5171
5172/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
5173/// privileges; every other object class parses and is accepted as a no-op, so
5174/// a pg_dump that grants on schemas / sequences / functions still restores.
5175#[derive(Debug, Clone, PartialEq, Eq)]
5176pub enum GrantObject {
5177 /// `ON [TABLE] a, b` — the enforced case.
5178 Tables(Vec<String>),
5179 /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
5180 /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
5181 /// granted roles; the grantees are the members.
5182 Roles(Vec<String>),
5183 /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
5184 /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
5185 Sequences(Vec<String>),
5186 /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
5187 Schemas(Vec<String>),
5188 /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
5189 Databases(Vec<String>),
5190 /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
5191 /// (SPG keys functions by name); the argument list parses and is dropped.
5192 Functions(Vec<(String, Option<Vec<String>>)>),
5193 /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
5194 /// every table at GRANT time, exactly like PG.
5195 AllTablesInSchema,
5196 /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
5197 /// message.
5198 Other(String),
5199}
5200
5201impl GrantStatement {
5202 /// Round-trip text. `grant = false` renders the REVOKE form.
5203 fn render(&self, grant: bool) -> alloc::string::String {
5204 use core::fmt::Write as _;
5205 let mut s = alloc::string::String::new();
5206 let privs = if self.privileges.is_empty() {
5207 alloc::string::String::from("ALL")
5208 } else {
5209 let parts: Vec<_> = self
5210 .privileges
5211 .iter()
5212 .map(|p| {
5213 if p.columns.is_empty() {
5214 p.word.clone()
5215 } else {
5216 let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
5217 alloc::format!("{} ({})", p.word, cols.join(", "))
5218 }
5219 })
5220 .collect();
5221 parts.join(", ")
5222 };
5223 let obj = match &self.object {
5224 GrantObject::Tables(t) => {
5225 let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
5226 alloc::format!("TABLE {}", names.join(", "))
5227 }
5228 GrantObject::Roles(r) => {
5229 let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
5230 names.join(", ")
5231 }
5232 GrantObject::Sequences(n) => {
5233 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5234 alloc::format!("SEQUENCE {}", names.join(", "))
5235 }
5236 GrantObject::Schemas(n) => {
5237 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5238 alloc::format!("SCHEMA {}", names.join(", "))
5239 }
5240 GrantObject::Databases(n) => {
5241 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5242 alloc::format!("DATABASE {}", names.join(", "))
5243 }
5244 GrantObject::Functions(n) => {
5245 let names: Vec<_> = n
5246 .iter()
5247 .map(|(name, args)| match args {
5248 Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
5249 None => quote_ident(name),
5250 })
5251 .collect();
5252 alloc::format!("FUNCTION {}", names.join(", "))
5253 }
5254 GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
5255 GrantObject::Other(k) => k.clone(),
5256 };
5257 let who: Vec<_> = self
5258 .grantees
5259 .iter()
5260 .map(|g| {
5261 if g.is_empty() {
5262 "PUBLIC".into()
5263 } else {
5264 quote_ident(g)
5265 }
5266 })
5267 .collect();
5268 if let GrantObject::Roles(_) = &self.object {
5269 let _ = if grant {
5270 write!(s, "GRANT {obj} TO {}", who.join(", "))
5271 } else {
5272 write!(s, "REVOKE {obj} FROM {}", who.join(", "))
5273 };
5274 return s;
5275 }
5276 if grant {
5277 let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
5278 if self.grant_option {
5279 s.push_str(" WITH GRANT OPTION");
5280 }
5281 } else {
5282 s.push_str("REVOKE ");
5283 if self.grant_option {
5284 s.push_str("GRANT OPTION FOR ");
5285 }
5286 let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
5287 }
5288 s
5289 }
5290}
5291
5292impl fmt::Display for Statement {
5293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5294 match self {
5295 Self::Empty => Ok(()),
5296 // v7.39 (round 695) — deparsed the way PG writes it.
5297 // v7.39 (round 696) — never deparsed into a dump (nothing is
5298 // stored), so the shortest faithful spelling of what it was.
5299 Self::DropAggregate { if_exists, items } => {
5300 f.write_str("DROP AGGREGATE ")?;
5301 if *if_exists {
5302 f.write_str("IF EXISTS ")?;
5303 }
5304 for (i, (name, args)) in items.iter().enumerate() {
5305 if i > 0 {
5306 f.write_str(", ")?;
5307 }
5308 match args {
5309 Some(a) => write!(f, "{name}({})", a.join(", "))?,
5310 None => write!(f, "{name}(*)")?,
5311 }
5312 }
5313 Ok(())
5314 }
5315 Self::AlterRolePassword { name, password } => {
5316 write!(f, "ALTER ROLE {}", quote_ident(name))?;
5317 match password {
5318 Some(_) => f.write_str(" PASSWORD '<redacted>'"),
5319 None => f.write_str(" PASSWORD NULL"),
5320 }
5321 }
5322 Self::ValidateOnly { kind, names } => match kind {
5323 ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
5324 ValidateOnlyKind::RoleName => {
5325 write!(f, "DROP OWNED BY {}", names.join(", "))
5326 }
5327 ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
5328 ValidateOnlyKind::ExtensionAvailable => {
5329 write!(f, "CREATE EXTENSION {}", names.join(", "))
5330 }
5331 ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
5332 ValidateOnlyKind::CollationName => {
5333 write!(f, "DROP COLLATION {}", names.join(", "))
5334 }
5335 ValidateOnlyKind::TsConfigName => {
5336 write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
5337 }
5338 ValidateOnlyKind::EventTriggerName => {
5339 write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
5340 }
5341 ValidateOnlyKind::TablespaceName => {
5342 write!(f, "DROP TABLESPACE {}", names.join(", "))
5343 }
5344 ValidateOnlyKind::LargeObjectOid => {
5345 write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
5346 }
5347 ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
5348 ValidateOnlyKind::AggregateName => {
5349 write!(f, "ALTER AGGREGATE {}", names.join(", "))
5350 }
5351 ValidateOnlyKind::ConversionName => {
5352 write!(f, "DROP CONVERSION {}", names.join(", "))
5353 }
5354 ValidateOnlyKind::LanguageName => {
5355 write!(f, "DROP LANGUAGE {}", names.join(", "))
5356 }
5357 ValidateOnlyKind::ExtensionInstalled => {
5358 write!(f, "DROP EXTENSION {}", names.join(", "))
5359 }
5360 },
5361 Self::AlterSystem { parameter } => match parameter {
5362 Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
5363 None => f.write_str("ALTER SYSTEM RESET ALL"),
5364 },
5365 // v7.39 (round 547) — round-trips as PG writes it.
5366 Self::SetDbRoleSetting(st) => {
5367 match (&st.database, &st.role) {
5368 (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
5369 (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
5370 (None, None) => f.write_str("ALTER ROLE ALL")?,
5371 }
5372 if let (Some(d), Some(_)) = (&st.database, &st.role) {
5373 write!(f, " IN DATABASE {d}")?;
5374 }
5375 match (&st.param, &st.value) {
5376 (None, _) => f.write_str(" RESET ALL"),
5377 (Some(p), None) => write!(f, " RESET {p}"),
5378 (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
5379 }
5380 }
5381 Self::Maintain {
5382 kind,
5383 concurrently,
5384 target,
5385 } => {
5386 f.write_str(match kind {
5387 crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
5388 _ => "REINDEX ",
5389 })?;
5390 if *concurrently {
5391 f.write_str("CONCURRENTLY ")?;
5392 }
5393 if let Some(t) = target {
5394 f.write_str(t)?;
5395 }
5396 Ok(())
5397 }
5398 Self::DropDatabase { name, if_exists } => {
5399 f.write_str("DROP DATABASE ")?;
5400 if *if_exists {
5401 f.write_str("IF EXISTS ")?;
5402 }
5403 f.write_str(name)
5404 }
5405 Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
5406 Self::SetConstraints { names, deferred } => {
5407 f.write_str("SET CONSTRAINTS ")?;
5408 if names.is_empty() {
5409 f.write_str("ALL")?;
5410 } else {
5411 for (i, n) in names.iter().enumerate() {
5412 if i > 0 {
5413 f.write_str(", ")?;
5414 }
5415 f.write_str(n)?;
5416 }
5417 }
5418 f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
5419 }
5420 // v7.39 (round 277) — the source text is kept verbatim so
5421 // `pg_prepared_statements.statement` can report it the way
5422 // PG does (the whole PREPARE statement, not just the body).
5423 Self::Prepare { source, .. } => f.write_str(source),
5424 Self::Execute { name, args } => {
5425 write!(f, "EXECUTE {}", quote_ident(name))?;
5426 if !args.is_empty() {
5427 f.write_str("(")?;
5428 for (i, a) in args.iter().enumerate() {
5429 if i > 0 {
5430 f.write_str(", ")?;
5431 }
5432 write!(f, "{a}")?;
5433 }
5434 f.write_str(")")?;
5435 }
5436 Ok(())
5437 }
5438 Self::CreateStatistics {
5439 name,
5440 if_not_exists,
5441 kinds,
5442 columns,
5443 table,
5444 } => {
5445 f.write_str("CREATE STATISTICS ")?;
5446 if *if_not_exists {
5447 f.write_str("IF NOT EXISTS ")?;
5448 }
5449 write!(f, "{}", quote_ident(name))?;
5450 if !kinds.is_empty() {
5451 write!(f, " ({})", kinds.join(", "))?;
5452 }
5453 write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
5454 }
5455 Self::DropStatistics { name, if_exists } => {
5456 f.write_str("DROP STATISTICS ")?;
5457 if *if_exists {
5458 f.write_str("IF EXISTS ")?;
5459 }
5460 write!(f, "{}", quote_ident(name))
5461 }
5462 Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
5463 Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
5464 Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
5465 Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
5466 Self::DeclareCursor {
5467 name,
5468 scroll,
5469 hold,
5470 query,
5471 } => {
5472 write!(f, "DECLARE {} ", quote_ident(name))?;
5473 match scroll {
5474 Some(true) => f.write_str("SCROLL ")?,
5475 Some(false) => f.write_str("NO SCROLL ")?,
5476 None => {}
5477 }
5478 f.write_str("CURSOR ")?;
5479 if *hold {
5480 f.write_str("WITH HOLD ")?;
5481 }
5482 write!(f, "FOR {query}")
5483 }
5484 Self::FetchCursor { name, direction } => {
5485 write!(f, "FETCH {direction} FROM {}", quote_ident(name))
5486 }
5487 Self::MoveCursor { name, direction } => {
5488 write!(f, "MOVE {direction} FROM {}", quote_ident(name))
5489 }
5490 Self::CloseCursor { name } => match name {
5491 Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
5492 None => f.write_str("CLOSE ALL"),
5493 },
5494 Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
5495 Self::Notify { channel, payload } => {
5496 write!(f, "NOTIFY {}", quote_ident(channel))?;
5497 if let Some(p) = payload {
5498 write!(f, ", '{}'", p.replace('\'', "''"))?;
5499 }
5500 Ok(())
5501 }
5502 Self::Unlisten(ch) => match ch {
5503 Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
5504 None => f.write_str("UNLISTEN *"),
5505 },
5506 Self::CopyTo {
5507 table,
5508 columns,
5509 query,
5510 options,
5511 } => {
5512 if let Some(q) = query {
5513 write!(f, "COPY ({q})")?;
5514 } else {
5515 write!(f, "COPY {table}")?;
5516 if let Some(cols) = columns {
5517 write!(f, " ({})", cols.join(", "))?;
5518 }
5519 }
5520 write!(f, " TO STDOUT")?;
5521 let mut parts: Vec<String> = Vec::new();
5522 if options.format == CopyFormat::Csv {
5523 parts.push("FORMAT csv".to_string());
5524 }
5525 if options.header {
5526 parts.push("HEADER true".to_string());
5527 }
5528 if let Some(d) = options.delimiter {
5529 parts.push(alloc::format!("DELIMITER '{d}'"));
5530 }
5531 if let Some(n) = &options.null_str {
5532 parts.push(alloc::format!("NULL '{n}'"));
5533 }
5534 if let Some(q) = options.quote {
5535 parts.push(alloc::format!("QUOTE '{q}'"));
5536 }
5537 if !parts.is_empty() {
5538 write!(f, " WITH ({})", parts.join(", "))?;
5539 }
5540 Ok(())
5541 }
5542 Self::CopyFromFile {
5543 table,
5544 columns,
5545 path,
5546 options,
5547 } => {
5548 write!(f, "COPY {table}")?;
5549 if let Some(cols) = columns {
5550 write!(f, " ({})", cols.join(", "))?;
5551 }
5552 write!(f, " FROM '{path}'")?;
5553 let mut parts: Vec<String> = Vec::new();
5554 if options.format == CopyFormat::Csv {
5555 parts.push("FORMAT csv".to_string());
5556 }
5557 if options.header {
5558 parts.push("HEADER true".to_string());
5559 }
5560 if let Some(d) = options.delimiter {
5561 parts.push(alloc::format!("DELIMITER '{d}'"));
5562 }
5563 if let Some(n) = &options.null_str {
5564 parts.push(alloc::format!("NULL '{n}'"));
5565 }
5566 if let Some(q) = options.quote {
5567 parts.push(alloc::format!("QUOTE '{q}'"));
5568 }
5569 if !parts.is_empty() {
5570 write!(f, " WITH ({})", parts.join(", "))?;
5571 }
5572 Ok(())
5573 }
5574 Self::CopyToFile {
5575 table,
5576 columns,
5577 query,
5578 path,
5579 options,
5580 } => {
5581 if let Some(q) = query {
5582 write!(f, "COPY ({q})")?;
5583 } else {
5584 write!(f, "COPY {table}")?;
5585 if let Some(cols) = columns {
5586 write!(f, " ({})", cols.join(", "))?;
5587 }
5588 }
5589 write!(f, " TO '{path}'")?;
5590 let mut parts: Vec<String> = Vec::new();
5591 if options.format == CopyFormat::Csv {
5592 parts.push("FORMAT csv".to_string());
5593 }
5594 if options.header {
5595 parts.push("HEADER true".to_string());
5596 }
5597 if let Some(d) = options.delimiter {
5598 parts.push(alloc::format!("DELIMITER '{d}'"));
5599 }
5600 if let Some(n) = &options.null_str {
5601 parts.push(alloc::format!("NULL '{n}'"));
5602 }
5603 if let Some(q) = options.quote {
5604 parts.push(alloc::format!("QUOTE '{q}'"));
5605 }
5606 if !parts.is_empty() {
5607 write!(f, " WITH ({})", parts.join(", "))?;
5608 }
5609 Ok(())
5610 }
5611 Self::AlterDomain { name, action } => {
5612 write!(f, "ALTER DOMAIN {name} ")?;
5613 match action {
5614 AlterDomainAction::AddConstraint { name: cn, check } => match cn {
5615 Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
5616 None => write!(f, "ADD CHECK ({check})"),
5617 },
5618 AlterDomainAction::DropConstraint {
5619 name: cn,
5620 if_exists,
5621 } => {
5622 if *if_exists {
5623 write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
5624 } else {
5625 write!(f, "DROP CONSTRAINT {cn}")
5626 }
5627 }
5628 AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
5629 AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
5630 AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
5631 AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
5632 AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
5633 }
5634 }
5635 Self::Truncate {
5636 tables,
5637 restart_identity,
5638 cascade,
5639 only,
5640 } => {
5641 f.write_str("TRUNCATE TABLE ")?;
5642 if *only {
5643 f.write_str("ONLY ")?;
5644 }
5645 for (i, t) in tables.iter().enumerate() {
5646 if i > 0 {
5647 f.write_str(", ")?;
5648 }
5649 f.write_str(t)?;
5650 }
5651 if *restart_identity {
5652 f.write_str(" RESTART IDENTITY")?;
5653 }
5654 if *cascade {
5655 f.write_str(" CASCADE")?;
5656 }
5657 Ok(())
5658 }
5659 Self::DropTable { names, if_exists } => {
5660 f.write_str("DROP TABLE ")?;
5661 if *if_exists {
5662 f.write_str("IF EXISTS ")?;
5663 }
5664 for (i, n) in names.iter().enumerate() {
5665 if i > 0 {
5666 f.write_str(", ")?;
5667 }
5668 write!(f, "{}", quote_ident(n))?;
5669 }
5670 Ok(())
5671 }
5672 Self::DropIndex { name, if_exists } => {
5673 f.write_str("DROP INDEX ")?;
5674 if *if_exists {
5675 f.write_str("IF EXISTS ")?;
5676 }
5677 write!(f, "{}", quote_ident(name))
5678 }
5679 Self::Select(s) => s.fmt(f),
5680 Self::CreateTable(s) => s.fmt(f),
5681 Self::CreateIndex(s) => s.fmt(f),
5682 Self::Insert(s) => s.fmt(f),
5683 Self::Update(s) => s.fmt(f),
5684 Self::Delete(s) => s.fmt(f),
5685 Self::Merge(s) => s.fmt(f),
5686 Self::Vacuum { table, analyze } => {
5687 f.write_str("VACUUM")?;
5688 if *analyze {
5689 f.write_str(" ANALYZE")?;
5690 }
5691 if let Some(t) = table {
5692 write!(f, " {}", quote_ident(t))?;
5693 }
5694 Ok(())
5695 }
5696 Self::Begin(None) => f.write_str("BEGIN"),
5697 Self::Begin(Some(level)) => write!(f, "BEGIN ISOLATION LEVEL {level}"),
5698 Self::Commit => f.write_str("COMMIT"),
5699 Self::Rollback => f.write_str("ROLLBACK"),
5700 Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
5701 Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
5702 Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
5703 Self::ShowTables => f.write_str("SHOW TABLES"),
5704 Self::ShowDatabases => f.write_str("SHOW DATABASES"),
5705 Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
5706 Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
5707 Self::ShowStatus => f.write_str("SHOW STATUS"),
5708 Self::ShowVariables => f.write_str("SHOW VARIABLES"),
5709 Self::ShowVariablesLike(p) => {
5710 write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
5711 }
5712 Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
5713 Self::Discard(t) => write!(f, "DISCARD {t}"),
5714 Self::Kill { query_only, id } => {
5715 if *query_only {
5716 write!(f, "KILL QUERY {id}")
5717 } else {
5718 write!(f, "KILL CONNECTION {id}")
5719 }
5720 }
5721 Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
5722 Self::CreateUser(s) => write!(
5723 f,
5724 "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
5725 quote_ident(&s.name),
5726 s.role
5727 ),
5728 Self::DropUser { name, if_exists } => {
5729 let ie = if *if_exists { "IF EXISTS " } else { "" };
5730 write!(f, "DROP USER {ie}{}", quote_ident(name))
5731 }
5732 Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
5733 Self::SetRole(None) => f.write_str("RESET ROLE"),
5734 Self::Grant(g) => write!(f, "{}", g.render(true)),
5735 Self::Revoke(g) => write!(f, "{}", g.render(false)),
5736 Self::CreatePolicy(s) => {
5737 write!(
5738 f,
5739 "CREATE POLICY {} ON {}",
5740 quote_ident(&s.name),
5741 quote_ident(&s.table)
5742 )?;
5743 if !s.permissive {
5744 f.write_str(" AS RESTRICTIVE")?;
5745 }
5746 if !matches!(s.cmd, PolicyCmd::All) {
5747 let w = match s.cmd {
5748 PolicyCmd::Select => "SELECT",
5749 PolicyCmd::Insert => "INSERT",
5750 PolicyCmd::Update => "UPDATE",
5751 PolicyCmd::Delete => "DELETE",
5752 PolicyCmd::All => unreachable!(),
5753 };
5754 write!(f, " FOR {w}")?;
5755 }
5756 if !s.roles.is_empty() {
5757 write!(f, " TO {}", s.roles.join(", "))?;
5758 }
5759 if let Some(u) = &s.using {
5760 write!(f, " USING ({u})")?;
5761 }
5762 if let Some(c) = &s.with_check {
5763 write!(f, " WITH CHECK ({c})")?;
5764 }
5765 Ok(())
5766 }
5767 Self::AlterPolicy(s) => {
5768 write!(
5769 f,
5770 "ALTER POLICY {} ON {}",
5771 quote_ident(&s.name),
5772 quote_ident(&s.table)
5773 )?;
5774 if let Some(nn) = &s.rename_to {
5775 return write!(f, " RENAME TO {}", quote_ident(nn));
5776 }
5777 if let Some(roles) = &s.roles {
5778 write!(f, " TO {}", roles.join(", "))?;
5779 }
5780 if let Some(u) = &s.using {
5781 write!(f, " USING ({u})")?;
5782 }
5783 if let Some(c) = &s.with_check {
5784 write!(f, " WITH CHECK ({c})")?;
5785 }
5786 Ok(())
5787 }
5788 Self::DropPolicy(s) => {
5789 f.write_str("DROP POLICY ")?;
5790 if s.if_exists {
5791 f.write_str("IF EXISTS ")?;
5792 }
5793 write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
5794 }
5795 Self::ShowUsers => f.write_str("SHOW USERS"),
5796 Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
5797 Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
5798 Self::CreateSubscription(s) => {
5799 write!(
5800 f,
5801 "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
5802 quote_ident(&s.name),
5803 s.conn_str.replace('\'', "''")
5804 )?;
5805 for (i, p) in s.publications.iter().enumerate() {
5806 if i > 0 {
5807 f.write_str(", ")?;
5808 }
5809 write!(f, "{}", quote_ident(p))?;
5810 }
5811 Ok(())
5812 }
5813 Self::DropSubscription { name, if_exists } => {
5814 let opt = if *if_exists { "IF EXISTS " } else { "" };
5815 write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
5816 }
5817 Self::WaitForWalPosition { pos, timeout_ms } => {
5818 write!(f, "WAIT FOR WAL POSITION {pos}")?;
5819 if let Some(ms) = timeout_ms {
5820 write!(f, " WITH TIMEOUT {ms}")?;
5821 }
5822 Ok(())
5823 }
5824 Self::Analyze(None) => f.write_str("ANALYZE"),
5825 Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
5826 Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
5827 Self::Explain(e) => {
5828 if e.suggest {
5829 write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
5830 } else if e.analyze {
5831 write!(f, "EXPLAIN ANALYZE {}", e.inner)
5832 } else {
5833 write!(f, "EXPLAIN {}", e.inner)
5834 }
5835 }
5836 Self::AlterIndex(a) => {
5837 write!(f, "ALTER INDEX ")?;
5838 match &a.target {
5839 // Parameters are consumed, not stored; the shortest
5840 // faithful spelling.
5841 AlterIndexTarget::StorageParams => {
5842 write!(f, "{} SET ()", quote_ident(&a.name))
5843 }
5844 AlterIndexTarget::Rebuild { encoding } => {
5845 write!(f, "{} REBUILD", quote_ident(&a.name))?;
5846 if let Some(enc) = encoding {
5847 write!(f, " WITH (encoding = {enc})")?;
5848 }
5849 Ok(())
5850 }
5851 AlterIndexTarget::Rename { new, if_exists } => {
5852 if *if_exists {
5853 f.write_str("IF EXISTS ")?;
5854 }
5855 write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
5856 }
5857 }
5858 }
5859 Self::AlterTable(a) => {
5860 write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
5861 for (i, t) in a.targets.iter().enumerate() {
5862 if i > 0 {
5863 f.write_str(", ")?;
5864 }
5865 fmt_alter_target(f, t)?;
5866 }
5867 Ok(())
5868 }
5869 Self::CreatePublication(p) => {
5870 write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
5871 match &p.scope {
5872 PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
5873 PublicationScope::ForTables(ts) => {
5874 f.write_str(" FOR TABLE ")?;
5875 for (i, t) in ts.iter().enumerate() {
5876 if i > 0 {
5877 f.write_str(", ")?;
5878 }
5879 write!(f, "{}", quote_ident(t))?;
5880 }
5881 Ok(())
5882 }
5883 PublicationScope::TablesInSchema(schema) => {
5884 write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
5885 Ok(())
5886 }
5887 PublicationScope::AllTablesExcept(ts) => {
5888 f.write_str(" FOR ALL TABLES EXCEPT ")?;
5889 for (i, t) in ts.iter().enumerate() {
5890 if i > 0 {
5891 f.write_str(", ")?;
5892 }
5893 write!(f, "{}", quote_ident(t))?;
5894 }
5895 Ok(())
5896 }
5897 }
5898 }
5899 Self::CreateExtension(name) => {
5900 write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
5901 }
5902 Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
5903 Self::DropPublication { name, if_exists } => {
5904 let opt = if *if_exists { "IF EXISTS " } else { "" };
5905 write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
5906 }
5907 Self::SetParameter { name, value, local } => {
5908 write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
5909 match value {
5910 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
5911 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
5912 SetValue::Default => f.write_str("DEFAULT"),
5913 }
5914 }
5915 Self::SetTransaction { isolation } => {
5916 write!(f, "SET TRANSACTION ISOLATION LEVEL ")?;
5917 let name = match isolation {
5918 IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
5919 IsolationLevel::ReadCommitted => "READ COMMITTED",
5920 IsolationLevel::RepeatableRead => "REPEATABLE READ",
5921 IsolationLevel::Serializable => "SERIALIZABLE",
5922 };
5923 f.write_str(name)
5924 }
5925 Self::ShowParameter(name) => write!(f, "SHOW {name}"),
5926 Self::SetUserVars(assigns, _) => {
5927 f.write_str("SET ")?;
5928 for (i, (name, value)) in assigns.iter().enumerate() {
5929 if i > 0 {
5930 f.write_str(", ")?;
5931 }
5932 write!(f, "@{name} = {value}")?;
5933 }
5934 Ok(())
5935 }
5936 Self::SetParameterList(pairs) => {
5937 f.write_str("SET ")?;
5938 for (i, (name, value)) in pairs.iter().enumerate() {
5939 if i > 0 {
5940 f.write_str(", ")?;
5941 }
5942 write!(f, "{name} = ")?;
5943 match value {
5944 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
5945 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
5946 SetValue::Default => f.write_str("DEFAULT")?,
5947 }
5948 }
5949 Ok(())
5950 }
5951 Self::ResetParameter(None) => f.write_str("RESET ALL"),
5952 Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
5953 Self::CreateFunction(s) => s.fmt(f),
5954 Self::CreateTrigger(s) => s.fmt(f),
5955 Self::DropTrigger {
5956 name,
5957 table,
5958 if_exists,
5959 } => {
5960 f.write_str("DROP TRIGGER ")?;
5961 if *if_exists {
5962 f.write_str("IF EXISTS ")?;
5963 }
5964 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
5965 }
5966 Self::DropFunction {
5967 name,
5968 args,
5969 if_exists,
5970 } => {
5971 f.write_str("DROP FUNCTION ")?;
5972 if *if_exists {
5973 f.write_str("IF EXISTS ")?;
5974 }
5975 write!(f, "{}", quote_ident(name))?;
5976 if let Some(a) = args {
5977 write!(f, "({})", a.join(", "))?;
5978 }
5979 Ok(())
5980 }
5981 Self::CreateSequence(s) => s.fmt(f),
5982 Self::AlterSequence(s) => s.fmt(f),
5983 Self::DropSequence { names, if_exists } => {
5984 f.write_str("DROP SEQUENCE ")?;
5985 if *if_exists {
5986 f.write_str("IF EXISTS ")?;
5987 }
5988 for (i, n) in names.iter().enumerate() {
5989 if i > 0 {
5990 f.write_str(", ")?;
5991 }
5992 write!(f, "{}", quote_ident(n))?;
5993 }
5994 Ok(())
5995 }
5996 Self::CreateView(v) => v.fmt(f),
5997 Self::DropView { names, if_exists } => {
5998 f.write_str("DROP VIEW ")?;
5999 if *if_exists {
6000 f.write_str("IF EXISTS ")?;
6001 }
6002 for (i, n) in names.iter().enumerate() {
6003 if i > 0 {
6004 f.write_str(", ")?;
6005 }
6006 write!(f, "{}", quote_ident(n))?;
6007 }
6008 Ok(())
6009 }
6010 Self::CreateMaterializedView(v) => v.fmt(f),
6011 Self::RefreshMaterializedView { name, with_data } => {
6012 write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6013 if !*with_data {
6014 f.write_str(" WITH NO DATA")?;
6015 }
6016 Ok(())
6017 }
6018 Self::DropMaterializedView { names, if_exists } => {
6019 f.write_str("DROP MATERIALIZED VIEW ")?;
6020 if *if_exists {
6021 f.write_str("IF EXISTS ")?;
6022 }
6023 for (i, n) in names.iter().enumerate() {
6024 if i > 0 {
6025 f.write_str(", ")?;
6026 }
6027 write!(f, "{}", quote_ident(n))?;
6028 }
6029 Ok(())
6030 }
6031 Self::CreateType(t) => t.fmt(f),
6032 Self::CommentOn {
6033 kind,
6034 name,
6035 comment,
6036 } => {
6037 let body = match comment {
6038 Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6039 None => "NULL".into(),
6040 };
6041 write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6042 }
6043 Self::AlterTypeRenameValue {
6044 type_name,
6045 old,
6046 new,
6047 } => write!(
6048 f,
6049 "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6050 quote_ident(type_name),
6051 old.replace('\'', "''"),
6052 new.replace('\'', "''")
6053 ),
6054 Self::AlterTypeAddValue {
6055 type_name,
6056 label,
6057 if_not_exists,
6058 position,
6059 } => {
6060 write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6061 if *if_not_exists {
6062 write!(f, "IF NOT EXISTS ")?;
6063 }
6064 write!(f, "'{label}'")?;
6065 if let Some((is_before, anchor)) = position {
6066 write!(
6067 f,
6068 " {} '{anchor}'",
6069 if *is_before { "BEFORE" } else { "AFTER" }
6070 )?;
6071 }
6072 Ok(())
6073 }
6074 Self::DropType { names, if_exists } => {
6075 f.write_str("DROP TYPE ")?;
6076 if *if_exists {
6077 f.write_str("IF EXISTS ")?;
6078 }
6079 for (i, n) in names.iter().enumerate() {
6080 if i > 0 {
6081 f.write_str(", ")?;
6082 }
6083 write!(f, "{}", quote_ident(n))?;
6084 }
6085 Ok(())
6086 }
6087 Self::CreateDomain(d) => d.fmt(f),
6088 Self::DropDomain { names, if_exists } => {
6089 f.write_str("DROP DOMAIN ")?;
6090 if *if_exists {
6091 f.write_str("IF EXISTS ")?;
6092 }
6093 for (i, n) in names.iter().enumerate() {
6094 if i > 0 {
6095 f.write_str(", ")?;
6096 }
6097 write!(f, "{}", quote_ident(n))?;
6098 }
6099 Ok(())
6100 }
6101 Self::CreateSchema {
6102 name,
6103 if_not_exists,
6104 } => {
6105 f.write_str("CREATE SCHEMA ")?;
6106 if *if_not_exists {
6107 f.write_str("IF NOT EXISTS ")?;
6108 }
6109 write!(f, "{}", quote_ident(name))
6110 }
6111 Self::DropSchema { names, if_exists } => {
6112 f.write_str("DROP SCHEMA ")?;
6113 if *if_exists {
6114 f.write_str("IF EXISTS ")?;
6115 }
6116 for (i, n) in names.iter().enumerate() {
6117 if i > 0 {
6118 f.write_str(", ")?;
6119 }
6120 write!(f, "{}", quote_ident(n))?;
6121 }
6122 Ok(())
6123 }
6124 Self::CreateRule(r) => {
6125 f.write_str("CREATE ")?;
6126 if r.or_replace {
6127 f.write_str("OR REPLACE ")?;
6128 }
6129 write!(
6130 f,
6131 "RULE {} AS ON {} TO {}",
6132 quote_ident(&r.name),
6133 r.event,
6134 quote_ident(&r.table)
6135 )?;
6136 if let Some(w) = &r.when_condition {
6137 write!(f, " WHERE {w}")?;
6138 }
6139 f.write_str(if r.instead {
6140 " DO INSTEAD "
6141 } else {
6142 " DO ALSO "
6143 })?;
6144 if r.commands.is_empty() {
6145 f.write_str("NOTHING")?;
6146 } else if r.commands.len() == 1 {
6147 write!(f, "{}", r.commands[0])?;
6148 } else {
6149 f.write_str("(")?;
6150 for (i, c) in r.commands.iter().enumerate() {
6151 if i > 0 {
6152 f.write_str("; ")?;
6153 }
6154 write!(f, "{c}")?;
6155 }
6156 f.write_str(")")?;
6157 }
6158 Ok(())
6159 }
6160 Self::DropRule {
6161 name,
6162 table,
6163 if_exists,
6164 } => {
6165 f.write_str("DROP RULE ")?;
6166 if *if_exists {
6167 f.write_str("IF EXISTS ")?;
6168 }
6169 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6170 }
6171 }
6172 }
6173}
6174
6175impl fmt::Display for CreateDomainStatement {
6176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6177 write!(
6178 f,
6179 "CREATE DOMAIN {} AS {}",
6180 quote_ident(&self.name),
6181 self.base_type
6182 )?;
6183 if let Some(d) = &self.default {
6184 write!(f, " DEFAULT {d}")?;
6185 }
6186 if self.not_null {
6187 f.write_str(" NOT NULL")?;
6188 }
6189 for c in &self.checks {
6190 write!(f, " CHECK ({c})")?;
6191 }
6192 Ok(())
6193 }
6194}
6195
6196impl fmt::Display for CreateTypeStatement {
6197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6198 write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
6199 match &self.kind {
6200 TypeKind::Enum { labels } => {
6201 f.write_str("ENUM (")?;
6202 for (i, l) in labels.iter().enumerate() {
6203 if i > 0 {
6204 f.write_str(", ")?;
6205 }
6206 write!(f, "'{}'", l.replace('\'', "''"))?;
6207 }
6208 f.write_str(")")
6209 }
6210 TypeKind::Composite { fields, .. } => {
6211 f.write_str("(")?;
6212 for (i, (n, t)) in fields.iter().enumerate() {
6213 if i > 0 {
6214 f.write_str(", ")?;
6215 }
6216 write!(f, "{} {}", quote_ident(n), t)?;
6217 }
6218 f.write_str(")")
6219 }
6220 }
6221 }
6222}
6223
6224impl fmt::Display for CreateMaterializedViewStatement {
6225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6226 f.write_str("CREATE MATERIALIZED VIEW ")?;
6227 if self.if_not_exists {
6228 f.write_str("IF NOT EXISTS ")?;
6229 }
6230 write!(f, "{}", quote_ident(&self.name))?;
6231 if !self.columns.is_empty() {
6232 f.write_str(" (")?;
6233 for (i, c) in self.columns.iter().enumerate() {
6234 if i > 0 {
6235 f.write_str(", ")?;
6236 }
6237 write!(f, "{}", quote_ident(c))?;
6238 }
6239 f.write_str(")")?;
6240 }
6241 write!(f, " AS {}", self.body)?;
6242 if !self.with_data {
6243 f.write_str(" WITH NO DATA")?;
6244 }
6245 Ok(())
6246 }
6247}
6248
6249impl fmt::Display for CreateViewStatement {
6250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6251 f.write_str("CREATE ")?;
6252 if self.or_replace {
6253 f.write_str("OR REPLACE ")?;
6254 }
6255 if self.temporary {
6256 f.write_str("TEMPORARY ")?;
6257 }
6258 f.write_str("VIEW ")?;
6259 if self.if_not_exists {
6260 f.write_str("IF NOT EXISTS ")?;
6261 }
6262 write!(f, "{}", quote_ident(&self.name))?;
6263 if !self.columns.is_empty() {
6264 f.write_str(" (")?;
6265 for (i, c) in self.columns.iter().enumerate() {
6266 if i > 0 {
6267 f.write_str(", ")?;
6268 }
6269 write!(f, "{}", quote_ident(c))?;
6270 }
6271 f.write_str(")")?;
6272 }
6273 write!(f, " AS {}", self.body)?;
6274 match self.check_option {
6275 Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
6276 Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
6277 None => Ok(()),
6278 }
6279 }
6280}
6281
6282impl fmt::Display for CreateSequenceStatement {
6283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6284 f.write_str("CREATE ")?;
6285 if self.temporary {
6286 f.write_str("TEMPORARY ")?;
6287 }
6288 f.write_str("SEQUENCE ")?;
6289 if self.if_not_exists {
6290 f.write_str("IF NOT EXISTS ")?;
6291 }
6292 write!(f, "{}", quote_ident(&self.name))?;
6293 if let Some(dt) = self.data_type {
6294 write!(f, " AS {dt}")?;
6295 }
6296 write_sequence_options(f, &self.options)
6297 }
6298}
6299
6300impl fmt::Display for AlterSequenceStatement {
6301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6302 f.write_str("ALTER SEQUENCE ")?;
6303 if self.if_exists {
6304 f.write_str("IF EXISTS ")?;
6305 }
6306 write!(f, "{}", quote_ident(&self.name))?;
6307 write_sequence_options(f, &self.options)
6308 }
6309}
6310
6311impl fmt::Display for SequenceDataType {
6312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6313 f.write_str(match self {
6314 Self::SmallInt => "smallint",
6315 Self::Int => "integer",
6316 Self::BigInt => "bigint",
6317 })
6318 }
6319}
6320
6321fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
6322 if let Some(n) = o.increment {
6323 write!(f, " INCREMENT BY {n}")?;
6324 }
6325 match o.min_value {
6326 Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
6327 Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
6328 None => {}
6329 }
6330 match o.max_value {
6331 Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
6332 Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
6333 None => {}
6334 }
6335 if let Some(n) = o.start {
6336 write!(f, " START WITH {n}")?;
6337 }
6338 match o.restart {
6339 Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
6340 Some(None) => f.write_str(" RESTART")?,
6341 None => {}
6342 }
6343 if let Some(n) = o.cache {
6344 write!(f, " CACHE {n}")?;
6345 }
6346 match o.cycle {
6347 Some(true) => f.write_str(" CYCLE")?,
6348 Some(false) => f.write_str(" NO CYCLE")?,
6349 None => {}
6350 }
6351 if let Some(ob) = &o.owned_by {
6352 match ob {
6353 SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
6354 SequenceOwnedBy::Column { table, column } => {
6355 write!(
6356 f,
6357 " OWNED BY {}.{}",
6358 quote_ident(table),
6359 quote_ident(column)
6360 )?;
6361 }
6362 }
6363 }
6364 Ok(())
6365}
6366
6367impl fmt::Display for CreateFunctionStatement {
6368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6369 f.write_str("CREATE ")?;
6370 if self.or_replace {
6371 f.write_str("OR REPLACE ")?;
6372 }
6373 write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
6374 for (i, arg) in self.args.iter().enumerate() {
6375 if i > 0 {
6376 f.write_str(", ")?;
6377 }
6378 match arg.mode {
6379 FunctionArgMode::In => {}
6380 FunctionArgMode::Out => f.write_str("OUT ")?,
6381 FunctionArgMode::InOut => f.write_str("INOUT ")?,
6382 }
6383 if let Some(name) = &arg.name {
6384 write!(f, "{} ", quote_ident(name))?;
6385 }
6386 match &arg.ty {
6387 FunctionArgType::Typed(t) => write!(f, "{t}")?,
6388 FunctionArgType::Raw(s) => f.write_str(s)?,
6389 }
6390 }
6391 f.write_str(") RETURNS ")?;
6392 match &self.returns {
6393 FunctionReturn::Trigger => f.write_str("TRIGGER")?,
6394 FunctionReturn::Void => f.write_str("VOID")?,
6395 FunctionReturn::Type(t) => write!(f, "{t}")?,
6396 FunctionReturn::Other(s) => f.write_str(s)?,
6397 }
6398 write!(f, " LANGUAGE {} AS $$", self.language)?;
6399 match &self.body {
6400 FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
6401 FunctionBody::Raw(s) => f.write_str(s)?,
6402 }
6403 f.write_str("$$")
6404 }
6405}
6406
6407impl fmt::Display for PlPgSqlBlock {
6408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6409 if !self.declarations.is_empty() {
6410 f.write_str("DECLARE\n")?;
6411 for d in &self.declarations {
6412 write!(f, " {} ", quote_ident(&d.name))?;
6413 match &d.ty {
6414 FunctionArgType::Typed(t) => write!(f, "{t}")?,
6415 FunctionArgType::Raw(s) => f.write_str(s)?,
6416 }
6417 if let Some(e) = &d.default {
6418 write!(f, " := {e}")?;
6419 }
6420 f.write_str(";\n")?;
6421 }
6422 }
6423 f.write_str("BEGIN\n")?;
6424 for stmt in &self.statements {
6425 writeln!(f, " {stmt};")?;
6426 }
6427 // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
6428 // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
6429 // parsed block through it — so every exception handler a function
6430 // declared was thrown away AT STORE TIME. The block executed fine while
6431 // it was still an AST (a DO block never round-trips through text), which
6432 // is why only functions and triggers lost theirs.
6433 if !self.exception_handlers.is_empty() {
6434 f.write_str("EXCEPTION\n")?;
6435 for h in &self.exception_handlers {
6436 writeln!(f, " WHEN {} THEN", h.conditions.join(" OR "))?;
6437 for stmt in &h.body {
6438 writeln!(f, " {stmt};")?;
6439 }
6440 }
6441 }
6442 f.write_str("END")
6443 }
6444}
6445
6446impl fmt::Display for PlPgSqlStmt {
6447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6448 match self {
6449 Self::Assign { target, value } => write!(f, "{target} := {value}"),
6450 Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
6451 Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
6452 Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
6453 Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
6454 Self::Return(t) => match t {
6455 ReturnTarget::New => f.write_str("RETURN NEW"),
6456 ReturnTarget::Old => f.write_str("RETURN OLD"),
6457 ReturnTarget::Null => f.write_str("RETURN NULL"),
6458 ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
6459 },
6460 Self::If {
6461 branches,
6462 else_branch,
6463 } => {
6464 for (i, (cond, body)) in branches.iter().enumerate() {
6465 if i == 0 {
6466 write!(f, "IF {cond} THEN ")?;
6467 } else {
6468 write!(f, " ELSIF {cond} THEN ")?;
6469 }
6470 for (j, s) in body.iter().enumerate() {
6471 if j > 0 {
6472 f.write_str("; ")?;
6473 }
6474 write!(f, "{s}")?;
6475 }
6476 }
6477 if !else_branch.is_empty() {
6478 f.write_str(" ELSE ")?;
6479 for (j, s) in else_branch.iter().enumerate() {
6480 if j > 0 {
6481 f.write_str("; ")?;
6482 }
6483 write!(f, "{s}")?;
6484 }
6485 }
6486 f.write_str(" END IF")
6487 }
6488 Self::Raise {
6489 level,
6490 message,
6491 args,
6492 } => {
6493 let lvl = match level {
6494 RaiseLevel::Notice => "NOTICE",
6495 RaiseLevel::Warning => "WARNING",
6496 RaiseLevel::Info => "INFO",
6497 RaiseLevel::Log => "LOG",
6498 RaiseLevel::Debug => "DEBUG",
6499 RaiseLevel::Exception => "EXCEPTION",
6500 };
6501 write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
6502 for a in args {
6503 write!(f, ", {a}")?;
6504 }
6505 Ok(())
6506 }
6507 Self::EmbeddedSql(s) => write!(f, "{s}"),
6508 Self::Assert { condition, message } => {
6509 write!(f, "ASSERT {condition}")?;
6510 if let Some(m) = message {
6511 write!(f, ", {m}")?;
6512 }
6513 Ok(())
6514 }
6515 Self::While { condition, body } => {
6516 writeln!(f, "WHILE {condition} LOOP")?;
6517 for s in body {
6518 writeln!(f, " {s};")?;
6519 }
6520 f.write_str("END LOOP")
6521 }
6522 Self::ForRange {
6523 var,
6524 start,
6525 end,
6526 reverse,
6527 body,
6528 } => {
6529 write!(f, "FOR {var} IN ")?;
6530 if *reverse {
6531 f.write_str("REVERSE ")?;
6532 }
6533 writeln!(f, "{start}..{end} LOOP")?;
6534 for s in body {
6535 writeln!(f, " {s};")?;
6536 }
6537 f.write_str("END LOOP")
6538 }
6539 Self::Loop { body } => {
6540 writeln!(f, "LOOP")?;
6541 for s in body {
6542 writeln!(f, " {s};")?;
6543 }
6544 f.write_str("END LOOP")
6545 }
6546 Self::Exit { when } => {
6547 f.write_str("EXIT")?;
6548 if let Some(c) = when {
6549 write!(f, " WHEN {c}")?;
6550 }
6551 Ok(())
6552 }
6553 Self::Continue { when } => {
6554 f.write_str("CONTINUE")?;
6555 if let Some(c) = when {
6556 write!(f, " WHEN {c}")?;
6557 }
6558 Ok(())
6559 }
6560 Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
6561 Self::ForQuery { var, query, body } => {
6562 writeln!(f, "FOR {var} IN ({query}) LOOP")?;
6563 for s in body {
6564 writeln!(f, " {s};")?;
6565 }
6566 f.write_str("END LOOP")
6567 }
6568 Self::ForExecute {
6569 var,
6570 sql_expr,
6571 body,
6572 } => {
6573 writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
6574 for s in body {
6575 writeln!(f, " {s};")?;
6576 }
6577 f.write_str("END LOOP")
6578 }
6579 }
6580 }
6581}
6582
6583impl fmt::Display for AssignTarget {
6584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6585 match self {
6586 Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
6587 Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
6588 Self::Local(n) => f.write_str(n),
6589 }
6590 }
6591}
6592
6593impl fmt::Display for CreateTriggerStatement {
6594 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6595 f.write_str("CREATE ")?;
6596 if self.or_replace {
6597 f.write_str("OR REPLACE ")?;
6598 }
6599 write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
6600 match self.timing {
6601 TriggerTiming::Before => f.write_str("BEFORE")?,
6602 TriggerTiming::After => f.write_str("AFTER")?,
6603 TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
6604 }
6605 for (i, e) in self.events.iter().enumerate() {
6606 if i == 0 {
6607 f.write_str(" ")?;
6608 } else {
6609 f.write_str(" OR ")?;
6610 }
6611 match e {
6612 TriggerEvent::Insert => f.write_str("INSERT")?,
6613 TriggerEvent::Update => {
6614 f.write_str("UPDATE")?;
6615 if !self.update_columns.is_empty() {
6616 f.write_str(" OF ")?;
6617 for (j, col) in self.update_columns.iter().enumerate() {
6618 if j > 0 {
6619 f.write_str(", ")?;
6620 }
6621 f.write_str("e_ident(col))?;
6622 }
6623 }
6624 }
6625 TriggerEvent::Delete => f.write_str("DELETE")?,
6626 TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
6627 }
6628 }
6629 write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
6630 match self.for_each {
6631 TriggerForEach::Row => f.write_str("ROW")?,
6632 TriggerForEach::Statement => f.write_str("STATEMENT")?,
6633 }
6634 write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
6635 }
6636}
6637
6638impl fmt::Display for CreateIndexStatement {
6639 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6640 if self.is_unique {
6641 f.write_str("CREATE UNIQUE INDEX ")?;
6642 } else {
6643 f.write_str("CREATE INDEX ")?;
6644 }
6645 if self.if_not_exists {
6646 f.write_str("IF NOT EXISTS ")?;
6647 }
6648 write!(
6649 f,
6650 "{} ON {} ",
6651 quote_ident(&self.name),
6652 quote_ident(&self.table)
6653 )?;
6654 match self.method {
6655 IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
6656 IndexMethod::Brin => f.write_str("USING brin ")?,
6657 IndexMethod::Gin => f.write_str("USING gin ")?,
6658 IndexMethod::BTree => {}
6659 }
6660 if let Some(expr) = &self.expression {
6661 write!(f, "({})", expr)?;
6662 } else if self.extra_columns.is_empty() {
6663 // v7.15.0 — preserve operator class on round-trip
6664 // (`(col opclass)`) so WAL replay reconstructs the
6665 // engine-routing intent (e.g. `gin_trgm_ops` →
6666 // trigram-GIN build path).
6667 if let Some(op) = &self.opclass {
6668 write!(f, "({} {})", quote_ident(&self.column), op)?;
6669 } else {
6670 write!(f, "({})", quote_ident(&self.column))?;
6671 }
6672 } else {
6673 // v7.9.14 — multi-column key. Emit each column quoted
6674 // so the round-tripped form re-parses to identical AST.
6675 f.write_str("(")?;
6676 write!(f, "{}", quote_ident(&self.column))?;
6677 for c in &self.extra_columns {
6678 write!(f, ", {}", quote_ident(c))?;
6679 }
6680 f.write_str(")")?;
6681 }
6682 if !self.included_columns.is_empty() {
6683 f.write_str(" INCLUDE (")?;
6684 for (i, c) in self.included_columns.iter().enumerate() {
6685 if i > 0 {
6686 f.write_str(", ")?;
6687 }
6688 write!(f, "{}", quote_ident(c))?;
6689 }
6690 f.write_str(")")?;
6691 }
6692 if let Some(pred) = &self.partial_predicate {
6693 write!(f, " WHERE {}", pred)?;
6694 }
6695 Ok(())
6696 }
6697}
6698
6699impl fmt::Display for CreateTableStatement {
6700 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6701 f.write_str("CREATE TABLE ")?;
6702 if self.if_not_exists {
6703 f.write_str("IF NOT EXISTS ")?;
6704 }
6705 write!(f, "{}", quote_ident(&self.name))?;
6706 // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
6707 // no column list and no constraints; the table inherits its
6708 // columns from the parent at engine-DDL time.
6709 if let Some(spec) = &self.partition_of {
6710 write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
6711 return match &spec.bounds {
6712 PartitionOfBoundsAst::Range { lower, upper } => {
6713 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
6714 }
6715 PartitionOfBoundsAst::List { values } => {
6716 f.write_str("FOR VALUES IN (")?;
6717 for (i, v) in values.iter().enumerate() {
6718 if i > 0 {
6719 f.write_str(", ")?;
6720 }
6721 write!(f, "{}", v)?;
6722 }
6723 f.write_str(")")
6724 }
6725 PartitionOfBoundsAst::Hash { modulus, remainder } => {
6726 write!(
6727 f,
6728 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
6729 modulus, remainder
6730 )
6731 }
6732 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
6733 };
6734 }
6735 f.write_str(" (")?;
6736 for (i, col) in self.columns.iter().enumerate() {
6737 if i > 0 {
6738 f.write_str(", ")?;
6739 }
6740 write!(f, "{col}")?;
6741 }
6742 // v7.6.0 — render FK constraints in table-level form, after
6743 // the column list. WAL replay round-trips through Display, so
6744 // every FK must serialise here for replay to reconstruct the
6745 // schema bit-for-bit.
6746 for fk in &self.foreign_keys {
6747 f.write_str(", ")?;
6748 write!(f, "{fk}")?;
6749 }
6750 // v7.13.0 — render table-level constraints (PRIMARY KEY /
6751 // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
6752 // column-level UNIQUE / CHECK get lifted to this list at
6753 // parse time, so emitting only here avoids double-counting.
6754 for tc in &self.table_constraints {
6755 f.write_str(", ")?;
6756 write!(f, "{tc}")?;
6757 }
6758 f.write_str(")")?;
6759 // v7.37.6-B — partition-parent suffix renders after the
6760 // closing column-list paren, before the optional MySQL
6761 // table-options tail (which Display doesn't currently emit).
6762 if let Some(spec) = &self.partition_by {
6763 f.write_str(" PARTITION BY ")?;
6764 match spec.kind {
6765 PartitionKindAst::Range => f.write_str("RANGE ")?,
6766 PartitionKindAst::List => f.write_str("LIST ")?,
6767 PartitionKindAst::Hash => f.write_str("HASH ")?,
6768 }
6769 f.write_str("(")?;
6770 for (i, col) in spec.key_columns.iter().enumerate() {
6771 if i > 0 {
6772 f.write_str(", ")?;
6773 }
6774 f.write_str("e_ident(col))?;
6775 }
6776 f.write_str(")")?;
6777 }
6778 Ok(())
6779 }
6780}
6781
6782fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
6783 match t {
6784 AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
6785 AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
6786 write!(f, "REPLICA IDENTITY USING INDEX {index}")
6787 }
6788 AlterTableTarget::Inherit { parent, detach } => {
6789 if *detach {
6790 write!(f, "NO INHERIT {parent}")
6791 } else {
6792 write!(f, "INHERIT {parent}")
6793 }
6794 }
6795 AlterTableTarget::SetHotTierBytes(n) => {
6796 write!(f, "SET hot_tier_bytes = {n}")
6797 }
6798 AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
6799 AlterTableTarget::DropForeignKey { name, if_exists } => {
6800 f.write_str("DROP CONSTRAINT ")?;
6801 if *if_exists {
6802 f.write_str("IF EXISTS ")?;
6803 }
6804 write!(f, "{}", quote_ident(name))
6805 }
6806 AlterTableTarget::DropIndex { name, if_exists } => {
6807 f.write_str("DROP INDEX ")?;
6808 if *if_exists {
6809 f.write_str("IF EXISTS ")?;
6810 }
6811 write!(f, "{}", quote_ident(name))
6812 }
6813 AlterTableTarget::AddColumn {
6814 column,
6815 if_not_exists,
6816 } => {
6817 f.write_str("ADD COLUMN ")?;
6818 if *if_not_exists {
6819 f.write_str("IF NOT EXISTS ")?;
6820 }
6821 write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
6822 if !column.nullable {
6823 f.write_str(" NOT NULL")?;
6824 }
6825 if let Some(d) = &column.default {
6826 write!(f, " DEFAULT {d}")?;
6827 }
6828 if column.auto_increment {
6829 f.write_str(" AUTO_INCREMENT")?;
6830 }
6831 if column.is_primary_key {
6832 f.write_str(" PRIMARY KEY")?;
6833 }
6834 Ok(())
6835 }
6836 AlterTableTarget::AlterColumnType {
6837 column,
6838 new_type,
6839 using,
6840 collation,
6841 } => {
6842 write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
6843 if let Some((_, name)) = collation {
6844 write!(f, " COLLATE {}", quote_ident(name))?;
6845 }
6846 if let Some(u) = using {
6847 write!(f, " USING {u}")?;
6848 }
6849 Ok(())
6850 }
6851 AlterTableTarget::DropColumn {
6852 column,
6853 if_exists,
6854 cascade,
6855 } => {
6856 f.write_str("DROP COLUMN ")?;
6857 if *if_exists {
6858 f.write_str("IF EXISTS ")?;
6859 }
6860 write!(f, "{}", quote_ident(column))?;
6861 if *cascade {
6862 f.write_str(" CASCADE")?;
6863 }
6864 Ok(())
6865 }
6866 AlterTableTarget::AddTableConstraint(tc) => {
6867 write!(f, "ADD {tc}")
6868 }
6869 AlterTableTarget::ValidateConstraint { name } => {
6870 write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
6871 }
6872 AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
6873 AlterTableTarget::ClusterOn { index } => match index {
6874 Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
6875 None => f.write_str("SET WITHOUT CLUSTER"),
6876 },
6877 AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
6878 // Round-trip-safe spelling: re-parsing this form lowers
6879 // back to SetColumnAutoIncrement (the nextval default is
6880 // how pg_dump says "serial").
6881 let seq = seq_name
6882 .clone()
6883 .unwrap_or_else(|| alloc::format!("{column}_seq"));
6884 write!(
6885 f,
6886 "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
6887 quote_ident(column)
6888 )
6889 }
6890 AlterTableTarget::RenameColumn { old, new } => {
6891 write!(
6892 f,
6893 "RENAME COLUMN {} TO {}",
6894 quote_ident(old),
6895 quote_ident(new)
6896 )
6897 }
6898 AlterTableTarget::RenameConstraint { old, new } => {
6899 write!(
6900 f,
6901 "RENAME CONSTRAINT {} TO {}",
6902 quote_ident(old),
6903 quote_ident(new)
6904 )
6905 }
6906 AlterTableTarget::RenameTable { new } => {
6907 write!(f, "RENAME TO {}", quote_ident(new))
6908 }
6909 AlterTableTarget::SetTriggerEnabled { which, enabled } => {
6910 f.write_str(if *enabled {
6911 "ENABLE TRIGGER "
6912 } else {
6913 "DISABLE TRIGGER "
6914 })?;
6915 match which {
6916 TriggerSelector::All => f.write_str("ALL"),
6917 TriggerSelector::Named(n) => f.write_str("e_ident(n)),
6918 }
6919 }
6920 AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
6921 (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
6922 (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
6923 (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
6924 (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
6925 (None, None) => Ok(()),
6926 },
6927 AlterTableTarget::AttachPartition { child, bounds } => {
6928 write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
6929 match bounds {
6930 PartitionOfBoundsAst::Range { lower, upper } => {
6931 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
6932 }
6933 PartitionOfBoundsAst::List { values } => {
6934 f.write_str("FOR VALUES IN (")?;
6935 for (i, v) in values.iter().enumerate() {
6936 if i > 0 {
6937 f.write_str(", ")?;
6938 }
6939 write!(f, "{}", v)?;
6940 }
6941 f.write_str(")")
6942 }
6943 PartitionOfBoundsAst::Hash { modulus, remainder } => {
6944 write!(
6945 f,
6946 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
6947 modulus, remainder
6948 )
6949 }
6950 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
6951 }
6952 }
6953 AlterTableTarget::DetachPartition {
6954 child,
6955 concurrently,
6956 finalize,
6957 } => {
6958 write!(f, "DETACH PARTITION {}", quote_ident(child))?;
6959 if *concurrently {
6960 f.write_str(" CONCURRENTLY")?;
6961 }
6962 if *finalize {
6963 f.write_str(" FINALIZE")?;
6964 }
6965 Ok(())
6966 }
6967 AlterTableTarget::AlterColumnSetDefault {
6968 column,
6969 default_expr,
6970 } => write!(
6971 f,
6972 "ALTER COLUMN {} SET DEFAULT {}",
6973 quote_ident(column),
6974 default_expr
6975 ),
6976 AlterTableTarget::AlterColumnDropDefault { column } => {
6977 write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
6978 }
6979 AlterTableTarget::AlterColumnSetNotNull { column } => {
6980 write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
6981 }
6982 AlterTableTarget::AlterColumnDropNotNull { column } => {
6983 write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
6984 }
6985 AlterTableTarget::AlterColumnRestart { column, with } => {
6986 write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
6987 if let Some(n) = with {
6988 write!(f, " WITH {n}")?;
6989 }
6990 Ok(())
6991 }
6992 AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
6993 write!(
6994 f,
6995 "ALTER COLUMN {} DROP EXPRESSION{}",
6996 quote_ident(column),
6997 if *if_exists { " IF EXISTS" } else { "" }
6998 )
6999 }
7000 AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7001 write!(
7002 f,
7003 "ALTER COLUMN {} DROP IDENTITY{}",
7004 quote_ident(column),
7005 if *if_exists { " IF EXISTS" } else { "" }
7006 )
7007 }
7008 AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7009 write!(
7010 f,
7011 "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7012 quote_ident(column)
7013 )
7014 }
7015 }
7016}
7017
7018impl fmt::Display for TableConstraint {
7019 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7020 match self {
7021 Self::PrimaryKey { name, columns, .. } => {
7022 if let Some(n) = name {
7023 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7024 }
7025 f.write_str("PRIMARY KEY (")?;
7026 for (i, c) in columns.iter().enumerate() {
7027 if i > 0 {
7028 f.write_str(", ")?;
7029 }
7030 f.write_str("e_ident(c))?;
7031 }
7032 f.write_str(")")
7033 }
7034 Self::Unique {
7035 name,
7036 columns,
7037 nulls_not_distinct,
7038 ..
7039 } => {
7040 if let Some(n) = name {
7041 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7042 }
7043 f.write_str("UNIQUE ")?;
7044 if *nulls_not_distinct {
7045 f.write_str("NULLS NOT DISTINCT ")?;
7046 }
7047 f.write_str("(")?;
7048 for (i, c) in columns.iter().enumerate() {
7049 if i > 0 {
7050 f.write_str(", ")?;
7051 }
7052 f.write_str("e_ident(c))?;
7053 }
7054 f.write_str(")")
7055 }
7056 Self::Check {
7057 name,
7058 expr,
7059 not_valid,
7060 } => {
7061 if let Some(n) = name {
7062 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7063 }
7064 write!(f, "CHECK ({expr})")?;
7065 if *not_valid {
7066 write!(f, " NOT VALID")?;
7067 }
7068 Ok(())
7069 }
7070 Self::Index { name, columns } => {
7071 f.write_str("KEY ")?;
7072 if let Some(n) = name {
7073 write!(f, "{} ", quote_ident(n))?;
7074 }
7075 f.write_str("(")?;
7076 for (i, c) in columns.iter().enumerate() {
7077 if i > 0 {
7078 f.write_str(", ")?;
7079 }
7080 f.write_str("e_ident(c))?;
7081 }
7082 f.write_str(")")
7083 }
7084 Self::FulltextIndex { name, columns } => {
7085 // Mysqldump emits `FULLTEXT KEY name (cols)` —
7086 // Display rounds back to that shape so dump
7087 // replay reproduces the input verbatim.
7088 f.write_str("FULLTEXT KEY ")?;
7089 if let Some(n) = name {
7090 write!(f, "{} ", quote_ident(n))?;
7091 }
7092 f.write_str("(")?;
7093 for (i, c) in columns.iter().enumerate() {
7094 if i > 0 {
7095 f.write_str(", ")?;
7096 }
7097 f.write_str("e_ident(c))?;
7098 }
7099 f.write_str(")")
7100 }
7101 Self::Exclude {
7102 name,
7103 method,
7104 elements,
7105 } => {
7106 if let Some(n) = name {
7107 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7108 }
7109 f.write_str("EXCLUDE ")?;
7110 if let Some(m) = method {
7111 write!(f, "USING {m} ")?;
7112 }
7113 f.write_str("(")?;
7114 for (i, (col, op)) in elements.iter().enumerate() {
7115 if i > 0 {
7116 f.write_str(", ")?;
7117 }
7118 write!(f, "{} WITH {op}", quote_ident(col))?;
7119 }
7120 f.write_str(")")
7121 }
7122 }
7123 }
7124}
7125
7126impl fmt::Display for ForeignKeyConstraint {
7127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7128 if let Some(name) = &self.name {
7129 write!(f, "CONSTRAINT {} ", quote_ident(name))?;
7130 }
7131 f.write_str("FOREIGN KEY (")?;
7132 for (i, c) in self.columns.iter().enumerate() {
7133 if i > 0 {
7134 f.write_str(", ")?;
7135 }
7136 f.write_str("e_ident(c))?;
7137 }
7138 write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
7139 if !self.parent_columns.is_empty() {
7140 f.write_str(" (")?;
7141 for (i, c) in self.parent_columns.iter().enumerate() {
7142 if i > 0 {
7143 f.write_str(", ")?;
7144 }
7145 f.write_str("e_ident(c))?;
7146 }
7147 f.write_str(")")?;
7148 }
7149 // Only render non-default actions to keep Display output
7150 // close to user input. SPG's default is RESTRICT (matches
7151 // SQL spec).
7152 if self.on_delete != FkAction::Restrict {
7153 write!(f, " ON DELETE {}", self.on_delete)?;
7154 }
7155 if self.on_update != FkAction::Restrict {
7156 write!(f, " ON UPDATE {}", self.on_update)?;
7157 }
7158 Ok(())
7159 }
7160}
7161
7162impl fmt::Display for FkAction {
7163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7164 match self {
7165 Self::Restrict => f.write_str("RESTRICT"),
7166 Self::Cascade => f.write_str("CASCADE"),
7167 Self::SetNull => f.write_str("SET NULL"),
7168 Self::SetDefault => f.write_str("SET DEFAULT"),
7169 Self::NoAction => f.write_str("NO ACTION"),
7170 }
7171 }
7172}
7173
7174impl fmt::Display for ColumnDef {
7175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7176 // v7.30.1 (mailrs round-24 class audit) — the type position
7177 // must re-parse to the same ColumnDef: a user-defined type
7178 // reference and the MySQL inline ENUM / SET value lists all
7179 // lower `ty` to Text, so rendering `ty` lost them.
7180 write!(f, "{}", quote_ident(&self.name))?;
7181 if let Some(ut) = &self.user_type_ref {
7182 write!(f, " {}", quote_ident(ut))?;
7183 } else if let Some(variants) = &self.inline_enum_variants {
7184 write_variant_list(f, "ENUM", variants)?;
7185 } else if let Some(variants) = &self.inline_set_variants {
7186 write_variant_list(f, "SET", variants)?;
7187 } else {
7188 write!(f, " {}", self.ty)?;
7189 }
7190 if self.is_unsigned {
7191 f.write_str(" UNSIGNED")?;
7192 }
7193 // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
7194 // DDL. Only emits when non-default so the typical output
7195 // stays unchanged.
7196 match self.collation {
7197 Collation::Binary => {}
7198 Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
7199 }
7200 if let Some(d) = &self.default {
7201 write!(f, " DEFAULT {d}")?;
7202 }
7203 if self.auto_increment {
7204 f.write_str(" AUTO_INCREMENT")?;
7205 }
7206 if !self.nullable {
7207 f.write_str(" NOT NULL")?;
7208 }
7209 // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
7210 // is NOT lifted to a table-level constraint at parse time
7211 // (unlike UNIQUE / CHECK), so the WAL round trip of a
7212 // prepared CREATE TABLE silently dropped the primary key.
7213 if self.is_primary_key {
7214 f.write_str(" PRIMARY KEY")?;
7215 }
7216 // The parser accepts only CURRENT_TIMESTAMP here (stored as
7217 // now()), so that spelling is the lossless round trip.
7218 if self.on_update_runtime.is_some() {
7219 f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
7220 }
7221 // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
7222 // replay reconstructs the computed-column declaration. The
7223 // expression sits inside a single set of parens; STORED is
7224 // the only variant the parser accepts.
7225 if let Some(gen_expr) = &self.generated_stored_expr {
7226 write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
7227 }
7228 Ok(())
7229 }
7230}
7231
7232/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
7233/// types (MySQL flavour; `ty` is Text underneath).
7234fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
7235 write!(f, " {kw}(")?;
7236 for (i, v) in variants.iter().enumerate() {
7237 if i > 0 {
7238 f.write_str(", ")?;
7239 }
7240 write!(f, "'{}'", v.replace('\'', "''"))?;
7241 }
7242 f.write_str(")")
7243}
7244
7245impl fmt::Display for InsertStatement {
7246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7247 write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
7248 if let Some(cols) = &self.columns {
7249 f.write_str(" (")?;
7250 for (i, c) in cols.iter().enumerate() {
7251 if i > 0 {
7252 f.write_str(", ")?;
7253 }
7254 f.write_str("e_ident(c))?;
7255 }
7256 f.write_str(")")?;
7257 }
7258 // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
7259 // skipping the VALUES list (mailrs round-5 G4).
7260 if let Some(sel) = &self.select_source {
7261 write!(f, " {sel}")?;
7262 } else {
7263 f.write_str(" VALUES ")?;
7264 for (ri, row) in self.rows.iter().enumerate() {
7265 if ri > 0 {
7266 f.write_str(", ")?;
7267 }
7268 f.write_str("(")?;
7269 for (i, v) in row.iter().enumerate() {
7270 if i > 0 {
7271 f.write_str(", ")?;
7272 }
7273 write!(f, "{v}")?;
7274 }
7275 f.write_str(")")?;
7276 }
7277 }
7278 // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
7279 // Display round trip: WAL persistence renders the bind-final
7280 // AST through this impl, and a replayed bare INSERT turns a
7281 // legal upsert no-op into a UNIQUE violation that refuses to
7282 // open the catalog.
7283 if let Some(oc) = &self.on_conflict {
7284 write!(f, " {oc}")?;
7285 }
7286 write_returning(self.returning.as_deref(), f)?;
7287 Ok(())
7288 }
7289}
7290
7291/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
7292/// parser produced, so the AST→SQL round trip preserves upsert
7293/// semantics (WAL replay depends on it).
7294impl fmt::Display for OnConflictClause {
7295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7296 f.write_str("ON CONFLICT")?;
7297 if let Some(name) = &self.constraint_name {
7298 write!(f, " ON CONSTRAINT {name}")?;
7299 }
7300 if !self.target_columns.is_empty() {
7301 f.write_str(" (")?;
7302 for (i, c) in self.target_columns.iter().enumerate() {
7303 if i > 0 {
7304 f.write_str(", ")?;
7305 }
7306 f.write_str("e_ident(c))?;
7307 }
7308 f.write_str(")")?;
7309 }
7310 if let Some(w) = &self.index_where {
7311 write!(f, " WHERE {w}")?;
7312 }
7313 match &self.action {
7314 OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
7315 OnConflictAction::Update {
7316 assignments,
7317 where_,
7318 } => {
7319 f.write_str(" DO UPDATE SET ")?;
7320 for (i, (col, expr)) in assignments.iter().enumerate() {
7321 if i > 0 {
7322 f.write_str(", ")?;
7323 }
7324 write!(f, "{} = {expr}", quote_ident(col))?;
7325 }
7326 if let Some(w) = where_ {
7327 write!(f, " WHERE {w}")?;
7328 }
7329 Ok(())
7330 }
7331 }
7332 }
7333}
7334
7335/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
7336/// tail for the three DML Display impls.
7337fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7338 let Some(items) = ret else {
7339 return Ok(());
7340 };
7341 f.write_str(" RETURNING ")?;
7342 for (i, item) in items.iter().enumerate() {
7343 if i > 0 {
7344 f.write_str(", ")?;
7345 }
7346 write!(f, "{item}")?;
7347 }
7348 Ok(())
7349}
7350
7351impl fmt::Display for UpdateStatement {
7352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7353 write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
7354 for (i, (col, expr)) in self.assignments.iter().enumerate() {
7355 if i > 0 {
7356 f.write_str(", ")?;
7357 }
7358 write!(f, "{} = {expr}", quote_ident(col))?;
7359 }
7360 if let Some(w) = &self.where_ {
7361 write!(f, " WHERE {w}")?;
7362 }
7363 // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
7364 if let Some(ol) = self.order_limit.as_deref() {
7365 if !ol.order_by.is_empty() {
7366 f.write_str(" ORDER BY ")?;
7367 for (i, o) in ol.order_by.iter().enumerate() {
7368 if i > 0 {
7369 f.write_str(", ")?;
7370 }
7371 write!(f, "{}", o.expr)?;
7372 if o.desc {
7373 f.write_str(" DESC")?;
7374 }
7375 match o.nulls_first {
7376 Some(true) => f.write_str(" NULLS FIRST")?,
7377 Some(false) => f.write_str(" NULLS LAST")?,
7378 None => {}
7379 }
7380 }
7381 }
7382 if let Some(n) = ol.limit {
7383 write!(f, " LIMIT {n}")?;
7384 }
7385 }
7386 write_returning(self.returning.as_deref(), f)?;
7387 Ok(())
7388 }
7389}
7390
7391impl fmt::Display for DeleteStatement {
7392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7393 write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
7394 if let Some(w) = &self.where_ {
7395 write!(f, " WHERE {w}")?;
7396 }
7397 write_returning(self.returning.as_deref(), f)?;
7398 Ok(())
7399 }
7400}
7401
7402impl fmt::Display for CteBody {
7403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7404 match self {
7405 Self::Select(s) => write!(f, "{s}"),
7406 Self::Insert(s) => write!(f, "{s}"),
7407 Self::Update(s) => write!(f, "{s}"),
7408 Self::Delete(s) => write!(f, "{s}"),
7409 Self::Merge(s) => write!(f, "{s}"),
7410 }
7411 }
7412}
7413
7414impl fmt::Display for MergeStatement {
7415 // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
7416 // (it round-trips for the cases tests cover, not for
7417 // round-tripping every edge of the surface).
7418 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7419 fmt_with_clause(&self.ctes, f)?;
7420 f.write_str("MERGE INTO ")?;
7421 write!(f, "{}", quote_ident(&self.target))?;
7422 if let Some(a) = &self.target_alias {
7423 write!(f, " {}", quote_ident(a))?;
7424 }
7425 f.write_str(" USING ")?;
7426 if let Some(sub) = &self.source_select {
7427 write!(f, "({sub})")?;
7428 } else {
7429 write!(f, "{}", quote_ident(&self.source))?;
7430 }
7431 if let Some(a) = &self.source_alias {
7432 write!(f, " {}", quote_ident(a))?;
7433 }
7434 if !self.source_column_aliases.is_empty() {
7435 f.write_str("(")?;
7436 for (i, c) in self.source_column_aliases.iter().enumerate() {
7437 if i > 0 {
7438 f.write_str(", ")?;
7439 }
7440 write!(f, "{}", quote_ident(c))?;
7441 }
7442 f.write_str(")")?;
7443 }
7444 write!(f, " ON {}", self.on)?;
7445 for clause in &self.clauses {
7446 f.write_str(" WHEN ")?;
7447 f.write_str(match clause.matched {
7448 MergeMatched::Matched => "MATCHED",
7449 MergeMatched::NotMatched => "NOT MATCHED",
7450 MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
7451 })?;
7452 if let Some(c) = &clause.condition {
7453 write!(f, " AND {c}")?;
7454 }
7455 f.write_str(" THEN ")?;
7456 match &clause.action {
7457 MergeAction::Insert { columns, values } => {
7458 f.write_str("INSERT ")?;
7459 // A column list is optional (round 146): the bare
7460 // `INSERT VALUES (…)` form maps positionally.
7461 if !columns.is_empty() {
7462 f.write_str("(")?;
7463 for (i, c) in columns.iter().enumerate() {
7464 if i > 0 {
7465 f.write_str(", ")?;
7466 }
7467 write!(f, "{}", quote_ident(c))?;
7468 }
7469 f.write_str(") ")?;
7470 }
7471 f.write_str("VALUES (")?;
7472 for (i, v) in values.iter().enumerate() {
7473 if i > 0 {
7474 f.write_str(", ")?;
7475 }
7476 write!(f, "{v}")?;
7477 }
7478 f.write_str(")")?;
7479 }
7480 MergeAction::Update { assignments } => {
7481 f.write_str("UPDATE SET ")?;
7482 for (i, (c, e)) in assignments.iter().enumerate() {
7483 if i > 0 {
7484 f.write_str(", ")?;
7485 }
7486 write!(f, "{} = {e}", quote_ident(c))?;
7487 }
7488 }
7489 MergeAction::Delete => f.write_str("DELETE")?,
7490 MergeAction::DoNothing => f.write_str("DO NOTHING")?,
7491 }
7492 }
7493 if let Some(items) = &self.returning {
7494 f.write_str(" RETURNING ")?;
7495 for (i, it) in items.iter().enumerate() {
7496 if i > 0 {
7497 f.write_str(", ")?;
7498 }
7499 write!(f, "{it}")?;
7500 }
7501 }
7502 Ok(())
7503 }
7504}
7505
7506/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
7507/// carry a CTE list and must round-trip it identically.
7508fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
7509 if ctes.is_empty() {
7510 return Ok(());
7511 }
7512 f.write_str("WITH ")?;
7513 if ctes.iter().any(|c| c.recursive) {
7514 f.write_str("RECURSIVE ")?;
7515 }
7516 for (i, cte) in ctes.iter().enumerate() {
7517 if i > 0 {
7518 f.write_str(", ")?;
7519 }
7520 f.write_str("e_ident(&cte.name))?;
7521 if !cte.column_overrides.is_empty() {
7522 f.write_str(" (")?;
7523 for (ci, c) in cte.column_overrides.iter().enumerate() {
7524 if ci > 0 {
7525 f.write_str(", ")?;
7526 }
7527 f.write_str("e_ident(c))?;
7528 }
7529 f.write_str(")")?;
7530 }
7531 write!(f, " AS ({})", cte.body)?;
7532 }
7533 f.write_str(" ")
7534}
7535
7536impl fmt::Display for SelectStatement {
7537 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7538 // v7.30.1 (mailrs round-24 class audit) — the WITH clause
7539 // must survive the round trip; a CTE-using statement
7540 // re-parsed without it references undefined tables.
7541 fmt_with_clause(&self.ctes, f)?;
7542 write_bare_select(self, f)?;
7543 for (kind, peer) in &self.unions {
7544 f.write_str(match kind {
7545 UnionKind::Distinct => " UNION ",
7546 UnionKind::All => " UNION ALL ",
7547 UnionKind::Intersect => " INTERSECT ",
7548 UnionKind::IntersectAll => " INTERSECT ALL ",
7549 UnionKind::Except => " EXCEPT ",
7550 UnionKind::ExceptAll => " EXCEPT ALL ",
7551 })?;
7552 write_bare_select(peer, f)?;
7553 }
7554 if !self.order_by.is_empty() {
7555 f.write_str(" ORDER BY ")?;
7556 for (i, o) in self.order_by.iter().enumerate() {
7557 if i > 0 {
7558 f.write_str(", ")?;
7559 }
7560 write!(f, "{}", o.expr)?;
7561 if o.desc {
7562 f.write_str(" DESC")?;
7563 }
7564 match o.nulls_first {
7565 Some(true) => f.write_str(" NULLS FIRST")?,
7566 Some(false) => f.write_str(" NULLS LAST")?,
7567 None => {}
7568 }
7569 }
7570 }
7571 // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
7572 // exists in the FETCH FIRST spelling; rendering it as LIMIT
7573 // dropped the tie-extension semantics on replay. The parser
7574 // accepts OFFSET before FETCH, so keep that order here.
7575 if self.limit_with_ties {
7576 if let Some(o) = &self.offset {
7577 write!(f, " OFFSET {o}")?;
7578 }
7579 if let Some(n) = &self.limit {
7580 write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
7581 }
7582 } else {
7583 if let Some(n) = &self.limit {
7584 write!(f, " LIMIT {n}")?;
7585 }
7586 if let Some(o) = &self.offset {
7587 write!(f, " OFFSET {o}")?;
7588 }
7589 }
7590 Ok(())
7591 }
7592}
7593
7594fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7595 f.write_str("SELECT ")?;
7596 if s.distinct {
7597 f.write_str("DISTINCT ")?;
7598 }
7599 write_bare_select_body(s, f)
7600}
7601
7602fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7603 for (i, item) in s.items.iter().enumerate() {
7604 if i > 0 {
7605 f.write_str(", ")?;
7606 }
7607 write!(f, "{item}")?;
7608 }
7609 if let Some(t) = &s.from {
7610 write!(f, " FROM {t}")?;
7611 }
7612 if let Some(e) = &s.where_ {
7613 write!(f, " WHERE {e}")?;
7614 }
7615 if let Some(gs) = &s.group_by {
7616 f.write_str(" GROUP BY ")?;
7617 for (i, g) in gs.iter().enumerate() {
7618 if i > 0 {
7619 f.write_str(", ")?;
7620 }
7621 write!(f, "{g}")?;
7622 }
7623 } else if s.group_by_all {
7624 // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
7625 // shortcut parses to group_by: None + this flag; dropping
7626 // it turned an aggregate query into a bare projection on
7627 // re-parse.
7628 f.write_str(" GROUP BY ALL")?;
7629 }
7630 if let Some(h) = &s.having {
7631 write!(f, " HAVING {h}")?;
7632 }
7633 Ok(())
7634}
7635
7636impl fmt::Display for SelectItem {
7637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7638 match self {
7639 Self::Wildcard => f.write_str("*"),
7640 Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
7641 Self::Expr { expr, alias } => {
7642 write!(f, "{expr}")?;
7643 if let Some(a) = alias {
7644 write!(f, " AS {}", quote_ident(a))?;
7645 }
7646 Ok(())
7647 }
7648 }
7649 }
7650}
7651
7652impl fmt::Display for FromClause {
7653 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7654 write!(f, "{}", self.primary)?;
7655 for j in &self.joins {
7656 match j.kind {
7657 JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
7658 JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
7659 JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
7660 JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
7661 JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
7662 JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
7663 }
7664 if let Some(on) = &j.on {
7665 write!(f, " ON {on}")?;
7666 }
7667 }
7668 Ok(())
7669 }
7670}
7671
7672/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
7673/// for NESTED). Kept close to the parser's grammar so it re-parses.
7674fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
7675 for (i, c) in cols.iter().enumerate() {
7676 if i > 0 {
7677 f.write_str(", ")?;
7678 }
7679 match c {
7680 JsonTableColumn::Ordinality { name } => {
7681 write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
7682 }
7683 JsonTableColumn::Nested { path, columns } => {
7684 write!(f, "NESTED PATH '{path}' COLUMNS (")?;
7685 fmt_json_table_columns(f, columns)?;
7686 f.write_str(")")?;
7687 }
7688 JsonTableColumn::Regular {
7689 name,
7690 ty,
7691 path,
7692 exists,
7693 format_json,
7694 wrapper,
7695 on_empty,
7696 on_error,
7697 } => {
7698 write!(f, "{} {ty}", quote_ident(name))?;
7699 if *format_json {
7700 f.write_str(" FORMAT JSON")?;
7701 }
7702 if *exists {
7703 write!(f, " EXISTS PATH '{path}'")?;
7704 } else {
7705 write!(f, " PATH '{path}'")?;
7706 }
7707 if *wrapper {
7708 f.write_str(" WITH WRAPPER")?;
7709 }
7710 if let JsonTableOnBehavior::Error = on_empty {
7711 f.write_str(" ERROR ON EMPTY")?;
7712 } else if let JsonTableOnBehavior::Default(e) = on_empty {
7713 write!(f, " DEFAULT {e} ON EMPTY")?;
7714 }
7715 if let JsonTableOnBehavior::Error = on_error {
7716 f.write_str(" ERROR ON ERROR")?;
7717 } else if let JsonTableOnBehavior::Default(e) = on_error {
7718 write!(f, " DEFAULT {e} ON ERROR")?;
7719 }
7720 }
7721 }
7722 }
7723 Ok(())
7724}
7725
7726impl fmt::Display for TableRef {
7727 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7728 // v7.30.1 (mailrs round-24 class audit) — the dynamic
7729 // table-ref shapes must round-trip: rendering only the
7730 // (synthetic) name turned LATERAL / unnest() /
7731 // generate_series() into references to nonexistent tables
7732 // on re-parse.
7733 // v7.39 (round 205) — JSON_TABLE round-trips through Display
7734 // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
7735 if let Some(jt) = &self.json_table {
7736 write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
7737 if !jt.passing.is_empty() {
7738 f.write_str(" PASSING ")?;
7739 for (i, (n, e)) in jt.passing.iter().enumerate() {
7740 if i > 0 {
7741 f.write_str(", ")?;
7742 }
7743 write!(f, "{e} AS {}", quote_ident(n))?;
7744 }
7745 }
7746 f.write_str(" COLUMNS (")?;
7747 fmt_json_table_columns(f, &jt.columns)?;
7748 f.write_str(")")?;
7749 if let Some(a) = &self.alias {
7750 write!(f, " AS {}", quote_ident(a))?;
7751 }
7752 return Ok(());
7753 }
7754 if let Some(inner) = &self.lateral_subquery {
7755 write!(f, "LATERAL ({inner})")?;
7756 if let Some(a) = &self.alias {
7757 write!(f, " AS {}", quote_ident(a))?;
7758 // v7.37 D.28 — a derived table on the lateral_subquery channel
7759 // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
7760 // lowers here). Rendering the alias without the column list lost
7761 // the column names on re-parse (a view body round-trips through
7762 // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
7763 if !self.unnest_column_aliases.is_empty() {
7764 f.write_str(" (")?;
7765 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
7766 if i > 0 {
7767 f.write_str(", ")?;
7768 }
7769 f.write_str("e_ident(c))?;
7770 }
7771 f.write_str(")")?;
7772 }
7773 }
7774 return Ok(());
7775 }
7776 if let Some(expr) = &self.unnest_expr {
7777 write!(f, "UNNEST({expr})")?;
7778 if let Some(a) = &self.alias {
7779 write!(f, " AS {}", quote_ident(a))?;
7780 if !self.unnest_column_aliases.is_empty() {
7781 f.write_str(" (")?;
7782 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
7783 if i > 0 {
7784 f.write_str(", ")?;
7785 }
7786 f.write_str("e_ident(c))?;
7787 }
7788 f.write_str(")")?;
7789 }
7790 }
7791 return Ok(());
7792 }
7793 // 7.38.1 S5.1 — a FROM-position table function must re-render
7794 // as the CALL, not its bare name: ARRAY(subquery) desugars by
7795 // re-parsing the subquery's canonical text, and a dropped
7796 // argument list turned `pg_options_to_table(x)` into a
7797 // relation lookup that does not exist.
7798 if let Some(call) = &self.table_fn_call {
7799 let (fn_name, args) = call.as_ref();
7800 write!(f, "{fn_name}(")?;
7801 for (i, a) in args.iter().enumerate() {
7802 if i > 0 {
7803 f.write_str(", ")?;
7804 }
7805 write!(f, "{a}")?;
7806 }
7807 f.write_str(")")?;
7808 if let Some(a) = &self.alias {
7809 write!(f, " AS {}", quote_ident(a))?;
7810 if !self.unnest_column_aliases.is_empty() {
7811 f.write_str("(")?;
7812 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
7813 if i > 0 {
7814 f.write_str(", ")?;
7815 }
7816 write!(f, "{}", quote_ident(c))?;
7817 }
7818 f.write_str(")")?;
7819 }
7820 }
7821 return Ok(());
7822 }
7823 if let Some(args) = &self.generate_series_args {
7824 f.write_str("generate_series(")?;
7825 for (i, a) in args.iter().enumerate() {
7826 if i > 0 {
7827 f.write_str(", ")?;
7828 }
7829 write!(f, "{a}")?;
7830 }
7831 f.write_str(")")?;
7832 if let Some(a) = &self.alias {
7833 write!(f, " AS {}", quote_ident(a))?;
7834 }
7835 return Ok(());
7836 }
7837 write!(f, "{}", quote_ident(&self.name))?;
7838 if let Some(seg) = self.as_of_segment {
7839 write!(f, " AS OF SEGMENT {seg}")?;
7840 }
7841 if let Some(a) = &self.alias {
7842 write!(f, " AS {}", quote_ident(a))?;
7843 }
7844 Ok(())
7845 }
7846}
7847
7848impl fmt::Display for ColumnName {
7849 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7850 if let Some(q) = &self.qualifier {
7851 write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
7852 } else {
7853 write!(f, "{}", quote_ident(&self.name))
7854 }
7855 }
7856}
7857
7858/// v7.39 (round 311) — render the left spine of an AND / OR chain
7859/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
7860/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
7861/// SAME operator flattens; anything else is an ordinary operand.
7862fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
7863 if let Expr::Binary {
7864 lhs,
7865 op: inner,
7866 rhs,
7867 } = e
7868 && *inner == op
7869 {
7870 write_bool_chain(f, lhs, op)?;
7871 return write!(f, " {op} {rhs}");
7872 }
7873 write!(f, "{e}")
7874}
7875
7876/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
7877/// form `pg_get_constraintdef(oid, true)` and friends return.
7878///
7879/// The default [`fmt::Display`] parenthesises every operator node, which
7880/// is what PG's non-pretty deparse does and what makes the text
7881/// round-trip. Pretty drops the pairs the grammar can put back, and the
7882/// rule is NOT plain precedence minimisation — measured against PG 18.4
7883/// across 37 shapes:
7884///
7885/// * the boolean layer follows precedence (NOT > AND > OR): an OR
7886/// under an AND keeps its parens, an AND under an OR does not, and a
7887/// comparison under any of them does not (`NOT a > 1`);
7888/// * an associative chain flattens completely, even where the source
7889/// nested it to the right (`a AND (b AND c)` prints as one chain);
7890/// * but an operand of a comparison or arithmetic operator keeps its
7891/// parens whenever it is itself an operator expression — so
7892/// `(a + b) > 10` and `(- a) + b`, even though precedence alone
7893/// would not require either. A cast, function call, column or
7894/// literal in that position does not (`a::text = t`,
7895/// `length(code) > 2`); a cast counts as compound exactly when the
7896/// thing it casts is (`((a + b)::text) = t`).
7897///
7898/// Anything outside that layer defers to `Display`, which is never
7899/// wrong — only more parenthesised than PG would print.
7900#[must_use]
7901pub fn pretty_expr(e: &Expr) -> String {
7902 let mut out = String::new();
7903 write_pretty(&mut out, e, PrettyParent::None, false, false);
7904 out
7905}
7906
7907/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
7908/// writes it.
7909///
7910/// MariaDB names the offending expression in its out-of-range message
7911/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
7912/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
7913/// MySQL client, for a cast the client had just written the other way.
7914#[must_use]
7915pub fn pretty_expr_mysql(e: &Expr) -> String {
7916 let mut out = String::new();
7917 write_pretty(&mut out, e, PrettyParent::None, false, true);
7918 out
7919}
7920
7921/// v7.39 (round 505) — how strongly an expression suggests its own column
7922/// name. A cast keeps its argument's name only when that name is STRONG;
7923/// otherwise the cast reports the type it casts to.
7924///
7925/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
7926/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
7927/// itself `text` — so `case` and a function name cannot be the same kind of
7928/// answer, even though a bare `CASE …` does report `case`.
7929#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7930enum NameStrength {
7931 /// Nothing to go on — PG reports `?column?`.
7932 None,
7933 /// A name, but one a cast overrides: `case`, or a type name.
7934 Weak,
7935 /// A name a cast keeps: a column, or the function that produced it.
7936 Strong,
7937}
7938
7939/// v7.39 (round 505) — the column name PG18 gives a projected expression
7940/// that carries no `AS` alias. `None` means `?column?`.
7941///
7942/// SPG used to print the parsed expression back out, which matched neither
7943/// oracle and made name-keyed row access miss on both wires:
7944///
7945/// | query | PG18 | SPG (before) |
7946/// |--------------|------------|--------------|
7947/// | `upper(s)` | `upper` | `upper(s)` |
7948/// | `a+b` | `?column?` | `(a + b)` |
7949/// | `'lit'` | `?column?` | `'lit'` |
7950/// | `CASE …` | `case` | `CASE WHEN (a = 1) THEN …` |
7951///
7952/// Every rule below is one of those measurements, taken with `\gdesc`
7953/// against PG18: a call is named for its function, a cast recurses into its
7954/// argument and falls back to the type, a scalar subquery takes the name of
7955/// the column it selects, and operators have no name at all.
7956#[must_use]
7957pub fn figure_column_name(expr: &Expr) -> Option<String> {
7958 let (name, _) = figure_name_inner(expr);
7959 name
7960}
7961
7962/// The name a function reports, which is not always the name SPG parsed it
7963/// under: `count(*)` is held as `count_star` so the star arity survives the
7964/// AST, and that internal spelling must not reach a client. PG18 reports
7965/// `count`.
7966fn canonical_function_name(name: &str) -> String {
7967 match name {
7968 "count_star" => "count".to_string(),
7969 other => other.to_ascii_lowercase(),
7970 }
7971}
7972
7973/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
7974/// reports when its operand has none of its own. Only the spellings that
7975/// differ from what the user writes need an entry; everything else is
7976/// already its own typname.
7977fn cast_target_typname(target: &CastTarget) -> String {
7978 let written = target.to_string().to_ascii_lowercase();
7979 let base = written.strip_suffix("[]").unwrap_or(&written);
7980 let mapped = match base {
7981 "bigint" => "int8",
7982 "integer" | "int" => "int4",
7983 "smallint" => "int2",
7984 "boolean" => "bool",
7985 "double precision" => "float8",
7986 "real" => "float4",
7987 "character varying" => "varchar",
7988 "character" => "bpchar",
7989 "timestamp with time zone" => "timestamptz",
7990 "timestamp without time zone" => "timestamp",
7991 "time without time zone" => "time",
7992 "decimal" => "numeric",
7993 other => other,
7994 };
7995 if written.ends_with("[]") {
7996 alloc::format!("_{mapped}")
7997 } else {
7998 String::from(mapped)
7999 }
8000}
8001
8002fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8003 let strong = |n: String| (Some(n), NameStrength::Strong);
8004 match expr {
8005 // A column keeps its own name, qualifier and all discarded:
8006 // `lbl.a` reports `a`.
8007 Expr::Column(c) => strong(c.name.clone()),
8008 // Calls are named for the function. This covers the shapes that
8009 // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8010 // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8011 // because PG resolves them to functions before naming them.
8012 Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8013 strong(canonical_function_name(name))
8014 }
8015 Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8016 Expr::Extract { .. } => strong("extract".to_string()),
8017 Expr::Exists { .. } => strong("exists".to_string()),
8018 Expr::Array(_) => strong("array".to_string()),
8019 // `(expr).field` is named for the field, as a column would be.
8020 Expr::FieldAccess { field, .. } => strong(field.clone()),
8021 // A cast prefers its argument's name and settles for the type:
8022 // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8023 Expr::Cast {
8024 expr: inner,
8025 target,
8026 } => match figure_name_inner(inner) {
8027 (Some(n), NameStrength::Strong) => strong(n),
8028 // v7.38.7 — the fallback is the target type's INTERNAL name,
8029 // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8030 // the `bigint` the user typed. Measured on PG18 alongside
8031 // `CAST(7 AS bigint)`, which answers `int8` too.
8032 _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8033 },
8034 // A scalar subquery reports whatever its single output column
8035 // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8036 Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8037 // `CASE …` names itself, but weakly — a cast around it wins.
8038 Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8039 // A literal that carries its own type names itself for that type:
8040 // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8041 // reports nothing. Weak, like any other type name.
8042 Expr::Literal(Literal::Interval { .. }) => {
8043 (Some("interval".to_string()), NameStrength::Weak)
8044 }
8045 // A wrapper that adds no name of its own.
8046 Expr::Variadic(inner) => figure_name_inner(inner),
8047 Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
8048 // Everything else — operators, comparisons, IS NULL, LIKE, IN,
8049 // literals, placeholders — reports `?column?`.
8050 _ => (None, NameStrength::None),
8051 }
8052}
8053
8054/// The name a scalar subquery's single projected column reports.
8055fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
8056 match sel.items.as_slice() {
8057 [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
8058 [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
8059 _ => (None, NameStrength::None),
8060 }
8061}
8062
8063/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
8064fn pretty_prec(e: &Expr) -> u8 {
8065 match e {
8066 Expr::Binary { op, .. } => match op {
8067 // v7.39 (round 407) — this deparse ladder mirrors the parser's:
8068 // OR < XOR < AND < NOT < comparison < additive < multiplicative.
8069 // XOR (MySQL-only) sits between OR and AND, so AND and everything
8070 // above shifted +1 to open rung 2 for it.
8071 BinOp::Or => 1,
8072 BinOp::LogicalXor => 2,
8073 BinOp::And => 3,
8074 BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
8075 BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
8076 // Everything else in this enum is a comparison-shaped
8077 // operator; they share one level, as in the grammar.
8078 _ => 5,
8079 },
8080 Expr::Unary { op, .. } => match op {
8081 UnOp::Not => 4,
8082 UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
8083 },
8084 _ => u8::MAX,
8085 }
8086}
8087
8088/// Is this node an operator expression — the thing an arithmetic or
8089/// comparison parent keeps parentheses around? A cast inherits the
8090/// answer from what it casts.
8091fn pretty_is_compound(e: &Expr) -> bool {
8092 match e {
8093 Expr::Binary { .. } | Expr::Unary { .. } => true,
8094 Expr::Cast { expr, .. } => pretty_is_compound(expr),
8095 _ => false,
8096 }
8097}
8098
8099/// `parent` describes the enclosing operator: its binding power, and
8100/// whether it is a comparison (which keeps parens around any operator
8101/// operand) or a NOT (which keeps them at equal power too).
8102#[derive(Clone, Copy, PartialEq)]
8103enum PrettyParent {
8104 /// Nothing encloses this node.
8105 None,
8106 /// A comparison-shaped operator: an operator operand always keeps
8107 /// its parens, whatever precedence would allow.
8108 Comparison,
8109 /// Arithmetic / concatenation: precedence decides.
8110 Arith(u8),
8111 /// A boolean connective: precedence decides.
8112 Bool(u8),
8113 /// `NOT`: precedence decides, but equal power still needs parens so
8114 /// `NOT (NOT a > 1)` does not collapse.
8115 Not,
8116}
8117
8118fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
8119 let prec = pretty_prec(e);
8120 let is_unary_sign = matches!(
8121 e,
8122 Expr::Unary {
8123 op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
8124 ..
8125 }
8126 );
8127 let needs = match parent {
8128 PrettyParent::None => false,
8129 PrettyParent::Comparison => pretty_is_compound(e),
8130 // A sign always keeps its parens under an operator — PG writes
8131 // `(- a) + b` even though precedence would not require it.
8132 PrettyParent::Arith(p) => {
8133 is_unary_sign
8134 || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
8135 && (prec < p || (prec == p && is_rhs)))
8136 }
8137 PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
8138 PrettyParent::Not => {
8139 matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
8140 }
8141 };
8142 if needs {
8143 out.push('(');
8144 }
8145 match e {
8146 Expr::Binary { lhs, op, rhs } => {
8147 let child = match op {
8148 BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
8149 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
8150 PrettyParent::Arith(prec)
8151 }
8152 _ => PrettyParent::Comparison,
8153 };
8154 write_pretty(out, lhs, child, false, mysql);
8155 out.push(' ');
8156 out.push_str(&alloc::format!("{op}"));
8157 out.push(' ');
8158 // AND / OR are associative, so an explicitly right-nested
8159 // chain still prints as one chain.
8160 let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
8161 write_pretty(out, rhs, child, rhs_is_rhs, mysql);
8162 }
8163 Expr::Unary { op, expr } => match op {
8164 UnOp::Not => {
8165 out.push_str("NOT ");
8166 write_pretty(out, expr, PrettyParent::Not, false, mysql);
8167 }
8168 UnOp::Neg => {
8169 out.push_str("- ");
8170 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8171 }
8172 UnOp::Plus => {
8173 out.push_str("+ ");
8174 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8175 }
8176 UnOp::BitNot => {
8177 out.push('~');
8178 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8179 }
8180 },
8181 Expr::Cast { expr, target } => {
8182 if mysql {
8183 // MySQL's own spelling, which is what its error messages
8184 // quote back.
8185 out.push_str("cast(");
8186 write_pretty(out, expr, PrettyParent::None, false, mysql);
8187 out.push_str(&alloc::format!(
8188 " as {})",
8189 target.to_string().to_lowercase()
8190 ));
8191 } else {
8192 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8193 out.push_str(&alloc::format!("::{target}"));
8194 }
8195 }
8196 Expr::IsNull { expr, negated } => {
8197 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8198 out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
8199 }
8200 other => out.push_str(&alloc::format!("{other}")),
8201 }
8202 if needs {
8203 out.push(')');
8204 }
8205}
8206
8207const fn pretty_prec_not() -> u8 {
8208 // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
8209 // when the XOR insertion shifted the deparse ladder up by one).
8210 4
8211}
8212
8213impl fmt::Display for Expr {
8214 #[allow(clippy::too_many_lines)]
8215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8216 match self {
8217 Self::Literal(l) => write!(f, "{l}"),
8218 Self::Column(c) => write!(f, "{c}"),
8219 Self::Placeholder(n) => write!(f, "${n}"),
8220 // Round-trips as the spelling PG's docs lead with.
8221 Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
8222 Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
8223 // v7.39 (round 311) — an AND / OR chain that nests to the
8224 // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
8225 // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
8226 // its parentheses, because that is a different grouping as
8227 // written. Both halves measured against PG 18.4's deparse,
8228 // which flattens a same-operator left chain at parse time and
8229 // leaves `a AND (b AND c)` alone.
8230 Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
8231 f.write_str("(")?;
8232 write_bool_chain(f, lhs, *op)?;
8233 write!(f, " {op} {rhs}")?;
8234 f.write_str(")")
8235 }
8236 Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
8237 Self::Unary { op, expr } => match op {
8238 UnOp::Not => write!(f, "(NOT {expr})"),
8239 // A space after the sign, as PG's deparse writes it.
8240 UnOp::Neg => write!(f, "(- {expr})"),
8241 UnOp::Plus => write!(f, "(+ {expr})"),
8242 UnOp::BitNot => write!(f, "(~{expr})"),
8243 },
8244 // The OPERAND carries the parentheses, not the cast:
8245 // `(a)::text`, `((a + b))::text`. PG words it this way, and
8246 // it is what keeps `a::text = t` from reading as a cast of
8247 // the comparison.
8248 Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
8249 Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
8250 Self::AggregateOrdered {
8251 call,
8252 order_by,
8253 distinct,
8254 filter,
8255 } => {
8256 let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8257 for (i, o) in order_by.iter().enumerate() {
8258 if i > 0 {
8259 f.write_str(", ")?;
8260 }
8261 write!(f, "{}", o.expr)?;
8262 if o.desc {
8263 f.write_str(" DESC")?;
8264 }
8265 match o.nulls_first {
8266 Some(true) => f.write_str(" NULLS FIRST")?,
8267 Some(false) => f.write_str(" NULLS LAST")?,
8268 None => {}
8269 }
8270 }
8271 Ok(())
8272 };
8273 // Ordered-set aggregates (`percentile_cont(f) WITHIN
8274 // GROUP (ORDER BY x)`) render the in-parens args as the
8275 // direct argument and the sort spec under WITHIN GROUP —
8276 // not as an in-argument ORDER BY.
8277 let ordered_set = matches!(
8278 call.as_ref(),
8279 Expr::FunctionCall { name, .. }
8280 if matches!(
8281 name.to_ascii_lowercase().as_str(),
8282 "percentile_cont" | "percentile_disc" | "mode"
8283 )
8284 );
8285 if ordered_set {
8286 write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
8287 fmt_order_by(f)?;
8288 f.write_str(")")?;
8289 } else {
8290 // `name([DISTINCT ]args [ORDER BY …])` — peel the
8291 // inner call's parens to splice modifiers.
8292 let inner = alloc::format!("{call}");
8293 let body = inner.strip_suffix(')').unwrap_or(&inner);
8294 let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
8295 write!(f, "{head}(")?;
8296 if *distinct {
8297 f.write_str("DISTINCT ")?;
8298 }
8299 write!(f, "{args_part}")?;
8300 if !order_by.is_empty() {
8301 f.write_str(" ORDER BY ")?;
8302 fmt_order_by(f)?;
8303 }
8304 f.write_str(")")?;
8305 }
8306 if let Some(cond) = filter {
8307 write!(f, " FILTER (WHERE {cond})")?;
8308 }
8309 Ok(())
8310 }
8311 Self::IsNull { expr, negated } => {
8312 if *negated {
8313 write!(f, "({expr} IS NOT NULL)")
8314 } else {
8315 write!(f, "({expr} IS NULL)")
8316 }
8317 }
8318 Self::BoolTest {
8319 expr,
8320 value,
8321 negated,
8322 } => {
8323 let word = match value {
8324 Some(true) => "TRUE",
8325 Some(false) => "FALSE",
8326 None => "UNKNOWN",
8327 };
8328 if *negated {
8329 write!(f, "({expr} IS NOT {word})")
8330 } else {
8331 write!(f, "({expr} IS {word})")
8332 }
8333 }
8334 Self::FunctionCall { name, args } => {
8335 write!(f, "{name}(")?;
8336 for (i, a) in args.iter().enumerate() {
8337 if i > 0 {
8338 f.write_str(", ")?;
8339 }
8340 write!(f, "{a}")?;
8341 }
8342 f.write_str(")")
8343 }
8344 Self::Like {
8345 expr,
8346 pattern,
8347 negated,
8348 case_insensitive,
8349 } => {
8350 let op = match (negated, case_insensitive) {
8351 (false, false) => "LIKE",
8352 (true, false) => "NOT LIKE",
8353 (false, true) => "ILIKE",
8354 (true, true) => "NOT ILIKE",
8355 };
8356 write!(f, "({expr} {op} {pattern})")
8357 }
8358 Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
8359 Self::WindowFunction {
8360 name,
8361 args,
8362 partition_by,
8363 order_by,
8364 frame,
8365 null_treatment,
8366 filter,
8367 } => {
8368 write!(f, "{name}(")?;
8369 for (i, a) in args.iter().enumerate() {
8370 if i > 0 {
8371 f.write_str(", ")?;
8372 }
8373 write!(f, "{a}")?;
8374 }
8375 f.write_str(")")?;
8376 // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
8377 // OVER; it round-trips so a window body's Display re-parses.
8378 if let Some(cond) = filter {
8379 write!(f, " FILTER (WHERE {cond})")?;
8380 }
8381 // v7.30.1 (mailrs round-24 class audit) — IGNORE
8382 // NULLS sits between the arg list and OVER; dropping
8383 // it reverted replayed queries to RESPECT NULLS.
8384 if matches!(null_treatment, NullTreatment::Ignore) {
8385 f.write_str(" IGNORE NULLS")?;
8386 }
8387 f.write_str(" OVER (")?;
8388 if !partition_by.is_empty() {
8389 f.write_str("PARTITION BY ")?;
8390 for (i, p) in partition_by.iter().enumerate() {
8391 if i > 0 {
8392 f.write_str(", ")?;
8393 }
8394 write!(f, "{p}")?;
8395 }
8396 }
8397 if !order_by.is_empty() {
8398 if !partition_by.is_empty() {
8399 f.write_str(" ")?;
8400 }
8401 f.write_str("ORDER BY ")?;
8402 for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
8403 if i > 0 {
8404 f.write_str(", ")?;
8405 }
8406 write!(f, "{e}")?;
8407 if *desc {
8408 f.write_str(" DESC")?;
8409 }
8410 match nulls_first {
8411 Some(true) => f.write_str(" NULLS FIRST")?,
8412 Some(false) => f.write_str(" NULLS LAST")?,
8413 None => {}
8414 }
8415 }
8416 }
8417 if let Some(fr) = frame {
8418 if !partition_by.is_empty() || !order_by.is_empty() {
8419 f.write_str(" ")?;
8420 }
8421 let k = match fr.kind {
8422 FrameKind::Rows => "ROWS",
8423 FrameKind::Range => "RANGE",
8424 FrameKind::Groups => "GROUPS",
8425 };
8426 if let Some(end) = &fr.end {
8427 write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
8428 } else {
8429 write!(f, "{k} {}", fr.start)?;
8430 }
8431 }
8432 f.write_str(")")
8433 }
8434 Self::ScalarSubquery(s) => write!(f, "({s})"),
8435 Self::Exists { subquery, negated } => {
8436 if *negated {
8437 write!(f, "NOT EXISTS ({subquery})")
8438 } else {
8439 write!(f, "EXISTS ({subquery})")
8440 }
8441 }
8442 Self::InSubquery {
8443 expr,
8444 subquery,
8445 negated,
8446 } => {
8447 if *negated {
8448 write!(f, "({expr} NOT IN ({subquery}))")
8449 } else {
8450 write!(f, "({expr} IN ({subquery}))")
8451 }
8452 }
8453 Self::RowInSubquery {
8454 row,
8455 subquery,
8456 negated,
8457 } => {
8458 write!(f, "(")?;
8459 for (i, e) in row.iter().enumerate() {
8460 if i > 0 {
8461 write!(f, ", ")?;
8462 }
8463 write!(f, "{e}")?;
8464 }
8465 let kw = if *negated { ") NOT IN (" } else { ") IN (" };
8466 write!(f, "{kw}{subquery})")
8467 }
8468 Self::RowCmpSubquery { row, op, subquery } => {
8469 write!(f, "(")?;
8470 for (i, e) in row.iter().enumerate() {
8471 if i > 0 {
8472 write!(f, ", ")?;
8473 }
8474 write!(f, "{e}")?;
8475 }
8476 write!(f, ") {op} ({subquery})")
8477 }
8478 Self::InList {
8479 expr,
8480 list,
8481 negated,
8482 } => {
8483 let kw = if *negated { " NOT IN (" } else { " IN (" };
8484 write!(f, "({expr}{kw}")?;
8485 for (i, e) in list.iter().enumerate() {
8486 if i > 0 {
8487 f.write_str(", ")?;
8488 }
8489 write!(f, "{e}")?;
8490 }
8491 f.write_str("))")
8492 }
8493 Self::Array(items) => {
8494 f.write_str("ARRAY[")?;
8495 for (i, e) in items.iter().enumerate() {
8496 if i > 0 {
8497 f.write_str(", ")?;
8498 }
8499 write!(f, "{e}")?;
8500 }
8501 f.write_str("]")
8502 }
8503 Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
8504 Self::ArraySlice { target, lo, hi } => {
8505 write!(f, "({target}[")?;
8506 if let Some(l) = lo {
8507 write!(f, "{l}")?;
8508 }
8509 write!(f, ":")?;
8510 if let Some(h) = hi {
8511 write!(f, "{h}")?;
8512 }
8513 write!(f, "])")
8514 }
8515 Self::AnyAll {
8516 expr,
8517 op,
8518 array,
8519 is_any,
8520 } => {
8521 let kw = if *is_any { "ANY" } else { "ALL" };
8522 write!(f, "({expr} {op} {kw}({array}))")
8523 }
8524 Self::Case {
8525 operand,
8526 branches,
8527 else_branch,
8528 } => {
8529 f.write_str("CASE")?;
8530 if let Some(op) = operand {
8531 write!(f, " {op}")?;
8532 }
8533 for (w, t) in branches {
8534 write!(f, " WHEN {w} THEN {t}")?;
8535 }
8536 if let Some(e) = else_branch {
8537 write!(f, " ELSE {e}")?;
8538 }
8539 f.write_str(" END")
8540 }
8541 }
8542 }
8543}
8544
8545/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
8546/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
8547pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
8548 use alloc::string::ToString;
8549 if scale == 0 {
8550 return alloc::format!("{unscaled}");
8551 }
8552 let neg = unscaled < 0;
8553 let digits = alloc::format!("{}", unscaled.unsigned_abs());
8554 let scale = scale as usize;
8555 let (int_part, frac_part) = if digits.len() > scale {
8556 (
8557 digits[..digits.len() - scale].to_string(),
8558 digits[digits.len() - scale..].to_string(),
8559 )
8560 } else {
8561 ("0".to_string(), alloc::format!("{digits:0>scale$}"))
8562 };
8563 alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
8564}
8565
8566/// A single-quoted SQL string, with an embedded quote doubled.
8567fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
8568 f.write_str("'")?;
8569 for c in s.chars() {
8570 if c == '\'' {
8571 f.write_str("''")?;
8572 } else {
8573 write!(f, "{c}")?;
8574 }
8575 }
8576 f.write_str("'")
8577}
8578
8579impl fmt::Display for Literal {
8580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8581 match self {
8582 Self::Integer(n) => write!(f, "{n}"),
8583 Self::Float(x) => {
8584 let s = format!("{x}");
8585 // Default Display for an integral f64 (e.g. 1.0) emits "1",
8586 // which would round-trip back to Integer. Force a dot.
8587 if s.contains('.') || s.contains('e') || s.contains('E') {
8588 f.write_str(&s)
8589 } else {
8590 write!(f, "{s}.0")
8591 }
8592 }
8593 Self::Numeric { unscaled, scale } => {
8594 // Render the exact decimal `unscaled / 10^scale`, preserving
8595 // scale (trailing zeros) — round-trips to the same literal.
8596 f.write_str(&render_exact_decimal(*unscaled, *scale))
8597 }
8598 Self::NumericBig(s) => f.write_str(s),
8599 // Printed exactly as the text form was, so a reader cannot
8600 // tell whether the constant was decoded or not.
8601 Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
8602 Self::String(s) => write_quoted(f, s),
8603 Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
8604 Self::Null => f.write_str("NULL"),
8605 // PG external array form. Display round-trip re-enters
8606 // through the column-typed text coerce, same as pgwire.
8607 Self::TextArray(items) => {
8608 f.write_str("'{")?;
8609 for (i, it) in items.iter().enumerate() {
8610 if i > 0 {
8611 f.write_str(",")?;
8612 }
8613 match it {
8614 None => f.write_str("NULL")?,
8615 Some(s) => {
8616 f.write_str("\"")?;
8617 for c in s.chars() {
8618 match c {
8619 // array-element escapes
8620 '"' | '\\' => write!(f, "\\{c}")?,
8621 // the OUTER wrapper is a SQL string
8622 // literal — embedded quotes must
8623 // double, or the rendered form
8624 // (WAL replay parses it back) is
8625 // invalid SQL
8626 '\'' => f.write_str("''")?,
8627 _ => write!(f, "{c}")?,
8628 }
8629 }
8630 f.write_str("\"")?;
8631 }
8632 }
8633 }
8634 f.write_str("}'")
8635 }
8636 Self::IntArray(items) => {
8637 f.write_str("'{")?;
8638 for (i, it) in items.iter().enumerate() {
8639 if i > 0 {
8640 f.write_str(",")?;
8641 }
8642 match it {
8643 None => f.write_str("NULL")?,
8644 Some(n) => write!(f, "{n}")?,
8645 }
8646 }
8647 f.write_str("}'")
8648 }
8649 Self::BigIntArray(items) => {
8650 f.write_str("'{")?;
8651 for (i, it) in items.iter().enumerate() {
8652 if i > 0 {
8653 f.write_str(",")?;
8654 }
8655 match it {
8656 None => f.write_str("NULL")?,
8657 Some(n) => write!(f, "{n}")?,
8658 }
8659 }
8660 f.write_str("}'")
8661 }
8662 Self::Vector(v) => {
8663 f.write_str("[")?;
8664 for (i, x) in v.iter().enumerate() {
8665 if i > 0 {
8666 f.write_str(", ")?;
8667 }
8668 let s = format!("{x}");
8669 // Mirror Float Display: force a dot so re-parse stays
8670 // numerically literal.
8671 if s.contains('.') || s.contains('e') || s.contains('E') {
8672 f.write_str(&s)?;
8673 } else {
8674 write!(f, "{s}.0")?;
8675 }
8676 }
8677 f.write_str("]")
8678 }
8679 Self::Interval { text, .. } => {
8680 f.write_str("INTERVAL '")?;
8681 for c in text.chars() {
8682 if c == '\'' {
8683 f.write_str("''")?;
8684 } else {
8685 write!(f, "{c}")?;
8686 }
8687 }
8688 f.write_str("'")
8689 }
8690 }
8691 }
8692}
8693
8694impl fmt::Display for BinOp {
8695 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8696 f.write_str(match self {
8697 Self::Or => "OR",
8698 Self::And => "AND",
8699 Self::Eq => "=",
8700 Self::NotEq => "<>",
8701 Self::IsDistinctFrom => "IS DISTINCT FROM",
8702 Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
8703 Self::IntDiv => "DIV",
8704 Self::Lt => "<",
8705 Self::LtEq => "<=",
8706 Self::Gt => ">",
8707 Self::GtEq => ">=",
8708 Self::Add => "+",
8709 Self::Sub => "-",
8710 Self::Mul => "*",
8711 Self::Div => "/",
8712 Self::Mod => "%",
8713 Self::L2Distance => "<->",
8714 Self::GeomParallel => "?||",
8715 Self::OverLeft => "&<",
8716 Self::OverRight => "&>",
8717 Self::GeomPerp => "?-|",
8718 Self::GeomSameAs => "~=",
8719 Self::ClosestPoint => "##",
8720 Self::GeomHoriz => "?-",
8721 Self::InnerProduct => "<#>",
8722 Self::CosineDistance => "<=>",
8723 Self::Concat => "||",
8724 Self::BitOr => "|",
8725 Self::BitAnd => "&",
8726 Self::BitXor => "#",
8727 Self::LogicalXor => "xor",
8728 Self::JsonGet => "->",
8729 Self::JsonGetText => "->>",
8730 Self::JsonGetPath => "#>",
8731 Self::JsonGetPathText => "#>>",
8732 Self::JsonContains => "@>",
8733 Self::JsonPathExists => "@?",
8734 Self::JsonContainedBy => "<@",
8735 Self::JsonKeyExists => "?",
8736 Self::JsonKeysAny => "?|",
8737 Self::JsonKeysAll => "?&",
8738 Self::JsonDeletePath => "#-",
8739 Self::TsMatch => "@@",
8740 Self::InetContainedBy => "<<",
8741 Self::InetContainedByEq => "<<=",
8742 Self::InetContains => ">>",
8743 Self::InetContainsEq => ">>=",
8744 Self::InetOverlap => "&&",
8745 Self::Intersects => "?#",
8746 Self::IsBelow => "<^",
8747 Self::IsAbove => ">^",
8748 Self::PatternLt => "~<~",
8749 Self::PatternLtEq => "~<=~",
8750 Self::PatternGt => "~>~",
8751 Self::PatternGtEq => "~>=~",
8752 })
8753 }
8754}
8755
8756/// Quote `s` as a PG double-quoted identifier when required (keyword,
8757/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
8758/// Otherwise return it as-is. Returns an owned `String` to keep the call site
8759/// uniform.
8760pub(crate) fn quote_ident(s: &str) -> String {
8761 let needs_quote = match s.chars().next() {
8762 None => true,
8763 Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
8764 _ => {
8765 s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
8766 || s.chars().any(|c| c.is_ascii_uppercase())
8767 || is_keyword(s)
8768 }
8769 };
8770 if !needs_quote {
8771 return s.to_string();
8772 }
8773 let mut out = String::with_capacity(s.len() + 2);
8774 out.push('"');
8775 for c in s.chars() {
8776 if c == '"' {
8777 out.push_str("\"\"");
8778 } else {
8779 out.push(c);
8780 }
8781 }
8782 out.push('"');
8783 out
8784}
8785
8786fn is_keyword(s: &str) -> bool {
8787 matches!(
8788 &*s.to_ascii_lowercase(),
8789 "select"
8790 | "from"
8791 | "where"
8792 | "as"
8793 | "null"
8794 | "true"
8795 | "false"
8796 | "and"
8797 | "or"
8798 | "not"
8799 | "create"
8800 | "table"
8801 | "insert"
8802 | "into"
8803 | "values"
8804 | "index"
8805 | "on"
8806 | "begin"
8807 | "commit"
8808 | "rollback"
8809 | "is"
8810 | "between"
8811 | "in"
8812 | "like"
8813 | "group"
8814 | "distinct"
8815 | "union"
8816 | "all"
8817 | "join"
8818 | "inner"
8819 | "left"
8820 | "cross"
8821 | "outer"
8822 | "default"
8823 | "savepoint"
8824 | "release"
8825 | "to"
8826 | "having"
8827 | "show"
8828 | "extract"
8829 | "offset"
8830 | "asc"
8831 | "desc"
8832 | "interval"
8833 )
8834}
8835
8836#[cfg(test)]
8837mod tests {
8838 use super::*;
8839 use alloc::vec;
8840
8841 #[test]
8842 fn integer_literal_renders_without_dot() {
8843 assert_eq!(Literal::Integer(42).to_string(), "42");
8844 }
8845
8846 #[test]
8847 fn integral_float_keeps_dot() {
8848 assert_eq!(Literal::Float(1.0).to_string(), "1.0");
8849 assert_eq!(Literal::Float(1.5).to_string(), "1.5");
8850 assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
8851 }
8852
8853 #[test]
8854 fn string_literal_doubles_quote() {
8855 assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
8856 }
8857
8858 #[test]
8859 fn bool_and_null_render_uppercase() {
8860 assert_eq!(Literal::Bool(true).to_string(), "TRUE");
8861 assert_eq!(Literal::Bool(false).to_string(), "FALSE");
8862 assert_eq!(Literal::Null.to_string(), "NULL");
8863 }
8864
8865 #[test]
8866 fn binary_op_always_parenthesised() {
8867 let e = Expr::Binary {
8868 lhs: Box::new(Expr::Literal(Literal::Integer(1))),
8869 op: BinOp::Add,
8870 rhs: Box::new(Expr::Literal(Literal::Integer(2))),
8871 };
8872 assert_eq!(e.to_string(), "(1 + 2)");
8873 }
8874
8875 #[test]
8876 fn select_star_from_table() {
8877 let s = SelectStatement {
8878 locking: None,
8879 items: vec![SelectItem::Wildcard],
8880 from: Some(FromClause {
8881 primary: TableRef {
8882 name: "users".into(),
8883 alias: None,
8884 only: false,
8885 as_of_segment: None,
8886 unnest_expr: None,
8887 unnest_column_aliases: Vec::new(),
8888 with_ordinality: false,
8889 generate_series_args: None,
8890 lateral_subquery: None,
8891 jsonb_each_text_arg: None,
8892 table_fn_call: None,
8893 rows_from: None,
8894 json_table: None,
8895 scalar_fn_item: false,
8896 },
8897 joins: vec![],
8898 }),
8899 where_: None,
8900 group_by: None,
8901 group_by_all: false,
8902 having: None,
8903 unions: vec![],
8904 order_by: Vec::new(),
8905 limit: None,
8906 offset: None,
8907 limit_with_ties: false,
8908 window_check_exprs: Vec::new(),
8909 distinct: false,
8910 distinct_on: Vec::new(),
8911 ctes: vec![],
8912 };
8913 assert_eq!(s.to_string(), "SELECT * FROM users");
8914 }
8915
8916 #[test]
8917 fn quote_ident_for_uppercase_and_keyword() {
8918 assert_eq!(quote_ident("foo"), "foo");
8919 assert_eq!(quote_ident("Foo"), "\"Foo\"");
8920 assert_eq!(quote_ident("select"), "\"select\"");
8921 assert_eq!(quote_ident(""), "\"\"");
8922 assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
8923 }
8924}