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 /// v7.38.19 — the database's name, so `pg_database` can list one
239 /// that was created and can be connected to. It was thrown away
240 /// with the rest of the statement.
241 name: Option<String>,
242 },
243 /// v7.39 (round 696) — statements SPG performs nothing for, but whose
244 /// OPERAND PG validates before performing nothing either.
245 ///
246 /// All four used to be consumed whole by `is_dump_noise_statement`,
247 /// which meant `LOCK TABLE nosuch` and `DROP OWNED BY nosuchrole` were
248 /// ACCEPTED where PG18 errors. Accepting a statement that names
249 /// something that does not exist is the F29 shape: the caller is told
250 /// their intent was understood when the object it referred to is not
251 /// there.
252 ///
253 /// They share one variant because they share one rule — resolve the
254 /// name, refuse if absent, otherwise no-op — and four variants would be
255 /// four places for that rule to drift.
256 /// v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS] name(argtypes)[, …]`.
257 /// Consumed whole by the dump-noise list before, so `DROP AGGREGATE
258 /// nosuch(int)` reported success. PG validates every named aggregate's
259 /// EXISTENCE first (measured: a list with one unknown fails on the
260 /// unknown even when an earlier entry exists), renders the signature
261 /// with canonical type names (`int` → `integer`), and refuses to drop a
262 /// built-in (`cannot drop function sum(integer) because it is required
263 /// by the database system`). Every SPG aggregate is a built-in, so the
264 /// outcome is one of those two errors — or the IF EXISTS no-op.
265 ///
266 /// `args` holds the argument type names as written; `None` is the
267 /// `(*)` spelling.
268 DropAggregate {
269 if_exists: bool,
270 items: Vec<(String, Option<Vec<String>>)>,
271 },
272 /// v7.39 (round 750) — `ALTER ROLE|USER <name> … PASSWORD 'x' |
273 /// PASSWORD NULL`. The one attribute of the no-op family with a
274 /// SECURITY consequence: it was silently dropped (ledgered r710),
275 /// so a rotated credential never rotated. `None` = PASSWORD NULL
276 /// (the role keeps existing but can no longer password-auth).
277 AlterRolePassword {
278 name: String,
279 password: Option<String>,
280 },
281 ValidateOnly {
282 kind: ValidateOnlyKind,
283 /// The names the statement referred to. Empty means the form names
284 /// nothing (`SECURITY LABEL`, whose refusal is unconditional).
285 names: Vec<String>,
286 },
287
288 /// v7.39 (round 547) — `ALTER ROLE … SET/RESET` and
289 /// `ALTER DATABASE … SET/RESET`: the GUC defaults a session picks up
290 /// when it starts. Both used to land in the pg_dump no-op tail, so
291 /// the statement reported success and changed nothing.
292 ///
293 /// `database` / `role` are `None` for PG's oid 0 — `ALTER ROLE ALL`
294 /// sets both to None. `param` is `None` for RESET ALL. `value` is
295 /// `None` for RESET of one parameter.
296 SetDbRoleSetting(Box<SetDbRoleSettingStatement>),
297 /// v7.39 (round 288) — `SET CONSTRAINTS { ALL | <name>… }
298 /// { DEFERRED | IMMEDIATE }`. `deferred` carries the timing; the
299 /// name list is not yet honoured (ALL is what pg_dump emits and
300 /// what a circular-FK restore needs), so a named form applies to
301 /// all deferrable constraints too rather than silently doing
302 /// nothing.
303 /// v7.39 (round 308) — `SET CONSTRAINTS { ALL | name [, …] }
304 /// { DEFERRED | IMMEDIATE }`. An empty `names` is the ALL form;
305 /// otherwise the timing applies only to the constraints listed.
306 SetConstraints {
307 names: Vec<String>,
308 deferred: bool,
309 },
310
311 /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
312 /// [CASCADE | RESTRICT]`. Engine removes the matching tables
313 /// (each one) from the catalog; IF EXISTS makes the drop
314 /// idempotent. CASCADE / RESTRICT trailers parsed silently
315 /// (SPG always cascades index drops on table drop).
316 DropTable {
317 names: Vec<String>,
318 if_exists: bool,
319 },
320 /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
321 /// matching index across whichever table holds it.
322 DropIndex {
323 name: String,
324 if_exists: bool,
325 },
326 /// v7.14.0 — empty / comment-only statement. The lexer strips
327 /// `--` line comments and `/* … */` block comments (including
328 /// the MySQL conditional `/*!NNNNN … */` form) before the
329 /// parser ever sees them; a SQL chunk that contains nothing
330 /// else lands here. Engine returns CommandOk no-op so
331 /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
332 /// wrapped in conditional comments, etc.) load cleanly.
333 /// v7.39 (round 277) — SQL-level `PREPARE <name> [(type, …)] AS
334 /// <stmt>`. Session-scoped; the body keeps its `$N` placeholders
335 /// and is substituted at EXECUTE time.
336 Prepare {
337 name: String,
338 /// Declared parameter type names, in order. Empty when the
339 /// `(type, …)` list was omitted (PG infers them).
340 param_types: Vec<String>,
341 body: alloc::boxed::Box<Statement>,
342 /// The statement's own source text, which
343 /// `pg_prepared_statements.statement` reports verbatim.
344 source: String,
345 },
346 /// v7.39 (round 277) — `EXECUTE <name> [(arg, …)]`.
347 Execute {
348 name: String,
349 args: Vec<Expr>,
350 },
351 /// v7.39 (round 277) — `DEALLOCATE {<name> | ALL}`. `None` = ALL.
352 Deallocate(Option<String>),
353 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
354 /// [(kind, …)] ON <col>, … FROM <table>`. SPG records the object so
355 /// dumps restore and reflection is honest; the planner does not
356 /// consult it yet.
357 CreateStatistics {
358 name: String,
359 if_not_exists: bool,
360 /// Requested kinds as PG's single letters (`d` ndistinct,
361 /// `f` dependencies, `m` mcv). Empty = PG's default set.
362 kinds: Vec<String>,
363 columns: Vec<String>,
364 table: String,
365 },
366 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
367 DropStatistics {
368 name: String,
369 if_exists: bool,
370 },
371 /// v7.39 (round 278) — `CALL <proc>(…)`. Parses; the engine
372 /// reports that the procedure does not exist, because SPG has no
373 /// procedure catalog. Carried as a statement rather than raised at
374 /// parse time so the failure is a missing OBJECT (42883), not a
375 /// syntax error.
376 Call(String),
377 /// v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'`. Same shape:
378 /// 2PC is unavailable, which PG itself reports when
379 /// `max_prepared_transactions` is 0.
380 PrepareTransaction(String),
381 Empty,
382 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE]
383 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. The
384 /// canonical driver path for streaming large result sets (psycopg2
385 /// named cursors, JDBC setFetchSize).
386 DeclareCursor {
387 name: String,
388 /// `None` = neither keyword (PG default: backward allowed when the
389 /// plan supports it — always, for SPG's materialized cursors);
390 /// `Some(true)` = SCROLL; `Some(false)` = NO SCROLL (backward
391 /// fetch errors 55000).
392 scroll: Option<bool>,
393 /// `WITH HOLD` — survives the creating transaction's COMMIT.
394 hold: bool,
395 query: Box<Statement>,
396 },
397 /// v7.39 (round 218) — `FETCH [<direction>] [FROM|IN] <name>`.
398 FetchCursor {
399 name: String,
400 direction: CursorDirection,
401 },
402 /// v7.39 (round 218) — `MOVE [<direction>] [FROM|IN] <name>`: FETCH
403 /// without returning rows; the command tag carries the move count.
404 MoveCursor {
405 name: String,
406 direction: CursorDirection,
407 },
408 /// v7.39 (round 218) — `CLOSE <name>` / `CLOSE ALL` (`None` = ALL).
409 CloseCursor {
410 name: Option<String>,
411 },
412 /// v7.39 (round 222) — `LISTEN <channel>`: subscribe this session to
413 /// async notifications on the channel.
414 Listen(String),
415 /// v7.39 (round 222) — `NOTIFY <channel> [, '<payload>']`. Delivered at
416 /// COMMIT (PG semantics: transactional, deduplicated within the tx);
417 /// immediately under autocommit.
418 Notify {
419 channel: String,
420 payload: Option<String>,
421 },
422 /// v7.39 (round 222) — `UNLISTEN <channel>` / `UNLISTEN *` (`None` = *).
423 Unlisten(Option<String>),
424 /// `COPY table [(cols)] TO STDOUT` — the engine renders the
425 /// visible rows in COPY text format (tab-separated, `\N`
426 /// nulls, backslash escapes) as a single-text-column result
427 /// set; the wire layer streams CopyData from it.
428 CopyTo {
429 table: String,
430 columns: Option<Vec<String>>,
431 /// v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT`: an
432 /// arbitrary SELECT/VALUES/CTE (a whole [`Statement`], so set-ops and
433 /// VALUES ride through unchanged) whose result set is streamed in COPY
434 /// format. `Some` overrides `table`/`columns` (which are empty then);
435 /// `None` is the classic `COPY <table> …` shape.
436 query: Option<Box<Statement>>,
437 /// v7.37.x — `WITH (FORMAT csv, HEADER, DELIMITER, NULL, QUOTE)`
438 /// and the legacy `WITH CSV HEADER …` spelling. Default =
439 /// text format, no header (bare `COPY … TO STDOUT`).
440 options: CopyOptions,
441 },
442 /// v7.39 (round 249) — `COPY table [(cols)] FROM '<path>' [(opts)]`.
443 /// The engine is no_std and cannot read the file itself: the host
444 /// (embedded / server / tooling) reads the path and hands the bytes to
445 /// `Engine::copy_from_buffer`. Dispatching this statement straight to
446 /// the engine reports that contract.
447 CopyFromFile {
448 table: String,
449 columns: Option<Vec<String>>,
450 path: String,
451 options: CopyOptions,
452 },
453 /// v7.39 (round 249/252) — `COPY <table> [(cols)] TO '<file>'` (and
454 /// the `COPY (<query>) TO '<file>'` form). The engine is no_std and
455 /// cannot write the file itself: the host renders the payload via
456 /// `Engine::copy_to_buffer` and writes the path.
457 CopyToFile {
458 table: String,
459 columns: Option<Vec<String>>,
460 query: Option<Box<Statement>>,
461 path: String,
462 options: CopyOptions,
463 },
464 Select(SelectStatement),
465 CreateTable(CreateTableStatement),
466 /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
467 /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
468 /// no-op so PG dumps that include extension declarations
469 /// (notably `pgvector`) load against SPG without splitting
470 /// init scripts. mailrs migration follow-up F3.
471 CreateExtension(String),
472 /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
473 /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
474 /// the engine executes it at top level (mailrs round-10
475 /// A.2). Pre-v7.16.2 the parser discarded the body and the
476 /// engine returned CommandOk — a SEV-1 silent no-op that
477 /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
478 /// $$` idempotent migrations into invisible no-ops.
479 DoBlock(PlPgSqlBlock),
480 CreateIndex(CreateIndexStatement),
481 Insert(InsertStatement),
482 /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
483 Update(UpdateStatement),
484 /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
485 Delete(DeleteStatement),
486 /// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ `MERGE` statement.
487 /// `MERGE INTO target [alias] USING source [alias] ON cond
488 /// WHEN MATCHED [AND cond] THEN { UPDATE SET … | DELETE | DO NOTHING }
489 /// WHEN NOT MATCHED [AND cond] THEN { INSERT (cols) VALUES (vals) | DO NOTHING }
490 /// [WHEN …]`. SPG v7.17 supports table-based source (subquery
491 /// source is a follow-up); BY SOURCE / BY TARGET and RETURNING
492 /// are also follow-ups.
493 Merge(MergeStatement),
494 /// v7.39 (round 169) — `VACUUM [(opts)] [FULL|FREEZE|VERBOSE|ANALYZE]
495 /// [<table>]`. Was a parse-time no-op from the pre-MVCC era; with the
496 /// in-place MVCC gate ON, tombstoned versions are REAL bloat and a
497 /// customer's manual VACUUM must actually reclaim. `analyze` mirrors
498 /// the `VACUUM ANALYZE` spelling.
499 Vacuum {
500 table: Option<String>,
501 analyze: bool,
502 },
503 /// `BEGIN` / `START TRANSACTION` — with an optional explicit
504 /// `ISOLATION LEVEL …` mode (`None` = use the session default). PG
505 /// applies the level for the duration of this transaction only.
506 Begin(TransactionModes),
507 Commit,
508 Rollback,
509 /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
510 /// stack so a later `ROLLBACK TO <name>` can undo just the work
511 /// since this point.
512 Savepoint(String),
513 /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
514 /// named savepoint and discard later savepoints. Does not end the
515 /// transaction.
516 RollbackToSavepoint(String),
517 /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
518 /// rolling back. Keeps the work done since then.
519 ReleaseSavepoint(String),
520 /// `SHOW TABLES` — return the list of tables in the catalog.
521 ShowTables,
522 /// v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES` /
523 /// `SHOW SCHEMAS`. SPG is single-database; the executor
524 /// returns the canonical MySQL set so the mysql / MariaDB
525 /// client populates its database selector.
526 ShowDatabases,
527 /// v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE TABLE <t>`
528 /// returns a 2-column row `(Table, "Create Table")` carrying
529 /// the synthesized DDL. mysqldump emits this for every
530 /// table at scrape time.
531 ShowCreateTable(String),
532 /// v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES FROM <t>`
533 /// (also `SHOW INDEX`, `SHOW KEYS`).
534 ShowIndexes(String),
535 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS`.
536 ShowStatus,
537 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW VARIABLES`.
538 ShowVariables,
539 /// r1067 — MySQL `SHOW VARIABLES LIKE 'pattern'` (sysbench-tpcc
540 /// probes isolation with it at connect).
541 ShowVariablesLike(String),
542 /// v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
543 ShowProcesslist,
544 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
545 /// pgbouncer sends `DISCARD ALL` between pooled client sessions to make
546 /// the connection look brand new to the next client; it used to be
547 /// swallowed as dump noise, so nothing was discarded.
548 Discard(DiscardTarget),
549 /// v7.39 (round 318, V51) — MySQL `KILL [CONNECTION | QUERY] <expr>`.
550 /// The id is an expression because MariaDB accepts one
551 /// (`KILL connection_id()` is the documented way to drop your own
552 /// connection). `query_only` is the `QUERY` form: stop the target's
553 /// running statement but leave it connected.
554 Kill {
555 query_only: bool,
556 id: Box<Expr>,
557 },
558 /// `SHOW COLUMNS FROM <table>` — return one row per column with
559 /// its declared name / type / nullability.
560 ShowColumns(String),
561 /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
562 /// Role is optional; defaults to `readonly` when omitted.
563 CreateUser(CreateUserStatement),
564 /// `DROP USER 'name'` (v4.1). v7.39 (read01 round 58) — `IF EXISTS` is
565 /// carried through: PG skips with a NOTICE rather than erroring.
566 DropUser {
567 name: String,
568 if_exists: bool,
569 },
570 /// v7.39 (RLS) — `SET ROLE { name | NONE | DEFAULT }` / `RESET ROLE`.
571 /// `Some(name)` switches the session's effective role (drives
572 /// `current_user` and RLS enforcement); `None` resets to the login
573 /// identity (the Admin superuser).
574 SetRole(Option<String>),
575 /// v7.39 (read01 round 57) — `GRANT <privs> ON <object> TO <roles>`.
576 Grant(GrantStatement),
577 /// v7.39 (read01 round 57) — `REVOKE [GRANT OPTION FOR] <privs> ON
578 /// <object> FROM <roles>`.
579 Revoke(GrantStatement),
580 /// v7.39 (RLS) — `CREATE POLICY name ON table …`.
581 CreatePolicy(CreatePolicyStatement),
582 /// v7.39 (RLS) — `ALTER POLICY name ON table …`.
583 AlterPolicy(AlterPolicyStatement),
584 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
585 DropPolicy(DropPolicyStatement),
586 /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
587 ShowUsers,
588 /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
589 /// single-column text table describing the rewritten plan tree
590 /// for `inner`. `analyze` triggers an actual exec to attach
591 /// observed row counts and elapsed micros to each node.
592 Explain(ExplainStatement),
593 /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
594 /// Synchronous rebuild of an NSW index. With the optional
595 /// encoding clause, every stored cell at the indexed column is
596 /// also re-encoded through `coerce_value` before the new graph
597 /// builds.
598 AlterIndex(AlterIndexStatement),
599 /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
600 /// The only setting in v6.7.2 is `hot_tier_bytes`, which
601 /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
602 /// for the named table.
603 AlterTable(AlterTableStatement),
604 /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
605 /// The catalog row lives in `spg_publications`. Publisher-side
606 /// WAL filtering arrives in v6.1.5.
607 CreatePublication(CreatePublicationStatement),
608 /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
609 /// no-op when the publication does not exist.
610 DropPublication {
611 name: String,
612 /// v7.39 (round 754, F31-B4) — `IF EXISTS` quietly skips a
613 /// missing publication; the bare form refuses with PG's
614 /// sentence (PG18-measured — the old "silent no-op" note on
615 /// the executor was wrong).
616 if_exists: bool,
617 },
618 /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
619 /// publication ordered by name with `(name, scope_summary,
620 /// table_count)` columns. The scope summary is the human-
621 /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
622 /// TABLES EXCEPT …`; `table_count` is `NULL` for the
623 /// `AllTables` scope and the table-list length otherwise.
624 ShowPublications,
625 /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
626 /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
627 /// in `spg_subscriptions`; when the subscription is
628 /// `enabled = true` (default) the server spawns a
629 /// background worker that connects to `conn` and drains the
630 /// requested publication(s) into the local engine.
631 CreateSubscription(CreateSubscriptionStatement),
632 /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
633 /// PUBLICATION, silent no-op when absent. Stops the
634 /// associated worker thread before removing the row.
635 DropSubscription {
636 name: String,
637 /// v7.39 (round 754, F31-B4) — same contract as
638 /// [`Statement::DropPublication`].
639 if_exists: bool,
640 },
641 /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
642 /// subscription ordered by name with `(name, conn_str,
643 /// publications, enabled, last_received_pos)`.
644 ShowSubscriptions,
645 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
646 /// Blocks until the local server's apply position reaches
647 /// `<pos>` or `<ms>` elapses. Server-layer command: the
648 /// engine refuses it (`EngineError::Unsupported`) since
649 /// `lag_state` lives in `spg-server`'s `ServerState`.
650 WaitForWalPosition {
651 pos: u64,
652 /// `None` → wait forever; `Some(ms)` → return after `ms`
653 /// milliseconds even if the target isn't reached.
654 timeout_ms: Option<u64>,
655 },
656 /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
657 /// table; `ANALYZE <name>` re-stats just one. Populates
658 /// `spg_statistic` with per-column null_frac + n_distinct +
659 /// 100-bucket equi-depth histogram.
660 Analyze(Option<String>),
661 /// v7.39 (round 535) — `REINDEX { INDEX | TABLE | SCHEMA | DATABASE
662 /// | SYSTEM } [CONCURRENTLY] <name>` and `CLUSTER [VERBOSE]
663 /// [<table> [USING <index>]]`.
664 ///
665 /// SPG has neither index bloat nor a clustering order to rebuild, so
666 /// the work is a no-op — but PG VALIDATES the target, and both were
667 /// swallowed at parse time, so `REINDEX TABLE typo` reported success.
668 /// The name is carried now so the engine can say what PG says.
669 Maintain {
670 kind: MaintainKind,
671 /// `REINDEX … CONCURRENTLY`. Carried for the same reason as
672 /// [`CreateIndexStatement::concurrently`]: PG bars the
673 /// CONCURRENTLY form inside a transaction block and allows the
674 /// plain one.
675 concurrently: bool,
676 /// `None` for the whole-database forms, which name nothing.
677 target: Option<String>,
678 },
679 /// v7.37.17 (17.6 sibling) — `TRUNCATE [TABLE] [ONLY] <name>
680 /// [, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE |
681 /// RESTRICT]`. Clears every row from each named table. SPG's
682 /// SEQUENCE identity is per-table; RESTART IDENTITY reinitializes
683 /// the associated sequence to its starting value. CASCADE
684 /// currently walks direct FK-referring tables and truncates
685 /// them too (PG's semantics). The ONLY modifier (skip partitions)
686 /// and RESTRICT (default) are accepted with no effect since
687 /// SPG's declarative partitions are always truncated together.
688 Truncate {
689 tables: Vec<String>,
690 restart_identity: bool,
691 cascade: bool,
692 /// v7.39 (round 647) — `TRUNCATE ONLY t`. Absorbed as a no-op
693 /// since v7.14 on the reasoning that SPG's children are separate
694 /// relations a truncate does not descend into. Same reasoning
695 /// round 621 applied to `FROM ONLY`, and it stopped being true
696 /// for the same reason: measured, `TRUNCATE <inheritance parent>`
697 /// leaves the children's rows where PG empties them, and
698 /// `TRUNCATE ONLY <partitioned parent>` is silently accepted
699 /// where PG refuses it outright.
700 only: bool,
701 },
702 /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
703 /// BTree-cold indices and merges small cold-tier segments
704 /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
705 /// 4 MiB) into a single larger segment per (table, index).
706 /// `WHERE` predicate filtering on which tables to compact is
707 /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
708 /// v6.7.3 only supports the bare form.
709 CompactColdSegments,
710 /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
711 /// parameter on the engine; v7.12.1 honours
712 /// `default_text_search_config` (consumed by `to_tsvector` /
713 /// `plainto_tsquery` family when called without an explicit
714 /// config arg). All other names are accepted as a no-op so PG
715 /// dumps with `SET client_encoding`, `SET search_path` etc.
716 /// load cleanly.
717 SetParameter {
718 name: String,
719 value: SetValue,
720 /// v7.38 (read01 P3.19) — `SET LOCAL` scopes the change to the
721 /// current transaction; the engine saves the prior value and
722 /// restores it at COMMIT / ROLLBACK. Plain `SET` (and `SET
723 /// SESSION`) leave this false and persist for the session.
724 local: bool,
725 },
726 /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
727 /// multi-assignment (mysqldump preamble uses
728 /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
729 /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
730 /// source order. Pairs whose LHS is a MySQL session/user
731 /// variable (`@VAR` / `@@VAR`) are recorded with the raw
732 /// name so the engine can ignore them; pairs whose LHS is
733 /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
734 /// go through the regular `set_session_param` path.
735 SetParameterList(Vec<(String, SetValue)>),
736 /// v7.39 (round 430) — MySQL's USER-defined variables:
737 /// `SET @x = 5, @s := CONCAT('a','b')`. Distinct from
738 /// [`Self::SetParameter`] (a `@@`-style engine/session setting) in
739 /// every way that matters: the value is an arbitrary EXPRESSION, the
740 /// name lives in its own per-session namespace, and reading an unset
741 /// one answers NULL rather than raising. `:=` and `=` are the same
742 /// assignment here.
743 ///
744 /// Before this the parser stripped every `@`, so `@x` and `@@x` were
745 /// the same node: `SET @x = 5` silently landed in the session-parameter
746 /// store where nothing could read it back, and `SELECT @x` failed with
747 /// "Unknown system variable".
748 /// v7.39 (round 554) — `SET @a = …, SETTING = …`.
749 ///
750 /// `settings` is the trailing half a mysqldump preamble writes:
751 /// `SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'`
752 /// saves a value and changes it in one statement. The parser used
753 /// to refuse the mixture outright, so no mysqldump could be
754 /// restored past its preamble.
755 SetUserVars(Vec<(String, Expr)>, Vec<(String, Expr)>),
756 /// v7.38 轴 4 — `SET [SESSION] TRANSACTION ISOLATION LEVEL …`
757 /// (plus optional READ ONLY / READ WRITE / DEFERRABLE clauses
758 /// silently accepted). PG-standard surface for picking an
759 /// isolation level. Engine tracks the value on
760 /// `Engine::current_isolation_level()`; actual MVCC / SSI
761 /// semantics implementation lands separately. PG itself maps
762 /// READ UNCOMMITTED to READ COMMITTED; SPG mirrors that —
763 /// effectively every level reads as READ COMMITTED in v7.37.8.
764 SetTransaction {
765 modes: TransactionModes,
766 },
767 /// v7.38 轴 4 — `SHOW <param>` returns a 1-column 1-row result
768 /// with the parameter's current value as TEXT. Today the only
769 /// recognised param is `transaction_isolation`; further
770 /// surfaces (`search_path`, `application_name`, …) land as the
771 /// session-parameter inventory grows.
772 ShowParameter(String),
773 /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
774 /// to its default. No-op for parameters SPG does not track.
775 ResetParameter(Option<String>),
776 /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
777 /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
778 /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
779 /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
780 /// languages parse but error at exec time with a clear
781 /// unsupported message.
782 CreateFunction(CreateFunctionStatement),
783 /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
784 /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
785 /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
786 /// triggers and column-list / WHEN clauses are out of scope
787 /// for v7.12.4.
788 CreateTrigger(CreateTriggerStatement),
789 /// v7.39 (round 139) — `CREATE RULE name AS ON event TO table [WHERE cond]
790 /// DO [ALSO|INSTEAD] { NOTHING | command }` query-rewrite rule.
791 CreateRule(CreateRuleStatement),
792 /// v7.39 (round 139) — `DROP RULE [IF EXISTS] name ON table`.
793 DropRule {
794 name: String,
795 table: String,
796 if_exists: bool,
797 },
798 /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
799 /// no-op when missing if `IF EXISTS` is set.
800 DropTrigger {
801 name: String,
802 table: String,
803 if_exists: bool,
804 },
805 /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
806 /// DROP TRIGGER but global (no table scope).
807 DropFunction {
808 name: String,
809 /// v7.39 (read01 round 62) — the argument TYPES, when the statement gave
810 /// them: `DROP FUNCTION f(int)` drops that overload only. `None` = no
811 /// argument list, which PG accepts only when the name is unambiguous.
812 args: Option<Vec<String>>,
813 if_exists: bool,
814 },
815 /// v7.17.0 — `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] name
816 /// [AS data_type]
817 /// [INCREMENT [BY] n]
818 /// [MINVALUE n | NO MINVALUE]
819 /// [MAXVALUE n | NO MAXVALUE]
820 /// [START [WITH] n]
821 /// [CACHE n]
822 /// [[NO] CYCLE]
823 /// [OWNED BY {table.col | NONE}]`.
824 /// Closes the round-7+ silent-no-op SEQUENCE story so pg_dump
825 /// emits + nextval/currval/setval downstream all work.
826 CreateSequence(CreateSequenceStatement),
827 /// v7.17.0 — `ALTER SEQUENCE [IF EXISTS] name <options>` with
828 /// the same option grammar as CREATE SEQUENCE, plus
829 /// `RESTART [WITH n]` and `OWNED BY ...` re-attach.
830 AlterSequence(AlterSequenceStatement),
831 /// v7.17.0 — `DROP SEQUENCE [IF EXISTS] name [, name…]
832 /// [CASCADE | RESTRICT]`. CASCADE / RESTRICT trailers parsed
833 /// silently (no FK on sequences).
834 DropSequence {
835 names: Vec<String>,
836 if_exists: bool,
837 },
838 /// v7.17.0 Phase 1.2 — `CREATE [OR REPLACE] [TEMPORARY] VIEW
839 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …>`. Closes the
840 /// silent-no-op VIEW story from the v7.17 customer-readiness
841 /// audit: pre-v7.17 SPG parsed CREATE VIEW as Statement::Empty
842 /// so any downstream `SELECT FROM v` errored with table-not-
843 /// found. The view body is stored verbatim; SELECT FROM <v>
844 /// rewrites at exec-time by prepending the view body as a
845 /// synthetic CTE.
846 CreateView(CreateViewStatement),
847 /// v7.17.0 Phase 1.2 — `DROP VIEW [IF EXISTS] name [, name…]
848 /// [CASCADE | RESTRICT]`. Removes the matching view from the
849 /// catalog; CASCADE/RESTRICT parsed silently.
850 DropView {
851 names: Vec<String>,
852 if_exists: bool,
853 },
854 /// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW [IF NOT
855 /// EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
856 /// Closes the silent-no-op MATERIALIZED VIEW story. Storage
857 /// model: the materialised result lives as a regular table
858 /// with the matching name + a parallel
859 /// `materialized_views` registry mapping name → body source
860 /// (used by REFRESH).
861 CreateMaterializedView(CreateMaterializedViewStatement),
862 /// v7.17.0 Phase 1.3 — `REFRESH MATERIALIZED VIEW name [WITH
863 /// [NO] DATA]`. Re-runs the stored body and replaces the
864 /// cached rows. `WITH NO DATA` truncates without re-running.
865 RefreshMaterializedView {
866 name: String,
867 with_data: bool,
868 },
869 /// v7.17.0 Phase 1.3 — `DROP MATERIALIZED VIEW [IF EXISTS]
870 /// name [, name…] [CASCADE | RESTRICT]`. Drops both the
871 /// backing table and the source registry entry.
872 DropMaterializedView {
873 names: Vec<String>,
874 if_exists: bool,
875 },
876 /// v7.17.0 Phase 1.4 — `CREATE TYPE name AS ENUM ('a', 'b',
877 /// …)`. Closes the silent-no-op CREATE TYPE story so PG
878 /// dumps that declare enum types load with real constraints
879 /// instead of becoming free-form TEXT. Future kinds
880 /// (composite / range / domain) extend the inner `kind`
881 /// enum.
882 CreateType(CreateTypeStatement),
883 /// v7.37 D.55 — `ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
884 /// [{BEFORE | AFTER} 'existing']`. Extends an enum's label list so
885 /// enum evolution stops being a silent no-op. `position` is
886 /// `Some((is_before, anchor))`.
887 /// v7.39 (read01 round 49) — `ALTER TYPE t RENAME VALUE 'old' TO 'new'`.
888 /// Used to be swallowed by the ALTER TYPE no-op tail, so the rename was
889 /// accepted and silently ignored.
890 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
891 /// Used to be swallowed as dump noise, so a comment was accepted and lost
892 /// (and obj_description / col_description always returned NULL).
893 /// `kind` is lowercase ("table" / "column" / "index" / …); for a column
894 /// `name` is the dotted `table.column`. `comment: None` = `IS NULL` = remove.
895 CommentOn {
896 kind: String,
897 name: String,
898 comment: Option<String>,
899 },
900 AlterTypeRenameValue {
901 type_name: String,
902 old: String,
903 new: String,
904 },
905 AlterTypeAddValue {
906 type_name: String,
907 label: String,
908 if_not_exists: bool,
909 position: Option<(bool, String)>,
910 },
911 /// v7.17.0 Phase 1.4 — `DROP TYPE [IF EXISTS] name [, name…]
912 /// [CASCADE | RESTRICT]`. Removes the matching enum/domain
913 /// from the catalog.
914 DropType {
915 names: Vec<String>,
916 if_exists: bool,
917 },
918 /// v7.17.0 Phase 1.5 — `CREATE DOMAIN name AS base_type
919 /// [DEFAULT expr] [NOT NULL | NULL] [CHECK (expr)]*`.
920 /// A DOMAIN is a named CHECK-constrained alias over a built-
921 /// in type. The CHECK + NOT NULL + DEFAULT clauses apply to
922 /// every column declared with the domain. Closes the
923 /// silent-no-op CREATE DOMAIN story so PG dumps that ship
924 /// validated identifier types (email, positive_int, …) keep
925 /// their guarantees.
926 CreateDomain(CreateDomainStatement),
927 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`. Every form was
928 /// previously swallowed by the catch-all DDL arm: the statement
929 /// reported success and did nothing, so a migration that dropped a
930 /// constraint kept rejecting the data it had just been told to
931 /// accept.
932 AlterDomain {
933 name: String,
934 action: AlterDomainAction,
935 },
936 /// v7.17.0 Phase 1.5 — `DROP DOMAIN [IF EXISTS] name
937 /// [, name…] [CASCADE | RESTRICT]`. Removes the matching
938 /// domain from the catalog.
939 DropDomain {
940 names: Vec<String>,
941 if_exists: bool,
942 },
943 /// v7.17.0 Phase 1.6 — `CREATE SCHEMA [IF NOT EXISTS]
944 /// name [AUTHORIZATION user]`. SPG is single-database;
945 /// schemas are tracked as a namespace registry so pg_dump
946 /// multi-schema declarations land cleanly and `SELECT *
947 /// FROM information_schema.schemata` returns real entries.
948 /// Schema-qualified `schema.table` references still strip
949 /// the prefix at lookup time per PG (schemas are not
950 /// isolation boundaries in v7.17 — see project-next-docket
951 /// for the v7.18+ isolation tracking).
952 CreateSchema {
953 name: String,
954 if_not_exists: bool,
955 },
956 /// v7.17.0 Phase 1.6 — `DROP SCHEMA [IF EXISTS] name
957 /// [, name…] [CASCADE | RESTRICT]`. Removes the schema
958 /// from the registry; built-in `public` / `pg_catalog` /
959 /// `information_schema` cannot be dropped.
960 DropSchema {
961 names: Vec<String>,
962 if_exists: bool,
963 },
964}
965
966/// v7.39 (round 260) — the `ALTER DOMAIN` actions SPG implements.
967#[derive(Debug, Clone, PartialEq)]
968pub enum AlterDomainAction {
969 AddConstraint { name: Option<String>, check: Expr },
970 DropConstraint { name: String, if_exists: bool },
971 SetDefault(Expr),
972 DropDefault,
973 SetNotNull,
974 DropNotNull,
975 RenameTo(String),
976}
977
978/// v7.17.0 Phase 1.5 — `CREATE DOMAIN` AST.
979#[derive(Debug, Clone, PartialEq)]
980pub struct CreateDomainStatement {
981 pub name: String,
982 /// Base type for the domain (one of the built-in
983 /// `ColumnTypeName` variants).
984 pub base_type: ColumnTypeName,
985 /// v7.39 (round 259) — `CREATE DOMAIN child AS parent …` where
986 /// `parent` is itself a DOMAIN. The parser already captured the
987 /// unknown type name; it just was not carried here, so the parent's
988 /// CHECK constraints were invisible and a value violating them was
989 /// silently accepted. `base_type` still holds the ultimate scalar
990 /// type, which is what the storage tier stores.
991 pub base_domain: Option<String>,
992 /// Optional `DEFAULT <expr>`. Resolved at engine-side
993 /// CREATE TABLE time when a column is bound to this domain.
994 pub default: Option<Expr>,
995 /// `NOT NULL` from the domain definition. Engine ORs this
996 /// with the column-level nullability so the strictest of the
997 /// two wins (i.e. the column is non-nullable if either side
998 /// says so).
999 pub not_null: bool,
1000 /// Zero-or-more `CHECK (expr)` predicates. Each one is
1001 /// enforced as part of the column's CHECK list at INSERT /
1002 /// UPDATE time, with `VALUE` substituted for the column's
1003 /// current cell value.
1004 pub checks: Vec<Expr>,
1005}
1006
1007/// v7.17.0 Phase 1.4 — `CREATE TYPE` AST.
1008#[derive(Debug, Clone, PartialEq, Eq)]
1009pub struct CreateTypeStatement {
1010 pub name: String,
1011 pub kind: TypeKind,
1012}
1013
1014/// v7.17.0 Phase 1.4 — flavour of the new type. Only ENUM is
1015/// implemented; the variant set is open so Phase 1.5 (DOMAIN)
1016/// and later (COMPOSITE, RANGE) can land without an AST shape
1017/// migration.
1018///
1019/// v7.37.x (ζ-B Phase 1 composite accept) — added Composite for
1020/// `CREATE TYPE name AS (field_name field_type, …)`. Phase 1
1021/// stores the field list in the catalog so PG dumps that emit
1022/// `CREATE TYPE … AS (…)` don't error out; using a composite type
1023/// as a column type lands in Phase 2 (Value::Composite encoding +
1024/// ROW() literal + field-access syntax).
1025#[derive(Debug, Clone, PartialEq, Eq)]
1026pub enum TypeKind {
1027 /// `AS ENUM ('a', 'b', …)`. Order is preserved (PG enum
1028 /// labels are ordered).
1029 Enum { labels: Vec<String> },
1030 /// `AS (field_name field_type, …)`. Order matters; PG
1031 /// composite literals are positional.
1032 Composite {
1033 fields: Vec<(String, ColumnTypeName)>,
1034 /// v7.39 (round 264) — parallel to `fields`: the raw type NAME
1035 /// when a field's type is not a builtin (i.e. another composite).
1036 /// The parser already captures it; without carrying it here a
1037 /// nested composite field resolved to the Text placeholder and
1038 /// the inner record never became a record.
1039 field_user_types: Vec<Option<String>>,
1040 },
1041}
1042
1043/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
1044/// a string literal, an identifier (often a config name), an
1045/// integer/float, or the bare `DEFAULT` keyword.
1046#[derive(Debug, Clone, PartialEq)]
1047pub enum SetValue {
1048 String(String),
1049 Ident(String),
1050 Number(String),
1051 Default,
1052}
1053
1054/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1055/// at parse time and tracks the selected value on the engine. The
1056/// actual semantic differentiation (REPEATABLE READ snapshot,
1057/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1058/// today every level reads as effective READ COMMITTED (which is
1059/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1060/// READ COMMITTED). Default = `ReadCommitted`.
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1062pub enum IsolationLevel {
1063 ReadUncommitted,
1064 #[default]
1065 ReadCommitted,
1066 RepeatableRead,
1067 Serializable,
1068}
1069
1070impl IsolationLevel {
1071 /// Canonical PG-style display name, as `SHOW transaction_isolation`
1072 /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1073 /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1074 /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1075 /// `read uncommitted`) and only BEHAVES as read committed; the old
1076 /// fold renamed the label too.
1077 /// v7.39 — the MySQL display name, which is NOT the PG one: MySQL
1078 /// hyphenates and upper-cases. Measured on MySQL 9.7.2 by setting
1079 /// each level and reading `@@transaction_isolation` back:
1080 /// `READ-UNCOMMITTED` / `READ-COMMITTED` / `REPEATABLE-READ` /
1081 /// `SERIALIZABLE` (the last has no hyphen because it is one word).
1082 ///
1083 /// This exists so the two MySQL surfaces cannot drift: both
1084 /// `SHOW VARIABLES` and `@@transaction_isolation` used to carry
1085 /// their own hard-coded literal, and the literals disagreed —
1086 /// one said `REPEATABLE-READ` while the engine ran read committed.
1087 /// v7.39 — parse what `default_transaction_isolation` holds. PG
1088 /// accepts the SQL spellings and stores them lower-cased with a
1089 /// space; anything else is not a level this understands and the
1090 /// caller keeps its own default rather than guessing.
1091 #[must_use]
1092 pub fn from_pg_name(name: &str) -> Option<Self> {
1093 match name.trim().to_ascii_lowercase().as_str() {
1094 "read uncommitted" => Some(Self::ReadUncommitted),
1095 "read committed" => Some(Self::ReadCommitted),
1096 "repeatable read" => Some(Self::RepeatableRead),
1097 "serializable" => Some(Self::Serializable),
1098 _ => None,
1099 }
1100 }
1101
1102 #[must_use]
1103 pub fn as_mysql_str(self) -> &'static str {
1104 match self {
1105 Self::ReadUncommitted => "READ-UNCOMMITTED",
1106 Self::ReadCommitted => "READ-COMMITTED",
1107 Self::RepeatableRead => "REPEATABLE-READ",
1108 Self::Serializable => "SERIALIZABLE",
1109 }
1110 }
1111
1112 pub fn as_pg_str(self) -> &'static str {
1113 match self {
1114 Self::ReadUncommitted => "read uncommitted",
1115 Self::ReadCommitted => "read committed",
1116 Self::RepeatableRead => "repeatable read",
1117 Self::Serializable => "serializable",
1118 }
1119 }
1120}
1121
1122impl core::fmt::Display for IsolationLevel {
1123 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1124 f.write_str(self.as_pg_str())
1125 }
1126}
1127
1128/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1129/// single fixed-shape DDL; the WITH-clause options PG supports
1130/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1131/// scope for v6.1.4 — `enabled` defaults to true and there are
1132/// no other knobs to set in v6.1.x.
1133#[derive(Debug, Clone, PartialEq, Eq)]
1134pub struct CreateSubscriptionStatement {
1135 pub name: String,
1136 /// Connection string in PG keyword=value form (e.g.
1137 /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1138 /// `host` and `port` fields; the rest is reserved for
1139 /// future v6.1.x options.
1140 pub conn_str: String,
1141 /// One or more publications on the remote side. Order is
1142 /// preserved verbatim from the DDL; the worker requests them
1143 /// in this order. v6.1.4 records the list; v6.1.5
1144 /// publisher-side filtering enforces it.
1145 pub publications: Vec<String>,
1146}
1147
1148/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1149#[derive(Debug, Clone, PartialEq, Eq)]
1150pub struct CreateSequenceStatement {
1151 pub name: String,
1152 pub if_not_exists: bool,
1153 pub temporary: bool,
1154 /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1155 pub data_type: Option<SequenceDataType>,
1156 pub options: SequenceOptions,
1157}
1158
1159/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1161pub enum SequenceDataType {
1162 SmallInt,
1163 Int,
1164 BigInt,
1165}
1166
1167/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1168/// All fields are optional. `min_value`/`max_value` carry
1169/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1170#[derive(Debug, Clone, Default, PartialEq, Eq)]
1171pub struct SequenceOptions {
1172 pub increment: Option<i64>,
1173 pub min_value: Option<SeqBound>,
1174 pub max_value: Option<SeqBound>,
1175 pub start: Option<i64>,
1176 /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1177 /// RESTART, `Some(Some(n))` = RESTART WITH n.
1178 pub restart: Option<Option<i64>>,
1179 pub cache: Option<i64>,
1180 pub cycle: Option<bool>,
1181 pub owned_by: Option<SequenceOwnedBy>,
1182}
1183
1184/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1186pub enum SeqBound {
1187 Value(i64),
1188 NoBound,
1189}
1190
1191/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1192#[derive(Debug, Clone, PartialEq, Eq)]
1193pub enum SequenceOwnedBy {
1194 None,
1195 Column { table: String, column: String },
1196}
1197
1198/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1199#[derive(Debug, Clone, PartialEq)]
1200pub struct CreateMaterializedViewStatement {
1201 pub name: String,
1202 pub if_not_exists: bool,
1203 /// Optional `(col, col, …)` rename list. Applies to the
1204 /// backing table at CREATE / REFRESH time.
1205 pub columns: Vec<String>,
1206 /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1207 /// the cached rows.
1208 pub body: SelectStatement,
1209 /// `WITH DATA` (default) = materialise the rows at CREATE
1210 /// time. `WITH NO DATA` = create an empty backing table;
1211 /// callers must REFRESH before SELECT returns rows.
1212 pub with_data: bool,
1213 /// v7.38 (read01 P6.49) — when true this node came from
1214 /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1215 /// executor creates a plain table and does NOT register it in the
1216 /// materialized-view registry (no REFRESH semantics).
1217 pub as_plain_table: bool,
1218 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1219 /// meaningful together with `as_plain_table`; the executor puts the
1220 /// resulting table in the creating session's namespace.
1221 pub temporary: bool,
1222}
1223
1224/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1225/// auto-updatable view. `Cascaded` is PG's default when the bare
1226/// `WITH CHECK OPTION` is written.
1227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1228pub enum ViewCheckOption {
1229 Local,
1230 Cascaded,
1231}
1232
1233/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1234#[derive(Debug, Clone, PartialEq)]
1235pub struct CreateViewStatement {
1236 pub name: String,
1237 pub or_replace: bool,
1238 pub if_not_exists: bool,
1239 pub temporary: bool,
1240 /// Optional `(col, col, …)` rename list. When non-empty,
1241 /// these override the body's projected column names per-
1242 /// position at SELECT-from-view time.
1243 pub columns: Vec<String>,
1244 /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1245 /// time to materialise the view as a synthetic CTE.
1246 pub body: SelectStatement,
1247 /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1248 /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1249 /// 44000). `None` = no check option.
1250 pub check_option: Option<ViewCheckOption>,
1251}
1252
1253/// v7.17.0 — `ALTER SEQUENCE` AST node.
1254#[derive(Debug, Clone, PartialEq, Eq)]
1255pub struct AlterSequenceStatement {
1256 pub name: String,
1257 pub if_exists: bool,
1258 pub options: SequenceOptions,
1259 /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1260 /// instead of `options`; the two forms are mutually exclusive in PG.
1261 pub rename_to: Option<String>,
1262}
1263
1264/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1265/// the [`PublicationScope`] shape. v6.1.2 only accepted
1266/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1267/// variants by flipping the parser gate (no AST migration).
1268#[derive(Debug, Clone, PartialEq, Eq)]
1269pub struct CreatePublicationStatement {
1270 pub name: String,
1271 pub scope: PublicationScope,
1272}
1273
1274/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1275/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1276/// variants — the on-disk shape, snapshot serialisation, and the
1277/// AST round-trip Display path were already in place in v6.1.2
1278/// so this is a parser-only widening.
1279#[derive(Debug, Clone, PartialEq, Eq)]
1280pub enum PublicationScope {
1281 AllTables,
1282 ForTables(Vec<String>),
1283 AllTablesExcept(Vec<String>),
1284 /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1285 /// (PG 15+). AST-only: the executor folds `public` to
1286 /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1287 /// and refuses any other schema with PG's sentence, so the
1288 /// catalog / serializer / replication filter never see it.
1289 TablesInSchema(String),
1290}
1291
1292#[derive(Debug, Clone, PartialEq, Eq)]
1293pub struct AlterIndexStatement {
1294 pub name: String,
1295 pub target: AlterIndexTarget,
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Eq)]
1299pub enum AlterIndexTarget {
1300 /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1301 /// rebuilds the existing graph in place without touching the
1302 /// column encoding; `Some(enc)` re-encodes every cell first.
1303 Rebuild { encoding: Option<VecEncoding> },
1304 /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1305 /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1306 /// uses it to make the migration idempotent (re-running on a
1307 /// DB where the rename already happened is a no-op rather
1308 /// than an error).
1309 Rename { new: String, if_exists: bool },
1310 /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1311 /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1312 /// does not exist`), so the index is validated and the storage
1313 /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1314 /// SET/RESET arms already record).
1315 StorageParams,
1316}
1317
1318/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1319/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1320/// can add more SET subjects without changing the dispatch shape.
1321#[derive(Debug, Clone, PartialEq)]
1322pub struct AlterTableStatement {
1323 pub name: String,
1324 /// v7.13.2 — mailrs round-6 S1. One or more subactions
1325 /// separated by commas in the source SQL. PG-semantic apply
1326 /// is sequential; engine bails on first error (no
1327 /// transactional rollback of completed subactions in v7.13).
1328 /// Single-subaction shape stays a 1-element vec.
1329 pub targets: Vec<AlterTableTarget>,
1330}
1331
1332#[derive(Debug, Clone, PartialEq)]
1333#[allow(clippy::large_enum_variant)]
1334pub enum AlterTableTarget {
1335 /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1336 ///
1337 /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1338 /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1339 /// the reasoning went stale: `NO INHERIT` reported success while the
1340 /// child stayed attached, which is the worst kind of answer — the
1341 /// statement says it worked and the catalog disagrees.
1342 Inherit { parent: String, detach: bool },
1343 /// Per-table hot-tier byte budget override. The freezer
1344 /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1345 SetHotTierBytes(u64),
1346 /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1347 /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1348 /// Engine validates existing rows against the new constraint
1349 /// before installing it.
1350 AddForeignKey(ForeignKeyConstraint),
1351 /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1352 /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1353 /// no-op when no FK with that name exists; otherwise raises.
1354 DropForeignKey { name: String, if_exists: bool },
1355 /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1356 /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1357 /// as the standalone `DROP INDEX` statement.
1358 DropIndex { name: String, if_exists: bool },
1359 /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1360 /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1361 /// (20 migrate-*.sql hits). Engine appends the column to the
1362 /// schema and back-fills every existing row with the DEFAULT
1363 /// (or NULL when no DEFAULT and the column is nullable).
1364 AddColumn {
1365 column: ColumnDef,
1366 if_not_exists: bool,
1367 },
1368 /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1369 /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1370 /// existing row's column value by evaluating the optional
1371 /// USING expression (default `col::<ty>`) and re-coercing
1372 /// against the new column type.
1373 AlterColumnType {
1374 column: String,
1375 new_type: ColumnTypeName,
1376 using: Option<Expr>,
1377 /// v7.39 (round 713) — `COLLATE <name>` between the type and
1378 /// USING. PG re-collates the column, and an ABSENT clause RESETS
1379 /// the collation to the type default (measured round 713) — so
1380 /// `None` is not "leave it alone". The type parser consumed the
1381 /// clause all along and this surface dropped it on the floor:
1382 /// the statement succeeded and the ordering did not change, the
1383 /// silent-divergence shape. Folded variant + the name as written.
1384 collation: Option<(Collation, String)>,
1385 },
1386 /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1387 /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1388 /// every row's value at that position is removed; any index
1389 /// on the column is dropped. `if_exists` makes the drop a
1390 /// no-op when the column is missing. `cascade` removes
1391 /// dependents (FKs referencing the column, partial indexes
1392 /// whose predicate names the column); without it, the engine
1393 /// rejects when dependents exist.
1394 DropColumn {
1395 column: String,
1396 if_exists: bool,
1397 cascade: bool,
1398 },
1399 /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1400 /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1401 /// CONSTRAINT name CHECK (expr)` — table-level constraints
1402 /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1403 /// separate ALTER TABLE statement, so this surface lets the
1404 /// dump load straight through.
1405 AddTableConstraint(TableConstraint),
1406 /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1407 /// there is nothing to record; what PG does that SPG did not is
1408 /// REFUSE a role that does not exist. The name has to reach the
1409 /// engine for that, because only the engine knows the roles.
1410 OwnerTo { role: String },
1411 /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1412 /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1413 /// the hint is still a no-op; naming an index that does not exist is
1414 /// not.
1415 ClusterOn { index: Option<String> },
1416 /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1417 /// already in the table against a constraint added `NOT VALID` and,
1418 /// if they all pass, mark it validated. It used to be swallowed as a
1419 /// no-op on the theory that SPG validated at ADD time; SPG did not.
1420 ValidateConstraint { name: String },
1421 /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1422 /// Renames the column in the schema and propagates the rename
1423 /// to every stored source string that references it as a
1424 /// (potentially-qualified) column identifier: CHECK predicates,
1425 /// partial-index predicates, runtime DEFAULT expressions, and
1426 /// triggers' `UPDATE OF` column lists. Function bodies and
1427 /// trigger bodies are NOT auto-rewritten — they're loose
1428 /// source text and may contain references SPG can't statically
1429 /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1430 /// the column even if dependents exist; users renaming a
1431 /// column referenced by a function body update the function
1432 /// body separately.
1433 RenameColumn { old: String, new: String },
1434 /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1435 /// Reachable now that the schema stores user-supplied constraint names.
1436 RenameConstraint { old: String, new: String },
1437 /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1438 /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1439 /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1440 /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1441 /// (identity); both lower to this. SPG's auto-increment is
1442 /// max+1-scan based, so the dump's `setval(…)` calls stay
1443 /// no-ops without losing the sequence position.
1444 SetColumnAutoIncrement {
1445 column: String,
1446 /// The implicit sequence pg_dump names for an identity
1447 /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1448 /// nextval target for a serial default. The engine creates
1449 /// it if absent so the dump's later `setval(s, …)` lands.
1450 seq_name: Option<String>,
1451 },
1452 /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1453 /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1454 /// migrate-042 uses it). The engine moves the table entry
1455 /// in the catalog under the new name; child catalog state
1456 /// (FKs pointing at this table, triggers watching this
1457 /// table) tracks the rename through the storage layer.
1458 RenameTable { new: String },
1459 /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1460 /// { ALL | <name> }`. Toggles whether row-level triggers
1461 /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1462 /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1463 /// ENABLE epilogue around every table's data block so the
1464 /// rows already-computed in prod don't get re-rewritten
1465 /// (and so trigger-driven side effects like
1466 /// audit/queueing don't re-fire during a bulk reload).
1467 /// `which == TriggerSelector::All` toggles every trigger
1468 /// on the table; `Named(name)` toggles one trigger. The
1469 /// engine persists the disabled state on `TriggerDef.enabled`
1470 /// (catalog FILE_VERSION 25+) and the row-write paths skip
1471 /// the trigger when `!enabled`.
1472 SetTriggerEnabled {
1473 which: TriggerSelector,
1474 enabled: bool,
1475 },
1476 /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1477 /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1478 /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1479 /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1480 SetRowSecurity {
1481 enabled: Option<bool>,
1482 force: Option<bool>,
1483 },
1484 /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1485 /// <bounds>`. Promotes an existing table `child` to a partition
1486 /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1487 /// Engine validates that `child`'s columns are layout-compatible
1488 /// with `parent` and that every row in `child` satisfies the
1489 /// bound before installing the role.
1490 AttachPartition {
1491 child: String,
1492 bounds: PartitionOfBoundsAst,
1493 },
1494 /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1495 /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1496 /// to a standalone table (clears `partition_role`) and removes
1497 /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1498 /// is parser-accepted; engine performs the same atomic detach
1499 /// (single-engine, no replication lag — the PG semantics that
1500 /// require the two-phase split don't apply).
1501 DetachPartition {
1502 child: String,
1503 concurrently: bool,
1504 finalize: bool,
1505 },
1506 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1507 /// <expr>`. Engine re-parses + freezes the literal at this point,
1508 /// matching CREATE TABLE-side default semantics. Volatile shapes
1509 /// (`now()` / `nextval`) take the runtime-default path.
1510 AlterColumnSetDefault { column: String, default_expr: Expr },
1511 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1512 AlterColumnDropDefault { column: String },
1513 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1514 /// Engine validates that no existing row has NULL in that column
1515 /// before flipping the flag (PG semantics — partial NOT NULL
1516 /// would surface inconsistently).
1517 AlterColumnSetNotNull { column: String },
1518 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1519 AlterColumnDropNotNull { column: String },
1520 /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1521 /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1522 /// column's start value = 1). Engine records a next-value floor over
1523 /// SPG's max+1 identity allocation.
1524 AlterColumnRestart { column: String, with: Option<i64> },
1525 /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1526 /// EXPRESSION` turns a stored generated column into a plain column
1527 /// (its generation expression is removed; existing values are kept).
1528 AlterColumnDropExpression { column: String, if_exists: bool },
1529 /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1530 /// de-generate an identity column into a plain column.
1531 AlterColumnDropIdentity { column: String, if_exists: bool },
1532 /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1533 /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1534 /// expression and recomputes every existing row.
1535 AlterColumnSetExpression { column: String, expr: Expr },
1536 /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1537 /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1538 /// (PG: `type "x" does not exist`).
1539 OfType { type_name: String },
1540 /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1541 /// identity setting no-ops (SPG has no logical replication consumer);
1542 /// the INDEX must exist on this table (PG: `index "i" for table "t"
1543 /// does not exist`).
1544 ReplicaIdentityUsingIndex { index: String },
1545}
1546
1547/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1548/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1549/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1550/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1551/// shouldn't surface from a dump.
1552#[derive(Debug, Clone, PartialEq, Eq)]
1553pub enum TriggerSelector {
1554 /// Every trigger on the table.
1555 All,
1556 /// A specific trigger by name.
1557 Named(String),
1558}
1559
1560/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1561/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1562/// bitflags word or a nested options struct would only relocate the lint
1563/// while making the option each caller sets harder to read.
1564#[allow(clippy::struct_excessive_bools)]
1565#[derive(Debug, Clone, PartialEq)]
1566pub struct ExplainStatement {
1567 pub analyze: bool,
1568 /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1569 /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1570 /// `Insert on / Update on / Delete on` trees for them.
1571 pub inner: Box<Statement>,
1572 /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1573 /// advisor pass: after the regular plan tree, the engine
1574 /// emits one suggestion line per column referenced in the
1575 /// query's WHERE / JOIN that has no covering index on the
1576 /// owning table.
1577 pub suggest: bool,
1578 /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1579 /// `elapsed=…us` annotations from the Total line (and any
1580 /// future cost-bearing lines). PG-standard option used by
1581 /// regression suites and diff-friendly EXPLAIN output. When
1582 /// `true`, takes precedence over the per-session
1583 /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1584 pub costs_off: bool,
1585 /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1586 /// option that surfaces hot/cold/shared block counters. SPG's
1587 /// hot-tier scan path counts examined rows; the BUFFERS option
1588 /// makes that an explicit per-operator annotation.
1589 pub buffers: bool,
1590 /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1591 /// uses this to disable per-operator timing while still
1592 /// emitting actual-row counts (cheaper than ANALYZE). Default
1593 /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1594 /// timing portion of the Total line. Decoupled from `costs_off`:
1595 /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1596 /// measured wall-clock.
1597 pub timing_off: bool,
1598 /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1599 /// modified GUC values to the plan output. SPG emits the
1600 /// session params that diverge from default after the main
1601 /// plan body.
1602 pub settings: bool,
1603 /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1604 /// bytes / records / FPI emitted by the query. SPG's
1605 /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1606 /// ANALYZE) report against the engine WAL counter delta.
1607 pub wal: bool,
1608 /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1609 /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1610 /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1611 /// this is set.
1612 pub summary_off: bool,
1613 /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1614 /// PG's standard format selector. Default is text. JSON / XML
1615 /// / YAML emit a single-row TEXT result whose body wraps the
1616 /// existing line-per-operator text in the chosen container —
1617 /// PG-compatible just enough for dashboards that parse those
1618 /// container shapes (pgAdmin's JSON path picker, etc.).
1619 pub format: ExplainFormat,
1620}
1621
1622#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1623pub enum ExplainFormat {
1624 #[default]
1625 Text,
1626 Json,
1627 Xml,
1628 Yaml,
1629}
1630
1631/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1632#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1633pub enum PolicyCmd {
1634 All,
1635 Select,
1636 Insert,
1637 Update,
1638 Delete,
1639}
1640
1641/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1642/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1643#[derive(Debug, Clone, PartialEq)]
1644pub struct CreatePolicyStatement {
1645 pub name: String,
1646 pub table: String,
1647 /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1648 pub permissive: bool,
1649 pub cmd: PolicyCmd,
1650 /// Empty = PUBLIC.
1651 pub roles: Vec<String>,
1652 pub using: Option<Expr>,
1653 pub with_check: Option<Expr>,
1654}
1655
1656/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1657/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1658/// or the command (matches PG).
1659#[derive(Debug, Clone, PartialEq)]
1660pub struct AlterPolicyStatement {
1661 pub name: String,
1662 pub table: String,
1663 pub rename_to: Option<String>,
1664 pub roles: Option<Vec<String>>,
1665 pub using: Option<Expr>,
1666 pub with_check: Option<Expr>,
1667}
1668
1669/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1670#[derive(Debug, Clone, PartialEq, Eq)]
1671pub struct DropPolicyStatement {
1672 pub name: String,
1673 pub table: String,
1674 pub if_exists: bool,
1675}
1676
1677#[derive(Debug, Clone, PartialEq, Eq)]
1678pub struct CreateUserStatement {
1679 pub name: String,
1680 /// Empty when the statement carried no PASSWORD — legal for a bare
1681 /// `CREATE ROLE`, which cannot log in anyway.
1682 pub password: String,
1683 /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1684 /// the parser; the engine validates against `Role::parse` so a
1685 /// typo lands as a runtime error with a clear message rather than
1686 /// a parse failure.
1687 pub role: String,
1688 /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1689 /// statement did not say, so the default for its spelling applies:
1690 /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1691 /// both default to INHERIT and NOSUPERUSER.
1692 pub login: Option<bool>,
1693 pub inherit: Option<bool>,
1694 pub superuser: Option<bool>,
1695 /// `true` when spelled `CREATE USER` (LOGIN by default).
1696 pub is_user: bool,
1697}
1698
1699/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1700/// it tells the planner how far a call may be moved or folded. SPG records
1701/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1702/// yet exploit it for constant folding.
1703#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1704pub enum FunctionVolatility {
1705 Immutable,
1706 Stable,
1707 #[default]
1708 Volatile,
1709}
1710
1711impl FunctionVolatility {
1712 /// PG's one-character `pg_proc.provolatile` code.
1713 #[must_use]
1714 pub const fn as_pg_char(self) -> &'static str {
1715 match self {
1716 Self::Immutable => "i",
1717 Self::Stable => "s",
1718 Self::Volatile => "v",
1719 }
1720 }
1721
1722 #[must_use]
1723 pub const fn as_sql(self) -> &'static str {
1724 match self {
1725 Self::Immutable => "IMMUTABLE",
1726 Self::Stable => "STABLE",
1727 Self::Volatile => "VOLATILE",
1728 }
1729 }
1730}
1731
1732/// v7.39 (round 322, V46) — PG's parallel-safety class.
1733#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1734pub enum FunctionParallel {
1735 #[default]
1736 Unsafe,
1737 Restricted,
1738 Safe,
1739}
1740
1741impl FunctionParallel {
1742 /// PG's one-character `pg_proc.proparallel` code.
1743 #[must_use]
1744 pub const fn as_pg_char(self) -> &'static str {
1745 match self {
1746 Self::Unsafe => "u",
1747 Self::Restricted => "r",
1748 Self::Safe => "s",
1749 }
1750 }
1751
1752 #[must_use]
1753 pub const fn as_sql(self) -> &'static str {
1754 match self {
1755 Self::Unsafe => "PARALLEL UNSAFE",
1756 Self::Restricted => "PARALLEL RESTRICTED",
1757 Self::Safe => "PARALLEL SAFE",
1758 }
1759 }
1760}
1761
1762/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1763/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1764/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1765/// language's default cost / rows.
1766#[derive(Debug, Clone, Copy, PartialEq, Default)]
1767pub struct FunctionAttrs {
1768 pub volatility: FunctionVolatility,
1769 /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1770 /// argument returns NULL without running the body.
1771 pub strict: bool,
1772 pub security_definer: bool,
1773 pub leakproof: bool,
1774 pub parallel: FunctionParallel,
1775 /// `COST n` — `None` leaves PG's per-language default.
1776 pub cost: Option<f64>,
1777 /// `ROWS n` — set-returning functions only; `None` = default.
1778 pub rows: Option<f64>,
1779}
1780
1781impl FunctionAttrs {
1782 /// The attribute words `pg_get_functiondef` puts on their own line,
1783 /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1784 /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1785 /// at its default — PG then emits no such line at all.
1786 #[must_use]
1787 pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1788 let mut out = alloc::vec::Vec::new();
1789 if self.volatility != FunctionVolatility::Volatile {
1790 out.push(alloc::string::String::from(self.volatility.as_sql()));
1791 }
1792 if self.parallel != FunctionParallel::Unsafe {
1793 out.push(alloc::string::String::from(self.parallel.as_sql()));
1794 }
1795 if self.strict {
1796 out.push(alloc::string::String::from("STRICT"));
1797 }
1798 if self.security_definer {
1799 out.push(alloc::string::String::from("SECURITY DEFINER"));
1800 }
1801 if self.leakproof {
1802 out.push(alloc::string::String::from("LEAKPROOF"));
1803 }
1804 if let Some(c) = self.cost {
1805 out.push(alloc::format!("COST {}", render_attr_number(c)));
1806 }
1807 if let Some(r) = self.rows {
1808 out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1809 }
1810 out
1811 }
1812}
1813
1814/// PG prints a whole-numbered cost / rows without a decimal point.
1815fn render_attr_number(v: f64) -> alloc::string::String {
1816 // no_std: `f64::fract` lives in std, so compare against the truncation.
1817 let whole = v as i64;
1818 if v.abs() < 1e15 && (whole as f64) == v {
1819 alloc::format!("{whole}")
1820 } else {
1821 alloc::format!("{v}")
1822 }
1823}
1824
1825/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1826/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1827/// (the row-level trigger body the CREATE TRIGGER below references).
1828/// Non-trigger user-defined functions parse but error at execution
1829/// time with a clear unsupported message; that surface lands in
1830/// v7.12.5+.
1831#[derive(Debug, Clone, PartialEq)]
1832pub struct CreateFunctionStatement {
1833 pub name: String,
1834 /// `OR REPLACE` was present; an existing function with the
1835 /// same name is overwritten instead of erroring.
1836 pub or_replace: bool,
1837 /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1838 /// list `()` (sufficient for trigger functions). Other shapes
1839 /// parse and store the args but the executor refuses to call
1840 /// them.
1841 pub args: Vec<FunctionArg>,
1842 /// `RETURNS <type>` — `trigger` is the supported shape for
1843 /// v7.12.4; arbitrary return types parse to
1844 /// [`FunctionReturn::Other`].
1845 pub returns: FunctionReturn,
1846 /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1847 /// side of `AS $$...$$`; the parser canonicalises to one slot.
1848 /// `plpgsql` and `sql` are the two interesting values.
1849 pub language: String,
1850 /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1851 /// a structured AST; non-trigger / non-plpgsql bodies stay as
1852 /// the raw source text so the v7.12.5+ executor can pick them
1853 /// up without a parser rev.
1854 pub body: FunctionBody,
1855 /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1856 /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1857 /// on either side of the body; before this they were a parse error, so
1858 /// PG's own `pg_dump` output would not restore.
1859 pub attrs: FunctionAttrs,
1860}
1861
1862/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1863#[derive(Debug, Clone, PartialEq)]
1864pub struct FunctionArg {
1865 /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1866 /// (the default); `OUT` / `INOUT` parse but the executor
1867 /// refuses them.
1868 pub mode: FunctionArgMode,
1869 /// Optional arg name. Trigger functions traditionally don't
1870 /// name their args (they read NEW/OLD instead), so `None` is
1871 /// the common case.
1872 pub name: Option<String>,
1873 /// Declared type, normalised to the SPG `DataType` mapping
1874 /// where one exists. Unknown / extension types parse as a
1875 /// raw string under [`FunctionArgType::Raw`].
1876 pub ty: FunctionArgType,
1877}
1878
1879#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1880pub enum FunctionArgMode {
1881 In,
1882 Out,
1883 InOut,
1884}
1885
1886#[derive(Debug, Clone, PartialEq)]
1887pub enum FunctionArgType {
1888 Typed(ColumnTypeName),
1889 /// Unknown / extension types — kept as the parser-side raw
1890 /// identifier so error messages can name them precisely.
1891 Raw(String),
1892}
1893
1894#[derive(Debug, Clone, PartialEq)]
1895pub enum FunctionReturn {
1896 /// `RETURNS TRIGGER` — the row-level trigger function shape.
1897 /// v7.12.4 ships exactly this for execution.
1898 Trigger,
1899 /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
1900 /// the function is unused (since v7.12.4 doesn't ship scalar
1901 /// function invocation).
1902 Void,
1903 /// `RETURNS <type>` for any concrete data type. Reserved for
1904 /// v7.12.5+'s scalar UDF surface.
1905 Type(ColumnTypeName),
1906 /// `RETURNS <ident>` for types SPG doesn't know — extension
1907 /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
1908 Other(String),
1909}
1910
1911#[derive(Debug, Clone, PartialEq)]
1912pub enum FunctionBody {
1913 /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
1914 /// trigger-function executor walks this directly without
1915 /// re-parsing.
1916 PlPgSql(PlPgSqlBlock),
1917 /// Raw source text — parser couldn't (or didn't try to)
1918 /// structure-parse the body. Used for `LANGUAGE sql`
1919 /// functions and any PL/pgSQL body that contains v7.12.5+
1920 /// features the v7.12.4 parser doesn't yet recognise. The
1921 /// executor returns an unsupported error when invoked.
1922 Raw(String),
1923}
1924
1925/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
1926/// from assignment + return to a real-PL/pgSQL surface:
1927/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
1928/// control flow, `RAISE` diagnostics, and embedded SQL
1929/// statements that execute through the regular engine path.
1930/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
1931/// which mailrs's trigger doesn't need but other PG customers
1932/// may; deferred to a future minor release.
1933#[derive(Debug, Clone, PartialEq)]
1934pub struct PlPgSqlBlock {
1935 /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
1936 /// preceding `BEGIN`. Empty when the body opens directly with
1937 /// `BEGIN`. Declarations execute in order; each may reference
1938 /// earlier-declared locals in its init expression.
1939 pub declarations: Vec<PlPgSqlDeclare>,
1940 pub statements: Vec<PlPgSqlStmt>,
1941 /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
1942 /// <body>` handlers appended to the block. Empty when no
1943 /// EXCEPTION clause is present. When a body statement raises
1944 /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
1945 /// handlers are tried in order; the first matching condition
1946 /// runs its body and the block terminates cleanly. `OTHERS`
1947 /// matches any exception. Unhandled exceptions propagate.
1948 pub exception_handlers: Vec<ExceptionHandler>,
1949}
1950
1951/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
1952/// arm inside an EXCEPTION block.
1953#[derive(Debug, Clone, PartialEq)]
1954pub struct ExceptionHandler {
1955 /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
1956 /// conditions joined by `OR` share one handler body.
1957 pub conditions: Vec<String>,
1958 /// Statements to run when a matching exception is caught.
1959 pub body: Vec<PlPgSqlStmt>,
1960}
1961
1962/// v7.12.6 — single `DECLARE` entry: variable name + declared
1963/// type + optional initialiser. Variables default to SQL NULL
1964/// when no init is given (matches PG).
1965#[derive(Debug, Clone, PartialEq)]
1966pub struct PlPgSqlDeclare {
1967 pub name: String,
1968 /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
1969 /// knows it; raw text otherwise).
1970 pub ty: FunctionArgType,
1971 pub default: Option<Expr>,
1972}
1973
1974#[derive(Debug, Clone, PartialEq)]
1975pub enum PlPgSqlStmt {
1976 /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
1977 /// for clarity in error reporting (PG also forbids it) — the
1978 /// executor errors with a clear "OLD is read-only" message.
1979 Assign { target: AssignTarget, value: Expr },
1980 /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
1981 /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
1982 /// the SELECT statement with the INTO clause stripped; the
1983 /// engine runs it via `Engine::execute`, takes the first
1984 /// row's first column, and assigns to the local variable
1985 /// in the DECLARE scope. Single-column / single-row
1986 /// queries only at v7.16.2; multi-target (`INTO a, b`) is
1987 /// a v7.16.x follow-up.
1988 SelectInto {
1989 var: String,
1990 body: Box<SelectStatement>,
1991 },
1992 /// `RETURN <target>;` — trigger functions canonically return
1993 /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
1994 /// expression for forward compatibility with scalar UDFs.
1995 Return(ReturnTarget),
1996 /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
1997 /// set a SETOF function is building, and KEEP GOING. Not a return.
1998 ReturnNext(Expr),
1999 /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
2000 /// query yields, and keep going. It used to desugar to a side-effect
2001 /// statement whose result was DISCARDED — in a SETOF function that is the
2002 /// whole answer thrown away.
2003 ReturnQuery(Box<SelectStatement>),
2004 /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
2005 /// twin. Its rows go to the set too; it used to run and discard them.
2006 ReturnQueryExecute { sql: Expr },
2007 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
2008 /// [ELSE body] END IF;`. Branches are tried in order; first
2009 /// truthy condition wins; the optional ELSE runs when no
2010 /// condition matched.
2011 If {
2012 branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
2013 else_branch: Vec<PlPgSqlStmt>,
2014 },
2015 /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
2016 /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
2017 /// (logging — observable side effect only) or `EXCEPTION`
2018 /// (aborts the trigger and propagates as an error). v7.12.6
2019 /// supports the basic format-string substitution PG uses
2020 /// (`%` placeholders consumed positionally).
2021 Raise {
2022 level: RaiseLevel,
2023 message: String,
2024 args: Vec<Expr>,
2025 },
2026 /// v7.12.6 — embedded SQL statement inside the trigger body
2027 /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
2028 /// NEW.col / OLD.col references inside the embedded
2029 /// statement's expression tree are substituted with the
2030 /// current trigger context before the engine re-executes the
2031 /// statement. Recursion depth into nested triggers is
2032 /// bounded by the engine's existing trigger-fire guard.
2033 EmbeddedSql(Box<Statement>),
2034 /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
2035 /// the condition evaluates falsy the trigger / DO block aborts
2036 /// with the message (defaulting to a generic shape when none
2037 /// is provided). Same propagation shape as `RAISE EXCEPTION`
2038 /// — the error reaches the caller's query path. PG's behaviour
2039 /// is identical except for a `plpgsql.check_asserts` GUC that
2040 /// can disable the check globally; SPG always evaluates.
2041 Assert {
2042 condition: Expr,
2043 message: Option<Expr>,
2044 },
2045 /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2046 /// Iterate the body while condition evaluates truthy. Iteration
2047 /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2048 /// loops; the executor errors out when reached. EXIT / CONTINUE
2049 /// inside the body queue with 20.2.
2050 While {
2051 condition: Expr,
2052 body: Vec<PlPgSqlStmt>,
2053 },
2054 /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2055 /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2056 /// bounds inclusive on both sides. REVERSE walks backward.
2057 /// Iteration budget guards runaway.
2058 ForRange {
2059 var: String,
2060 start: Expr,
2061 end: Expr,
2062 reverse: bool,
2063 body: Vec<PlPgSqlStmt>,
2064 },
2065 /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2066 /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2067 /// budget guards runaway.
2068 Loop { body: Vec<PlPgSqlStmt> },
2069 /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2070 /// Unconditional (no WHEN) or conditional (only breaks when
2071 /// condition is truthy). Bubbles up as BodyOutcome::Break which
2072 /// the enclosing loop catches. Outside a loop it's a no-op.
2073 Exit { when: Option<Expr> },
2074 /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2075 /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2076 /// which the enclosing loop catches, skipping the remainder of
2077 /// the body and jumping to the next iteration.
2078 Continue { when: Option<Expr> },
2079 /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2080 /// computed SQL statement. The expression is evaluated to a
2081 /// text value, the resulting string is parsed and dispatched
2082 /// through the engine like an EmbeddedSql. USING <param_list>
2083 /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2084 ExecuteDynamic { sql: Expr },
2085 /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2086 /// END LOOP;`. Runs the SELECT once, iterates the resulting
2087 /// rows, binds the first column of each row to `var` as a
2088 /// scalar Value, then runs the body per iteration. EXIT /
2089 /// CONTINUE / ASSERT / RAISE etc. propagate through the
2090 /// enclosing loop's BodyOutcome discipline the same way
2091 /// FOR range and WHILE do. Full record-binding (var as
2092 /// composite carrying all columns) queues with v7.40 record
2093 /// type infrastructure.
2094 ForQuery {
2095 var: String,
2096 query: Box<SelectStatement>,
2097 body: Vec<PlPgSqlStmt>,
2098 },
2099 /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2100 /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2101 /// computed at runtime from a text expression, parsed on the
2102 /// fly, then iterated. Enables dynamic queries where the
2103 /// projection / FROM / WHERE clauses depend on runtime values.
2104 ForExecute {
2105 var: String,
2106 sql_expr: Expr,
2107 body: Vec<PlPgSqlStmt>,
2108 },
2109}
2110
2111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2112pub enum RaiseLevel {
2113 /// `RAISE NOTICE` — diagnostic message, observable in the
2114 /// server log. Does not affect the trigger's outcome.
2115 Notice,
2116 /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2117 Warning,
2118 /// `RAISE INFO` — like NOTICE, slightly quieter.
2119 Info,
2120 /// `RAISE LOG` — like NOTICE, lower priority.
2121 Log,
2122 /// `RAISE DEBUG` — like NOTICE, lowest priority.
2123 Debug,
2124 /// `RAISE EXCEPTION` — aborts the trigger function with the
2125 /// given message, propagating up to the caller as a query-
2126 /// level error.
2127 Exception,
2128}
2129
2130#[derive(Debug, Clone, PartialEq)]
2131pub enum AssignTarget {
2132 NewColumn(String),
2133 OldColumn(String),
2134 /// Reserved for v7.12.5 DECLARE'd local variables.
2135 Local(String),
2136}
2137
2138#[derive(Debug, Clone, PartialEq)]
2139pub enum ReturnTarget {
2140 /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2141 /// actually gets written (possibly with NEW.col mutations
2142 /// applied). For AFTER triggers, the return value is ignored.
2143 New,
2144 /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2145 /// the delete proceed; for BEFORE UPDATE / INSERT it's
2146 /// equivalent to dropping the write.
2147 Old,
2148 /// `RETURN NULL;` — for BEFORE triggers, skips the write
2149 /// entirely. For AFTER, the return value is ignored.
2150 Null,
2151 /// `RETURN <expr>;` — non-row return shape; reserved for the
2152 /// scalar UDF surface in v7.12.5+. Executor errors when used
2153 /// inside a trigger function.
2154 Expr(Expr),
2155}
2156
2157/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2158/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2159/// but the executor refuses them. `WHEN (cond)` clauses are out
2160/// of scope; the trigger function can short-circuit on a leading
2161/// IF inside its body once v7.12.5 lands IF.
2162#[derive(Debug, Clone, PartialEq)]
2163pub struct CreateTriggerStatement {
2164 pub name: String,
2165 pub or_replace: bool,
2166 pub timing: TriggerTiming,
2167 /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2168 /// three entries in order.
2169 pub events: Vec<TriggerEvent>,
2170 pub table: String,
2171 /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2172 /// only `Row`; `Statement` parses but the executor refuses.
2173 pub for_each: TriggerForEach,
2174 /// Name of the function to invoke. The function must exist at
2175 /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2176 /// forward reference (`function no_such_fn() does not exist`), so
2177 /// requiring it IS the PG behaviour (the old note claimed the
2178 /// opposite).
2179 pub function: String,
2180 /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2181 /// (mailrs round-5 G7). Non-empty only when the events list
2182 /// contains UPDATE and the user wrote the column-list filter.
2183 /// PG fires the trigger only when at least one of these
2184 /// columns appears in the SET clause; SPG conservatively
2185 /// fires on any UPDATE matching the listed columns or
2186 /// rewriting them at the row level. Empty vec = no filter
2187 /// (fire on every UPDATE).
2188 pub update_columns: Vec<String>,
2189 /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2190 /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2191 /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2192 pub when_condition: Option<Expr>,
2193}
2194
2195/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2196#[derive(Debug, Clone, PartialEq)]
2197pub struct CreateRuleStatement {
2198 pub name: String,
2199 pub or_replace: bool,
2200 /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2201 pub event: String,
2202 pub table: String,
2203 /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2204 /// (run alongside; PG's default when neither keyword is written).
2205 pub instead: bool,
2206 /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2207 pub when_condition: Option<Expr>,
2208 /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2209 pub commands: Vec<Statement>,
2210}
2211
2212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2213pub enum TriggerTiming {
2214 /// Fires before the row is written; the trigger function's
2215 /// return value (NEW or NULL) decides the row content and
2216 /// whether the write proceeds at all.
2217 Before,
2218 /// Fires after the row is written; the return value is
2219 /// ignored.
2220 After,
2221 /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2222 /// v7.12.4 (SPG has no updatable-view surface).
2223 InsteadOf,
2224}
2225
2226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2227pub enum TriggerEvent {
2228 Insert,
2229 Update,
2230 Delete,
2231 /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2232 /// so the trigger never fires.
2233 Truncate,
2234}
2235
2236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2237pub enum TriggerForEach {
2238 Row,
2239 Statement,
2240}
2241
2242/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2243///
2244/// SPG's index does not scan in a direction, but `indexdef` reproduces
2245/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2246/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2247/// which case PG's default applies — LAST for ascending, FIRST for
2248/// descending, and neither is rendered.
2249#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2250pub struct IndexColumnOrder {
2251 pub descending: bool,
2252 pub nulls_first: Option<bool>,
2253}
2254
2255#[derive(Debug, Clone, PartialEq)]
2256pub struct CreateIndexStatement {
2257 pub name: String,
2258 /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2259 /// either way, so this changes nothing about how the index is made
2260 /// — it is carried because PG refuses the CONCURRENTLY form inside
2261 /// a transaction block and accepts the plain one, and the engine
2262 /// cannot tell them apart without it.
2263 pub concurrently: bool,
2264 /// v7.39 (round 537) — the leading key column's ordering clause,
2265 /// which is the column SPG indexes.
2266 pub key_order: IndexColumnOrder,
2267 /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2268 /// written. SPG orders text by bytes, so honouring it changes
2269 /// nothing; PG prints it, because an explicitly named collation and
2270 /// the one a column inherits are different objects.
2271 pub key_collation: Option<String>,
2272 pub table: String,
2273 pub column: String,
2274 /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2275 /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2276 /// any NULL in the key exempts the row from the uniqueness check.
2277 pub nulls_not_distinct: bool,
2278 /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2279 /// graph for vector kNN); unspecified is the default B-tree index.
2280 pub method: IndexMethod,
2281 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2282 /// index name already exists, instead of raising `DuplicateIndex`.
2283 pub if_not_exists: bool,
2284 /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2285 /// non-key columns the planner should treat as "covered" by
2286 /// this index when checking whether a query can run as an
2287 /// index-only scan. Empty when no `INCLUDE` clause was given.
2288 pub included_columns: Vec<String>,
2289 /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2290 /// for which `<expr>` evaluates truthy enter the index;
2291 /// queries whose `WHERE` clause's canonical Display form
2292 /// matches this expression's Display form can be served by the
2293 /// partial index. Stored as a parsed `Expr` so the engine
2294 /// re-uses the existing evaluation path; storage persists the
2295 /// Display form on the catalog snapshot.
2296 pub partial_predicate: Option<Expr>,
2297 /// v6.8.2 — expression-based index. When `Some(expr)`, the
2298 /// index key is the result of `expr` evaluated on each row
2299 /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2300 /// field still names the *primary* column the expression
2301 /// touches so existing planner shortcuts that resolve a
2302 /// column position stay valid. `None` = plain
2303 /// column-reference index (the legacy shape).
2304 pub expression: Option<Expr>,
2305 /// v7.9.14 — extra column names after the leading column in a
2306 /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2307 /// planner today still only uses the leading column for index
2308 /// seeks; the extras are tracked verbatim so the same DDL
2309 /// round-trips through WAL replay + catalog snapshot, and so
2310 /// the engine can emit a clear warning at INDEX CREATE time
2311 /// that only the leading column is currently honoured.
2312 /// Composite BTree index keys land in v7.10.
2313 pub extra_columns: Vec<String>,
2314 /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2315 /// enforces uniqueness on the indexed key (combined with the
2316 /// `partial_predicate` filter — only rows where the predicate
2317 /// evaluates truthy enter the uniqueness check). Standard SQL
2318 /// and PG's canonical way to express conditional uniqueness.
2319 /// mailrs K1.
2320 pub is_unique: bool,
2321 /// v7.15.0 — operator class on the leading column, when the
2322 /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2323 /// Lower-cased. Most opclasses are still informational; the
2324 /// engine routes on `gin_trgm_ops` specifically to build a
2325 /// trigram-shingle GIN over a TEXT column, and otherwise
2326 /// keeps the current "accepted and discarded" behaviour for
2327 /// pg_dump compatibility.
2328 pub opclass: Option<String>,
2329 /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2330 /// there was no `USING` clause.
2331 ///
2332 /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2333 /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2334 /// implementation for still load. That degradation is deliberate, but
2335 /// it loses the name — and the operator-class check needs it, both to
2336 /// look the class up under the AM the user actually named and to say
2337 /// which AM it was missing from, the way PG's message does.
2338 pub method_name: Option<String>,
2339}
2340
2341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2342pub enum IndexMethod {
2343 /// Default — B-tree over `IndexKey`. Used for equality / range
2344 /// lookups on scalar columns.
2345 BTree,
2346 /// `USING hnsw` — NSW graph for kNN over a vector column.
2347 Hnsw,
2348 /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2349 /// metadata that records (min_key, max_key) for each page in a
2350 /// cold-tier segment, on the indexed column. The optimizer
2351 /// can use these summaries to skip pages whose range does NOT
2352 /// overlap a query's WHERE predicate. BRIN indexes carry no
2353 /// in-memory data — the summaries live in the segment v2
2354 /// envelope's sidecar. Created via the standard
2355 /// `CREATE INDEX … USING brin (col)` syntax.
2356 Brin,
2357 /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2358 /// column. Posting lists map `lexeme word` → row locators; the
2359 /// planner uses them to narrow `WHERE col @@ tsquery` to the
2360 /// candidate rows whose vectors contain a matching term, then
2361 /// re-evaluates the full `@@` semantics on each candidate.
2362 /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2363 /// silently degraded to a full scan at query time.
2364 Gin,
2365}
2366
2367/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2368/// inside a CREATE TABLE column list.
2369///
2370/// The source table's shape can only be read from the catalog, so the
2371/// parser records the clause and the engine expands it. `at` is how many
2372/// explicit columns preceded it: PG keeps the written order, so
2373/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2374#[derive(Debug, Clone, PartialEq)]
2375pub struct LikeSpec {
2376 pub source: String,
2377 pub at: usize,
2378 pub options: LikeOptions,
2379}
2380
2381/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2382/// types and NOT NULL and nothing else — measured on PG18, where a
2383/// copied generated column becomes a plain one and a copied identity
2384/// column loses its identity.
2385#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2386pub struct LikeOptions {
2387 pub defaults: bool,
2388 pub constraints: bool,
2389 pub identity: bool,
2390 pub generated: bool,
2391 pub indexes: bool,
2392 pub comments: bool,
2393}
2394
2395#[derive(Debug, Clone, PartialEq)]
2396pub struct CreateTableStatement {
2397 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2398 /// creating session's own namespace: it shadows a permanent table of the
2399 /// same name, other sessions never see it, and it is dropped when the
2400 /// session ends. A `bool` here lands in the struct's existing padding.
2401 pub temporary: bool,
2402 pub name: String,
2403 /// v7.39 — the `ENGINE=` a MySQL dump names. Consumed and discarded
2404 /// before, so `ENGINE=NONSUCH` built a table where MySQL 9.7.2
2405 /// answers `ERROR 1286`, and `sql_mode` claimed
2406 /// `NO_ENGINE_SUBSTITUTION` while doing it.
2407 pub engine: Option<String>,
2408 pub columns: Vec<ColumnDef>,
2409 /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2410 /// the order written. Empty for a table that has none.
2411 pub like_specs: Vec<LikeSpec>,
2412 /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2413 /// Empty for a table that inherits from nothing. Order matters:
2414 /// the child takes each parent's columns in this order before its
2415 /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2416 pub inherits: Vec<String>,
2417 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2418 /// table name already exists, instead of raising `DuplicateTable`.
2419 pub if_not_exists: bool,
2420 /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2421 /// constraints. Column-level `REFERENCES` (single-column inline
2422 /// form) is normalised into this vec at parse time so the engine
2423 /// sees one uniform list.
2424 pub foreign_keys: Vec<ForeignKeyConstraint>,
2425 /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2426 /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2427 /// Engine resolves each into a BTree index named after the
2428 /// constraint's leading column at CREATE TABLE time; INSERT
2429 /// path enforces composite uniqueness via row scan on the
2430 /// leading column index.
2431 pub table_constraints: Vec<TableConstraint>,
2432 /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2433 /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2434 /// the engine creates a parent table whose own rows stay
2435 /// empty and routes INSERT/SELECT through children. Mutually
2436 /// exclusive with `partition_of` (parser enforces).
2437 pub partition_by: Option<PartitionBySpec>,
2438 /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2439 /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2440 /// the table inherits its column list from `parent` (the
2441 /// parser rejects an explicit column list when this is set);
2442 /// engine routes child rows back to the parent at INSERT.
2443 pub partition_of: Option<PartitionOfSpec>,
2444}
2445
2446/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2447/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2448/// future LIST / HASH without breaking the public AST shape.
2449#[derive(Debug, Clone, PartialEq)]
2450pub struct PartitionBySpec {
2451 pub kind: PartitionKindAst,
2452 /// One or more ident references into the parent's column list.
2453 /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2454 /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2455 /// shape PG-compatible.
2456 pub key_columns: Vec<String>,
2457}
2458
2459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2460pub enum PartitionKindAst {
2461 Range,
2462 /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2463 /// `FOR VALUES IN (lit, lit, …)`.
2464 List,
2465 /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2466 /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2467 Hash,
2468}
2469
2470/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2471/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2472/// or the catch-all `DEFAULT` partition.
2473#[derive(Debug, Clone, PartialEq)]
2474pub struct PartitionOfSpec {
2475 pub parent_name: String,
2476 pub bounds: PartitionOfBoundsAst,
2477}
2478
2479#[derive(Debug, Clone, PartialEq)]
2480pub enum PartitionOfBoundsAst {
2481 /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2482 /// (lits include vector bodies), so we box both bounds to keep
2483 /// the variant size in line with `Default` for clippy and to
2484 /// minimise per-statement footprint when the partition shape
2485 /// isn't in use.
2486 Range {
2487 lower: Box<Expr>,
2488 upper: Box<Expr>,
2489 },
2490 /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2491 /// expr resolves to a typed literal at child-create time.
2492 List {
2493 values: Vec<Expr>,
2494 },
2495 /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2496 /// PG enforces `0 ≤ r < m`; m must be positive.
2497 Hash {
2498 modulus: u32,
2499 remainder: u32,
2500 },
2501 Default,
2502}
2503
2504/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2505/// column list. Either a composite PRIMARY KEY or a UNIQUE
2506/// (single- or multi-column).
2507#[derive(Debug, Clone, PartialEq)]
2508pub enum TableConstraint {
2509 /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2510 /// referenced column. Engine builds a BTree index named
2511 /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2512 PrimaryKey {
2513 name: Option<String>,
2514 columns: Vec<String>,
2515 /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2516 /// Round 621 consumed the clauses; these carry them.
2517 deferrable: bool,
2518 initially_deferred: bool,
2519 },
2520 /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2521 /// named `<table>_<leading_col>_key` (single-column) or
2522 /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2523 /// uniqueness on INSERT.
2524 Unique {
2525 name: Option<String>,
2526 columns: Vec<String>,
2527 /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2528 /// G10). PG 15+ flips the NULL handling so any number of
2529 /// NULL rows collide on the constraint. Default is
2530 /// `false` (NULLS DISTINCT, standard SQL behaviour).
2531 nulls_not_distinct: bool,
2532 /// v7.39 (round 711) — see PrimaryKey.
2533 deferrable: bool,
2534 initially_deferred: bool,
2535 },
2536 /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2537 /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2538 /// this same variant at parse time. Engine evaluates the
2539 /// predicate against each INSERT/UPDATE candidate row; a
2540 /// false / NULL result rejects the mutation.
2541 /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2542 /// PG adds such a constraint without scanning the existing rows: new
2543 /// rows are checked, the ones already there are grandfathered in, and
2544 /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2545 /// scans and flips it. pg_dump emits the suffix for exactly those, so
2546 /// validating them on restore would refuse a dump PG itself produced.
2547 Check {
2548 name: Option<String>,
2549 expr: Expr,
2550 not_valid: bool,
2551 },
2552 /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2553 /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2554 /// every element (the booking/scheduling non-overlap constraint,
2555 /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2556 /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2557 /// enforcement doesn't build the index yet). Each element pairs a
2558 /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2559 Exclude {
2560 name: Option<String>,
2561 method: Option<String>,
2562 elements: Vec<(String, String)>,
2563 },
2564 /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2565 /// non-unique secondary-index declaration inline in CREATE
2566 /// TABLE. Engine builds a BTree index on the leading column
2567 /// (composite columns parse but only the leading column is
2568 /// honoured at v7.15 — matches the existing
2569 /// `CreateIndexStatement::extra_columns` semantics). Useful
2570 /// for `mysql/blog`-style schemas that lean on routine
2571 /// secondary indexes for ORM lookups.
2572 Index {
2573 name: Option<String>,
2574 columns: Vec<String>,
2575 },
2576 /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2577 /// (cols)` inline declaration. Pre-v7.17 the parser
2578 /// silently dropped these so MyISAM-imported FULLTEXT
2579 /// indexes vanished; v7.17 routes them through the
2580 /// existing tsvector-GIN engine path so MATCH AGAINST
2581 /// queries get a real inverted index instead of falling
2582 /// back to a full scan. Multi-column FULLTEXT KEYs build
2583 /// one GIN per column at v7.17 (per-column posting lists);
2584 /// the leading column drives query planning.
2585 FulltextIndex {
2586 name: Option<String>,
2587 columns: Vec<String>,
2588 },
2589}
2590
2591#[derive(Debug, Clone, PartialEq)]
2592#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2593pub struct ColumnDef {
2594 pub name: String,
2595 pub ty: ColumnTypeName,
2596 pub nullable: bool,
2597 /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2598 /// evaluates this once (with an empty row) and caches the resulting
2599 /// `Value` on the column schema.
2600 pub default: Option<Expr>,
2601 /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2602 /// per such column and fills the slot when INSERT leaves it
2603 /// unbound (omitted from a column-list INSERT or explicitly NULL).
2604 pub auto_increment: bool,
2605 /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2606 /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2607 /// an implicit BTree index named `<table>_pkey` over this
2608 /// column at CREATE TABLE time, satisfying the parent-side
2609 /// index requirement for any FOREIGN KEY pointing at it.
2610 pub is_primary_key: bool,
2611 /// v7.13.0 — inline `UNIQUE` column constraint
2612 /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2613 /// into a single-column `TableConstraint::Unique` so the
2614 /// engine path stays uniform with table-level UNIQUE.
2615 pub is_unique: bool,
2616 /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2617 /// inline column constraint: treat NULL keys as equal so only one NULL
2618 /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2619 /// `TableConstraint::Unique { nulls_not_distinct }`.
2620 pub unique_nulls_not_distinct: bool,
2621 /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2622 /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2623 /// since this round so the fold into the table-level constraint keeps it.
2624 pub constraint_deferrable: bool,
2625 pub constraint_initially_deferred: bool,
2626 /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2627 /// (mailrs round-5 G3). Stored alongside the column so the
2628 /// CREATE TABLE handler can fold these into table-level
2629 /// CHECK constraints. Multiple inline CHECKs on the same
2630 /// column are concatenated with AND at the table level.
2631 pub check: Option<Expr>,
2632 /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2633 /// parser sees an unknown column-type ident (anything not in
2634 /// the built-in `parse_column_type_name` table), it sets
2635 /// `ty = ColumnTypeName::Text` and records the original name
2636 /// here. The engine resolves at CREATE TABLE time: if a
2637 /// catalog enum/domain with this name exists, the column is
2638 /// bound to it (label-checked on INSERT for enums; CHECK-
2639 /// constrained for domains); otherwise the CREATE TABLE
2640 /// errors with "unknown type".
2641 pub user_type_ref: Option<String>,
2642 /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2643 /// CURRENT_TIMESTAMP` column attribute. When set, an
2644 /// UPDATE that does NOT explicitly bind this column
2645 /// overrides the new value with `now()` (engine clock).
2646 /// Pre-v7.17 SPG silently accepted the syntax and never
2647 /// fired the override — `updated_at` columns from mysqldump
2648 /// stayed pinned at their initial DEFAULT forever, an
2649 /// audit Tier-S silent-failure. Generalised as a stored
2650 /// expression source so future shapes (`ON UPDATE
2651 /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2652 /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2653 pub on_update_runtime: Option<Expr>,
2654 /// v7.17.0 Phase 2.5 — text collation derived from the
2655 /// post-fix `COLLATE <name>` clause (and / or the table-level
2656 /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2657 /// per column). Pre-2.5 SPG accepted the clause and
2658 /// discarded the name, leaving every column byte-compared
2659 /// — a Tier-S silent failure when the customer expected
2660 /// `_ci` / `case_insensitive` semantics. Parser normalises
2661 /// the raw collation name into the variants in `Collation`.
2662 /// Default `Binary` preserves the legacy compare path.
2663 pub collation: Collation,
2664 /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2665 /// explicit `COLLATE <name>` clause rather than the default. Under the
2666 /// MySQL dialect a text column with NO explicit clause takes the
2667 /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2668 /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2669 /// flag is the only thing that tells them apart.
2670 pub collation_explicit: bool,
2671 /// v7.39 (round 676) — the collation name AS WRITTEN, because
2672 /// `collation` above cannot carry it: `Collation` is a two-variant
2673 /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2674 /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2675 /// tell them apart.
2676 pub collation_name: Option<String>,
2677 /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2678 /// 4.4 SPG accepted and discarded the keyword, leaving
2679 /// negative values silently accepted on a column the
2680 /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2681 /// rejects negative INSERT / UPDATE values on UNSIGNED int
2682 /// columns. SPG widening to `u64`-shaped storage is out of
2683 /// v7.17 scope; the upper bound remains the signed-type max
2684 /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2685 /// exceeds what every mailrs / Rails app actually uses.
2686 pub is_unsigned: bool,
2687 /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2688 /// value list captured at parse time. When `Some`, the parser
2689 /// recognised `ENUM(...)` in the type slot; the engine
2690 /// validates INSERT cells against this list at
2691 /// column_def_to_schema time and persists the variants on
2692 /// `ColumnSchema.inline_enum_variants`. None for all
2693 /// non-ENUM columns.
2694 pub inline_enum_variants: Option<Vec<String>>,
2695 /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2696 /// value list. Distinct from ENUM (subset semantics rather
2697 /// than pick-one). None for all non-SET columns.
2698 pub inline_set_variants: Option<Vec<String>>,
2699 /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2700 /// STORED` computed-column source. When `Some`, the engine
2701 /// stores the Display-form of the parsed expression on
2702 /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2703 /// and re-evaluates the expression against every INSERT /
2704 /// UPDATE candidate row, overwriting whatever the caller
2705 /// supplied for this column. Boxed to keep `ColumnDef` from
2706 /// blowing past the `large_enum_variant` clippy ceiling
2707 /// (`Expr` widens with vector literals).
2708 pub generated_stored_expr: Option<Box<Expr>>,
2709 /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2710 /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2711 /// `auto_increment`; this additionally marks the ALWAYS one, whose
2712 /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2713 /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2714 /// VALUE`. Only meaningful when the column is also an identity column.
2715 pub identity_always: bool,
2716 /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2717 /// integer width (TINYINT / MEDIUMINT), captured before the type
2718 /// collapses to SmallInt / Int. The engine copies it to
2719 /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2720 /// path can enforce the real range. None for every other column and
2721 /// under the PG dialect.
2722 pub mysql_int_width: Option<MysqlIntWidth>,
2723 /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2724 /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2725 /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2726 /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2727 /// CREATE TABLE time so the write path can truncate and the render path
2728 /// can pad. None under the PG dialect, where temporal columns keep full
2729 /// microseconds.
2730 pub mysql_fsp: Option<u8>,
2731}
2732
2733/// v7.17.0 Phase 2.5 — text collation classification surfaced
2734/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2735/// engine bridges between the two at CREATE TABLE time.
2736///
2737/// Recognised collation-name patterns (case-insensitive):
2738/// * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase` → CaseInsensitive
2739/// * Everything else (`C`, `POSIX`, `default`,
2740/// `pg_catalog.default`, `*_cs`, `*_bin`, unknown names) → Binary
2741#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2742pub enum Collation {
2743 Binary,
2744 CaseInsensitive,
2745}
2746
2747/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2748/// integer width for a column whose `ColumnTypeName` is too wide to carry
2749/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2750/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2751/// TABLE time. Only recorded under the MySQL dialect.
2752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2753pub enum MysqlIntWidth {
2754 Tiny,
2755 Small,
2756 Medium,
2757 Int,
2758 /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2759 Big,
2760}
2761
2762#[allow(clippy::derivable_impls)]
2763impl Default for Collation {
2764 fn default() -> Self {
2765 Self::Binary
2766 }
2767}
2768
2769impl Collation {
2770 /// Classify a `COLLATE <name>` ident into one of the supported
2771 /// variants. Empty / unknown names fall back to `Binary` —
2772 /// matches the pre-2.5 silent-accept behaviour for snapshots
2773 /// that load through but don't actually depend on the
2774 /// collation semantics.
2775 #[must_use]
2776 pub fn from_collation_name(name: &str) -> Self {
2777 let lc = name.trim().to_ascii_lowercase();
2778 // Strip any quotes / schema-qualifier the parser left on
2779 // (e.g. `pg_catalog.default`).
2780 let bare = lc
2781 .trim_matches(|c: char| c == '"' || c == '\'')
2782 .rsplit('.')
2783 .next()
2784 .unwrap_or("");
2785 if bare.is_empty() {
2786 return Self::Binary;
2787 }
2788 if bare == "case_insensitive" || bare == "nocase" {
2789 return Self::CaseInsensitive;
2790 }
2791 // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2792 // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2793 if bare.ends_with("_ci") {
2794 return Self::CaseInsensitive;
2795 }
2796 Self::Binary
2797 }
2798}
2799
2800/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2801/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2802/// parse into this shape — the column-level form has a single-entry
2803/// `columns` / `parent_columns`.
2804#[derive(Debug, Clone, PartialEq)]
2805pub struct ForeignKeyConstraint {
2806 /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2807 /// today but parses + stores it so a future ALTER TABLE DROP
2808 /// CONSTRAINT can target by name (v7.6.8).
2809 pub name: Option<String>,
2810 /// Local columns participating in the FK (≥ 1).
2811 pub columns: Vec<String>,
2812 /// Referenced parent table.
2813 pub parent_table: String,
2814 /// Referenced parent columns. Must have the same arity as
2815 /// `columns`; engine validates parent has a PK / UNIQUE index
2816 /// on exactly this column set (v7.6.1).
2817 pub parent_columns: Vec<String>,
2818 /// `ON DELETE` action. Defaults to `Restrict` if absent.
2819 pub on_delete: FkAction,
2820 /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2821 pub on_update: FkAction,
2822 /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2823 pub match_type: MatchType,
2824 /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2825 /// dropped on the floor, so a constraint declared DEFERRABLE was
2826 /// enforced immediately and a circular-FK migration could not load.
2827 pub deferrable: bool,
2828 /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2829 /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2830 pub initially_deferred: bool,
2831}
2832
2833/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2834/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2835/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2836#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2837pub enum MatchType {
2838 #[default]
2839 Simple,
2840 Full,
2841}
2842
2843/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2844#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2845pub enum FkAction {
2846 /// Reject the parent mutation if any child row references it.
2847 /// SQL spec default; SPG default when no clause is given.
2848 Restrict,
2849 /// Recursively propagate the parent's delete / update to the
2850 /// child rows. Same TX.
2851 Cascade,
2852 /// Set the child FK column(s) to NULL. Requires the FK columns
2853 /// to be NULL-able.
2854 SetNull,
2855 /// Set the child FK column(s) to their declared DEFAULT.
2856 /// Requires the child column(s) to have DEFAULT.
2857 SetDefault,
2858 /// SQL spec `NO ACTION` (deferred check). SPG treats this as
2859 /// `Restrict` because the single-writer model has no deferred
2860 /// constraint window; the keyword is accepted for compatibility.
2861 NoAction,
2862}
2863
2864/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
2865/// optional `USING <encoding>` clause; omitting it keeps the
2866/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
2867/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
2868/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
2869/// binary16 (2× compression, ~3 decimal digits of precision).
2870#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2871pub enum VecEncoding {
2872 /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
2873 /// uncompressed `vector` type wire / storage layout.
2874 #[default]
2875 F32,
2876 /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
2877 /// `spg_storage::quantize::Sq8Vector` for the math + recall
2878 /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
2879 /// dim ≥ 32).
2880 Sq8,
2881 /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
2882 /// per-element. DDL keyword `HALF` (pgvector convention).
2883 /// Bit-exact dequantise to f32 at the storage layer; no
2884 /// rerank pass needed for kNN search.
2885 F16,
2886}
2887
2888impl fmt::Display for VecEncoding {
2889 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2890 match self {
2891 Self::F32 => f.write_str("F32"),
2892 Self::Sq8 => f.write_str("SQ8"),
2893 // pgvector convention: DDL keyword is `HALF`, not `F16`.
2894 Self::F16 => f.write_str("HALF"),
2895 }
2896 }
2897}
2898
2899/// SQL-level type names. The mapping to the storage runtime's `DataType`
2900/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
2901#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2902pub enum ColumnTypeName {
2903 /// v7.39 (round 291) — PG's `name`, the identifier type its
2904 /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
2905 /// answered `type "name" does not exist` to.
2906 Name,
2907 /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
2908 /// 32-bit wrapping counter the row header carries; `xid8` is the
2909 /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
2910 /// SPG answered `type "xid" does not exist` to.
2911 Xid,
2912 Xid8,
2913 /// v7.39 (round 667) — `OID`. `XID` was already a column type here
2914 /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
2915 /// `type "oid" does not exist` while `t(x XID)` built fine.
2916 Oid,
2917 SmallInt,
2918 Int,
2919 BigInt,
2920 Float,
2921 /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
2922 /// IEEE. It used to map to [`Self::Float`] on the theory that a
2923 /// wider float is harmless, but the width is observable: a `real`
2924 /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
2925 /// answered false where PG answers true.
2926 Real,
2927 Text,
2928 /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
2929 Varchar(u32),
2930 /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
2931 Char(u32),
2932 Bool,
2933 /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
2934 /// `USING <encoding>` clause; omitting it surfaces as
2935 /// `encoding = VecEncoding::F32` (the pre-v6 default).
2936 Vector {
2937 dim: u32,
2938 encoding: VecEncoding,
2939 },
2940 /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
2941 /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
2942 /// v7.39 (round 271) — scale widened to u16 alongside the value's.
2943 /// v7.39 (round 272) — precision too: PG's runs to 1000.
2944 /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
2945 /// a negative one rounds to tens / hundreds. A VALUE's display scale
2946 /// stays unsigned.
2947 Numeric(u16, i16),
2948 /// `DATE` — calendar day, no time-of-day component.
2949 Date,
2950 /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
2951 /// precision.
2952 Timestamp,
2953 /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
2954 /// stores all timestamps as UTC microseconds-since-epoch and
2955 /// does not carry per-row offset (PG's internal representation
2956 /// is the same — TZ is a display convention). The distinction
2957 /// from `TIMESTAMP` exists for the PG-wire layer to advertise
2958 /// OID 1184 so sqlx-style clients decode into
2959 /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
2960 Timestamptz,
2961 /// v4.9 `JSON` — text-backed JSON document. No parse-time
2962 /// validation; the engine round-trips the literal verbatim.
2963 /// PG OID 114 on the wire.
2964 Json,
2965 /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
2966 /// PG OID 3802 on the wire so sqlx-style binary-typed clients
2967 /// decode without a custom type registration.
2968 Jsonb,
2969 /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
2970 /// Literal forms (decoded by the engine at coercion time):
2971 /// - PG hex form: `'\xDEADBEEF'`
2972 /// - Escape form: `'foo\\000bar'` (backslash octal triples)
2973 Bytes,
2974 /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
2975 /// OID 1009. Literal forms accepted by the parser:
2976 /// - `ARRAY['a', 'b', NULL]`
2977 /// - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
2978 /// form at coerce time)
2979 TextArray,
2980 /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
2981 /// 1007. Same literal forms as TEXT[] (substituting integer
2982 /// elements).
2983 IntArray,
2984 /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
2985 /// OID 1016.
2986 BigIntArray,
2987 /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
2988 /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
2989 /// external form). G-CRIT-3.
2990 TsVector,
2991 /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
2992 /// wire OID 3615.
2993 TsQuery,
2994 /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
2995 /// Literal input accepts canonical hyphenated, unhyphenated,
2996 /// uppercase, and `{...}`-braced forms; display normalises to
2997 /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
2998 /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
2999 /// gen_random_uuid()`.
3000 Uuid,
3001 /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
3002 /// microseconds since 00:00:00. PG wire OID 1083. Literal
3003 /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
3004 /// (6-digit microsecond precision). Display normalises to
3005 /// the canonical `HH:MM:SS[.ffffff]`.
3006 Time,
3007 /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
3008 /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
3009 /// PG OID; advertised as INT4 on the wire. Display always
3010 /// 4 digits zero-padded.
3011 Year,
3012 /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
3013 /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
3014 /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
3015 /// Offset range: ±14 hours.
3016 TimeTz,
3017 /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
3018 /// (locale-independent storage). Wire OID 790. Literal input
3019 /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
3020 /// major units), optional leading `-`. Display: en_US locale.
3021 Money,
3022 /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
3023 /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
3024 /// — the engine bridges to `DataType::Range(RangeKind)`.
3025 Range(RangeKindAst),
3026 /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
3027 /// `text => text` map with NULL value support.
3028 Hstore,
3029 /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
3030 IntArray2D,
3031 BigIntArray2D,
3032 TextArray2D,
3033 /// v7.39 (read01 round 75) — `bool[][]`.
3034 BoolArray2D,
3035 /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
3036 /// three-field {months, days, micros} struct (PG-byte-equal),
3037 /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
3038 /// β-P2 `INTERVAL` was runtime-only — literal in expression
3039 /// position but rejected at CREATE TABLE.
3040 Interval,
3041 /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
3042 /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
3043 /// PG external form quotes each non-NULL element because
3044 /// interval text contains spaces / colons
3045 /// (`{"1 day","24:00:00",NULL}`).
3046 IntervalArray,
3047 /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3048 /// mirrors a scalar `ColumnTypeName` that already existed.
3049 BoolArray,
3050 SmallIntArray,
3051 FloatArray,
3052 NumericArray,
3053 DateArray,
3054 TimestampArray,
3055 TimestamptzArray,
3056 UuidArray,
3057 JsonArray,
3058 JsonbArray,
3059 BytesArray,
3060 VarcharArray,
3061 CharArray,
3062 /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3063 /// as `Range(RangeKindAst)` — one column type variant covers
3064 /// all six builtin multiranges, kind pins the element type.
3065 /// Wire OIDs in pgwire.
3066 Multirange(RangeKindAst),
3067 /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3068 /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3069 /// Wire OIDs in pgwire.
3070 Point,
3071 Lseg,
3072 Path,
3073 PgBox,
3074 Polygon,
3075 Line,
3076 Circle,
3077 /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3078 Inet,
3079 Cidr,
3080 Macaddr,
3081 Macaddr8,
3082 /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3083 Bit(u32),
3084 /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3085 BitVarying(u32),
3086 Xml,
3087 Char1,
3088 MoneyArray,
3089}
3090
3091/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3092/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3093/// crate doesn't depend on storage. Bridged at engine boundary.
3094#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3095pub enum RangeKindAst {
3096 Int4,
3097 Int8,
3098 Num,
3099 Ts,
3100 TsTz,
3101 Date,
3102}
3103
3104impl fmt::Display for ColumnTypeName {
3105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3106 match self {
3107 Self::SmallInt => f.write_str("SMALLINT"),
3108 Self::Int => f.write_str("INT"),
3109 Self::BigInt => f.write_str("BIGINT"),
3110 Self::Float => f.write_str("FLOAT"),
3111 Self::Real => f.write_str("REAL"),
3112 Self::Text => f.write_str("TEXT"),
3113 Self::Name => f.write_str("name"),
3114 Self::Xid => f.write_str("xid"),
3115 Self::Xid8 => f.write_str("xid8"),
3116 Self::Oid => f.write_str("oid"),
3117 Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3118 Self::Char(n) => write!(f, "CHAR({n})"),
3119 Self::Bool => f.write_str("BOOL"),
3120 Self::Vector { dim, encoding } => match encoding {
3121 VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3122 VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3123 VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3124 },
3125 Self::Json => f.write_str("JSON"),
3126 Self::Jsonb => f.write_str("JSONB"),
3127 Self::Bytes => f.write_str("BYTEA"),
3128 Self::TextArray => f.write_str("TEXT[]"),
3129 Self::IntArray => f.write_str("INT[]"),
3130 Self::BigIntArray => f.write_str("BIGINT[]"),
3131 Self::TsVector => f.write_str("TSVECTOR"),
3132 Self::TsQuery => f.write_str("TSQUERY"),
3133 Self::Uuid => f.write_str("UUID"),
3134 Self::Numeric(p, s) => {
3135 if *s == 0 {
3136 write!(f, "NUMERIC({p})")
3137 } else {
3138 write!(f, "NUMERIC({p}, {s})")
3139 }
3140 }
3141 Self::Date => f.write_str("DATE"),
3142 Self::Timestamp => f.write_str("TIMESTAMP"),
3143 Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3144 Self::Time => f.write_str("TIME"),
3145 Self::Year => f.write_str("YEAR"),
3146 Self::TimeTz => f.write_str("TIMETZ"),
3147 Self::Money => f.write_str("MONEY"),
3148 Self::Range(k) => f.write_str(match k {
3149 RangeKindAst::Int4 => "INT4RANGE",
3150 RangeKindAst::Int8 => "INT8RANGE",
3151 RangeKindAst::Num => "NUMRANGE",
3152 RangeKindAst::Ts => "TSRANGE",
3153 RangeKindAst::TsTz => "TSTZRANGE",
3154 RangeKindAst::Date => "DATERANGE",
3155 }),
3156 Self::Hstore => f.write_str("HSTORE"),
3157 Self::Interval => f.write_str("INTERVAL"),
3158 Self::IntervalArray => f.write_str("INTERVAL[]"),
3159 Self::BoolArray => f.write_str("BOOL[]"),
3160 Self::SmallIntArray => f.write_str("SMALLINT[]"),
3161 Self::FloatArray => f.write_str("FLOAT[]"),
3162 Self::NumericArray => f.write_str("NUMERIC[]"),
3163 Self::DateArray => f.write_str("DATE[]"),
3164 Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3165 Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3166 Self::UuidArray => f.write_str("UUID[]"),
3167 Self::JsonArray => f.write_str("JSON[]"),
3168 Self::JsonbArray => f.write_str("JSONB[]"),
3169 Self::BytesArray => f.write_str("BYTEA[]"),
3170 Self::VarcharArray => f.write_str("VARCHAR[]"),
3171 Self::CharArray => f.write_str("CHAR[]"),
3172 Self::Multirange(k) => f.write_str(match k {
3173 RangeKindAst::Int4 => "INT4MULTIRANGE",
3174 RangeKindAst::Int8 => "INT8MULTIRANGE",
3175 RangeKindAst::Num => "NUMMULTIRANGE",
3176 RangeKindAst::Ts => "TSMULTIRANGE",
3177 RangeKindAst::TsTz => "TSTZMULTIRANGE",
3178 RangeKindAst::Date => "DATEMULTIRANGE",
3179 }),
3180 Self::Point => f.write_str("POINT"),
3181 Self::Lseg => f.write_str("LSEG"),
3182 Self::Path => f.write_str("PATH"),
3183 Self::PgBox => f.write_str("BOX"),
3184 Self::Polygon => f.write_str("POLYGON"),
3185 Self::Line => f.write_str("LINE"),
3186 Self::Circle => f.write_str("CIRCLE"),
3187 Self::Inet => f.write_str("INET"),
3188 Self::Cidr => f.write_str("CIDR"),
3189 Self::Macaddr => f.write_str("MACADDR"),
3190 Self::Macaddr8 => f.write_str("MACADDR8"),
3191 Self::Bit(0) => f.write_str("BIT"),
3192 Self::Bit(n) => write!(f, "BIT({n})"),
3193 Self::BitVarying(0) => f.write_str("VARBIT"),
3194 Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3195 Self::Xml => f.write_str("XML"),
3196 Self::Char1 => f.write_str("\"char\""),
3197 Self::MoneyArray => f.write_str("MONEY[]"),
3198 Self::IntArray2D => f.write_str("INT[][]"),
3199 Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3200 Self::TextArray2D => f.write_str("TEXT[][]"),
3201 Self::BoolArray2D => f.write_str("BOOL[][]"),
3202 }
3203 }
3204}
3205
3206/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3207/// engine evaluates `expr` per matched row in the table's row order
3208/// and rewrites cells in place. Indexed columns are dropped + re-
3209/// inserted into the affected B-tree on each row change.
3210/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3211/// tail on a DML statement. Boxed off the statement struct so the PG-only
3212/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3213/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3214/// the identical meaning, so both share this one payload rather than each
3215/// growing its own.
3216#[derive(Debug, Clone, PartialEq)]
3217pub struct DmlOrderLimit {
3218 pub order_by: Vec<OrderBy>,
3219 pub limit: Option<u32>,
3220}
3221
3222/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3223/// FROM, kept so the engine can finish the job.
3224///
3225/// The parser rewrites the statement onto correlated subqueries, and it
3226/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3227/// name belongs to the target or to a source needs their column lists,
3228/// which parse time does not have. Carrying the clause lets the engine
3229/// — which has the catalog — resolve the rest.
3230#[derive(Debug, Clone, PartialEq)]
3231pub struct UpdateFromSources {
3232 pub from: FromClause,
3233 pub sub_where: Option<Expr>,
3234}
3235
3236#[derive(Debug, Clone, PartialEq)]
3237pub struct UpdateStatement {
3238 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3239 /// level UPDATE. Empty for a plain UPDATE.
3240 pub ctes: Vec<Cte>,
3241 pub table: String,
3242 /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3243 /// to `t`'s own rows and not to anything that descends from it.
3244 ///
3245 /// Round 644 taught the FROM clause the keyword and left DML behind
3246 /// because it needed a field here, and this struct carries a warning
3247 /// that round 413 measured widening it in place overflowing the
3248 /// parser's nesting stack. That warning was about `from_sources`, a
3249 /// struct wide enough to need boxing; a `bool` lands in the padding
3250 /// already present — same as `CreateTableStatement::temporary`.
3251 ///
3252 /// It also earns its keep beyond the spelling: the inheritance
3253 /// fan-out needs a way to say "the parent's own rows" as a
3254 /// statement, or running one on the parent recurses forever.
3255 pub only: bool,
3256 /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3257 /// statement's expressions refer to the target row by. PG allows the
3258 /// bare spelling here (unlike INSERT, which requires AS).
3259 pub alias: Option<String>,
3260 pub assignments: Vec<(String, Expr)>,
3261 /// v7.39 (round 533) — boxed: round 413 measured that widening this
3262 /// struct in place overflows the parser's nesting stack.
3263 pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3264 pub where_: Option<Expr>,
3265 /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3266 /// mutate the first `limit` rows in the given order. PG has no such
3267 /// clause; the parser accepts it only under the MySQL dialect. Boxed
3268 /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3269 /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3270 /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3271 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3272 /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3273 /// clause (legacy CommandComplete path). Some = engine
3274 /// evaluates the projection over each mutated row and
3275 /// streams the result as a Rows QueryResult.
3276 pub returning: Option<Vec<SelectItem>>,
3277}
3278
3279/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3280/// from the active catalog and prunes them from every index.
3281#[derive(Debug, Clone, PartialEq)]
3282pub struct DeleteStatement {
3283 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3284 /// level DELETE. Empty for a plain DELETE.
3285 pub ctes: Vec<Cte>,
3286 pub table: String,
3287 /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3288 /// to `t`'s own rows and not to anything that descends from it.
3289 ///
3290 /// Round 644 taught the FROM clause the keyword and left DML behind
3291 /// because it needed a field here, and this struct carries a warning
3292 /// that round 413 measured widening it in place overflowing the
3293 /// parser's nesting stack. That warning was about `from_sources`, a
3294 /// struct wide enough to need boxing; a `bool` lands in the padding
3295 /// already present — same as `CreateTableStatement::temporary`.
3296 ///
3297 /// It also earns its keep beyond the spelling: the inheritance
3298 /// fan-out needs a way to say "the parent's own rows" as a
3299 /// statement, or running one on the parent recurses forever.
3300 pub only: bool,
3301 /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3302 /// the WHERE / RETURNING expressions refer to the target row by.
3303 pub alias: Option<String>,
3304 pub where_: Option<Expr>,
3305 /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3306 /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3307 /// form (round 413), so it shares that payload — and it is boxed for
3308 /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3309 /// statement tipped the parser's 512 KiB nesting stack.
3310 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3311 /// v7.9.4 — `RETURNING <projection>`.
3312 pub returning: Option<Vec<SelectItem>>,
3313}
3314
3315/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3316/// One WHEN clause fires per source row depending on whether the
3317/// `on` condition matched any target row(s); the executor walks
3318/// `clauses` in declaration order and fires the first whose
3319/// `matched` kind and optional `condition` are both satisfied.
3320#[derive(Debug, Clone, PartialEq)]
3321pub struct MergeStatement {
3322 /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3323 /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3324 /// in PG). Each CTE materialises before the merge runs and its alias
3325 /// resolves as a source relation.
3326 pub ctes: Vec<Cte>,
3327 pub target: String,
3328 pub target_alias: Option<String>,
3329 pub source: String,
3330 pub source_alias: Option<String>,
3331 /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3332 /// the engine materialises this SELECT for the source rows and `source`
3333 /// is empty; the alias (required by PG for a subquery source) is in
3334 /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3335 pub source_select: Option<Box<SelectStatement>>,
3336 /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3337 /// positional column-alias list after the source alias. Empty when
3338 /// the statement carries none; the engine renames the materialised
3339 /// source columns positionally (PG's rule).
3340 pub source_column_aliases: Vec<String>,
3341 pub on: Expr,
3342 pub clauses: Vec<MergeWhenClause>,
3343 /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3344 /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3345 /// target/source aliases. `None` = no RETURNING (the common form).
3346 pub returning: Option<Vec<SelectItem>>,
3347}
3348
3349#[derive(Debug, Clone, PartialEq)]
3350pub struct MergeWhenClause {
3351 pub matched: MergeMatched,
3352 /// Optional `AND <expr>` filter — when present, the clause
3353 /// only fires for the source rows whose match-pair satisfies
3354 /// the predicate.
3355 pub condition: Option<Expr>,
3356 pub action: MergeAction,
3357}
3358
3359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3360pub enum MergeMatched {
3361 Matched,
3362 /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3363 /// target row (the classic insert branch).
3364 NotMatched,
3365 /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3366 /// row no source row matches. Actions are UPDATE / DELETE / DO
3367 /// NOTHING only (INSERT is a syntax error, as in PG).
3368 NotMatchedBySource,
3369}
3370
3371#[derive(Debug, Clone, PartialEq)]
3372pub enum MergeAction {
3373 /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3374 /// explicit column list (the bare `INSERT VALUES (vals)`
3375 /// shape lands later).
3376 Insert {
3377 columns: Vec<String>,
3378 values: Vec<Expr>,
3379 },
3380 /// `UPDATE SET col = expr [, …]` — applied to every matched
3381 /// target row for the firing source row.
3382 Update { assignments: Vec<(String, Expr)> },
3383 /// `DELETE` — drop every matched target row.
3384 Delete,
3385 /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3386 /// the clause and SPG mirrors so a customer-side MERGE that
3387 /// uses it for branch-control doesn't error).
3388 DoNothing,
3389}
3390
3391#[derive(Debug, Clone, PartialEq)]
3392pub struct InsertStatement {
3393 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3394 /// level INSERT (writable CTE outer body). Empty for a plain
3395 /// INSERT. PG semantics: each CTE materialises before the
3396 /// outer INSERT runs, sharing the same transaction.
3397 pub ctes: Vec<Cte>,
3398 pub table: String,
3399 /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3400 /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3401 /// row by. PG requires the AS keyword in this position.
3402 pub alias: Option<String>,
3403 /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3404 /// `None`, every tuple is positional and must match the table arity.
3405 /// When `Some`, the engine maps each tuple slot to the named column and
3406 /// fills the rest with NULL (must be nullable).
3407 pub columns: Option<Vec<String>>,
3408 /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3409 /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3410 /// `select_source` is `Some` (the engine builds rows from the
3411 /// inner SELECT result set instead).
3412 pub rows: Vec<Vec<Expr>>,
3413 /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3414 /// round-5 G4). When present, `rows` is empty and the engine
3415 /// materialises the SELECT result, coerces each output tuple to
3416 /// the target column types, and inserts as a single batch.
3417 pub select_source: Option<Box<SelectStatement>>,
3418 /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3419 /// upsert clause. None = legacy INSERT (conflict raises a
3420 /// DuplicateKey error). mailrs migration blocker #2.
3421 pub on_conflict: Option<OnConflictClause>,
3422 /// v7.9.4 — `RETURNING <projection>`.
3423 pub returning: Option<Vec<SelectItem>>,
3424 /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3425 /// between the column list and VALUES. Governs how explicitly-supplied
3426 /// values interact with `GENERATED … AS IDENTITY` columns:
3427 /// * `None` — default. A `GENERATED ALWAYS` identity column rejects
3428 /// an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3429 /// * `System` — override the ALWAYS restriction: the explicit value
3430 /// is used verbatim, as for a `BY DEFAULT` column.
3431 /// * `User` — ignore any explicit value on a `BY DEFAULT` identity
3432 /// column and generate from the sequence instead (no effect on
3433 /// non-identity columns).
3434 pub overriding: Overriding,
3435 /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3436 /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3437 /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3438 /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3439 /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3440 /// into a NOT NULL column becomes the type's default), and the engine
3441 /// cannot recover that intent from the conflict clause alone. A plain
3442 /// `bool` lands in this struct's existing padding, so the AST does not
3443 /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3444 pub mysql_ignore: bool,
3445}
3446
3447/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3448#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3449pub enum Overriding {
3450 /// No `OVERRIDING` clause.
3451 #[default]
3452 None,
3453 /// `OVERRIDING SYSTEM VALUE`.
3454 System,
3455 /// `OVERRIDING USER VALUE`.
3456 User,
3457}
3458
3459/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3460#[derive(Debug, Clone, PartialEq)]
3461pub struct OnConflictClause {
3462 /// Local columns that identify the conflict (must match a
3463 /// UNIQUE / PRIMARY KEY index on the target table). Empty
3464 /// list means the user wrote `ON CONFLICT DO …` without a
3465 /// target — the engine arbitrates on every unique constraint
3466 /// (round 240).
3467 pub target_columns: Vec<String>,
3468 /// v7.39 (round 240) — the index predicate after the target list
3469 /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3470 /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3471 /// which satisfy any predicate, so it is parsed and carried but not
3472 /// consulted (recorded residual: partial-unique-index arbiters).
3473 pub index_where: Option<Expr>,
3474 /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3475 /// <name>`: the pg_dump conflict-target form. The engine
3476 /// resolves the name to the constraint's columns.
3477 pub constraint_name: Option<String>,
3478 /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3479 /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3480 /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3481 /// `ON CONFLICT DO UPDATE` is refused (42601).
3482 pub mysql_lowered: bool,
3483 /// The action on conflict.
3484 pub action: OnConflictAction,
3485}
3486
3487/// v7.9.7 — action on conflict.
3488#[derive(Debug, Clone, PartialEq)]
3489pub enum OnConflictAction {
3490 /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3491 /// silently skips conflicting ones.
3492 Nothing,
3493 /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3494 /// may reference `EXCLUDED.col` to read the incoming row's
3495 /// value (engine wires `EXCLUDED` as a virtual table).
3496 Update {
3497 assignments: Vec<(String, Expr)>,
3498 where_: Option<Expr>,
3499 },
3500}
3501
3502/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3503///
3504/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3505/// policies are spelled again here and mapped at the engine boundary.
3506/// v7.39 — the modes `BEGIN` / `START TRANSACTION` / `SET TRANSACTION`
3507/// / `SET SESSION CHARACTERISTICS` accept. `read_only` used to be parsed
3508/// and dropped on the floor, so `BEGIN READ ONLY` opened an ordinary
3509/// read-write transaction and every write in it was accepted.
3510///
3511/// `None` on either field means the statement did not name that mode, so
3512/// the session default applies — which is not the same as naming the
3513/// default explicitly.
3514#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3515pub struct TransactionModes {
3516 pub isolation: Option<IsolationLevel>,
3517 /// `Some(true)` = READ ONLY, `Some(false)` = READ WRITE.
3518 pub read_only: Option<bool>,
3519}
3520
3521/// v7.39 — what a read-only transaction refuses, and what PG calls it.
3522///
3523/// SPG did not enforce read-only transactions at all: `BEGIN READ ONLY;
3524/// INSERT …` answered `INSERT 0 1` and committed, and
3525/// `default_transaction_read_only = on` changed nothing. Both GUCs were
3526/// in the inventory, so a session could set one, read it back, and be
3527/// told it held a guarantee nothing was enforcing. Applications open
3528/// read-only transactions as a SAFETY measure — a reporting connection,
3529/// a read-only leg in a pool, a "this path must not write" discipline —
3530/// so accepting the writes is the worst possible answer.
3531///
3532/// `Some(tag)` means refuse with PG's message, `cannot execute {tag} in
3533/// a read-only transaction` (SQLSTATE 25006). Every tag below was read
3534/// back from PostgreSQL 18.6 by running the statement inside
3535/// `BEGIN READ ONLY`, one statement per transaction so no error could be
3536/// attributed to the wrong line.
3537///
3538/// Several answers were not what one would guess, which is why they were
3539/// measured rather than reasoned:
3540///
3541/// * `CREATE TEMP TABLE` is REFUSED, tagged `CREATE TABLE`.
3542/// * `NOTIFY`, `LISTEN` and `REINDEX` are ALLOWED.
3543/// * `PREPARE` of an INSERT is ALLOWED — only the EXECUTE writes.
3544/// * `UPDATE … WHERE false`, which changes nothing, is still REFUSED:
3545/// the verb decides, not the row count.
3546/// * `GRANT`, `COMMENT ON` and `SELECT … FOR SHARE` are all REFUSED.
3547///
3548/// The match is exhaustive on purpose. A new statement cannot be added
3549/// without deciding here whether it writes, which is the failure this
3550/// repository keeps meeting: one member of a family gets handled and its
3551/// siblings quietly do not.
3552impl Statement {
3553 #[must_use]
3554 pub fn read_only_violation_tag(&self) -> Option<&'static str> {
3555 match self {
3556 // ---- writes rows -------------------------------------------
3557 Self::Insert { .. } => Some("INSERT"),
3558 Self::Update { .. } => Some("UPDATE"),
3559 Self::Delete { .. } => Some("DELETE"),
3560 Self::Merge { .. } => Some("MERGE"),
3561 Self::Truncate { .. } => Some("TRUNCATE TABLE"),
3562 Self::CopyFromFile { .. } => Some("COPY FROM"),
3563
3564 // A SELECT that takes row locks writes lock state, and PG
3565 // names the strength it was asked for.
3566 Self::Select(sel) => sel.locking.as_ref().map(|l| match l.strength {
3567 LockStrength::Update => "SELECT FOR UPDATE",
3568 LockStrength::NoKeyUpdate => "SELECT FOR NO KEY UPDATE",
3569 LockStrength::Share => "SELECT FOR SHARE",
3570 LockStrength::KeyShare => "SELECT FOR KEY SHARE",
3571 }),
3572
3573 // ---- changes the catalog -----------------------------------
3574 Self::CreateTable { .. } => Some("CREATE TABLE"),
3575 Self::DropTable { .. } => Some("DROP TABLE"),
3576 Self::AlterTable { .. } => Some("ALTER TABLE"),
3577 Self::CreateIndex { .. } => Some("CREATE INDEX"),
3578 Self::DropIndex { .. } => Some("DROP INDEX"),
3579 Self::AlterIndex { .. } => Some("ALTER INDEX"),
3580 Self::CreateView { .. } => Some("CREATE VIEW"),
3581 Self::DropView { .. } => Some("DROP VIEW"),
3582 Self::CreateMaterializedView { .. } => Some("CREATE MATERIALIZED VIEW"),
3583 Self::RefreshMaterializedView { .. } => Some("REFRESH MATERIALIZED VIEW"),
3584 Self::DropMaterializedView { .. } => Some("DROP MATERIALIZED VIEW"),
3585 Self::CreateSequence { .. } => Some("CREATE SEQUENCE"),
3586 Self::AlterSequence { .. } => Some("ALTER SEQUENCE"),
3587 Self::DropSequence { .. } => Some("DROP SEQUENCE"),
3588 Self::CreateType { .. } => Some("CREATE TYPE"),
3589 Self::DropType { .. } => Some("DROP TYPE"),
3590 Self::AlterTypeAddValue { .. } | Self::AlterTypeRenameValue { .. } => {
3591 Some("ALTER TYPE")
3592 }
3593 Self::CreateDomain { .. } => Some("CREATE DOMAIN"),
3594 Self::AlterDomain { .. } => Some("ALTER DOMAIN"),
3595 Self::DropDomain { .. } => Some("DROP DOMAIN"),
3596 Self::CreateSchema { .. } => Some("CREATE SCHEMA"),
3597 Self::DropSchema { .. } => Some("DROP SCHEMA"),
3598 Self::CreateFunction { .. } => Some("CREATE FUNCTION"),
3599 Self::DropFunction { .. } => Some("DROP FUNCTION"),
3600 Self::CreateTrigger { .. } => Some("CREATE TRIGGER"),
3601 Self::DropTrigger { .. } => Some("DROP TRIGGER"),
3602 Self::CreateRule { .. } => Some("CREATE RULE"),
3603 Self::DropRule { .. } => Some("DROP RULE"),
3604 Self::CreateExtension { .. } => Some("CREATE EXTENSION"),
3605 Self::CreateStatistics { .. } => Some("CREATE STATISTICS"),
3606 Self::DropStatistics { .. } => Some("DROP STATISTICS"),
3607 Self::DropAggregate { .. } => Some("DROP AGGREGATE"),
3608 Self::CommentOn { .. } => Some("COMMENT"),
3609 Self::DropDatabase { .. } => Some("DROP DATABASE"),
3610 Self::CreatePublication { .. } => Some("CREATE PUBLICATION"),
3611 Self::DropPublication { .. } => Some("DROP PUBLICATION"),
3612 Self::CreateSubscription { .. } => Some("CREATE SUBSCRIPTION"),
3613 Self::DropSubscription { .. } => Some("DROP SUBSCRIPTION"),
3614
3615 // ---- changes roles / permissions ---------------------------
3616 Self::CreateUser { .. } => Some("CREATE ROLE"),
3617 Self::DropUser { .. } => Some("DROP ROLE"),
3618 Self::AlterRolePassword { .. } | Self::SetDbRoleSetting { .. } => Some("ALTER ROLE"),
3619 Self::Grant { .. } => Some("GRANT"),
3620 Self::Revoke { .. } => Some("REVOKE"),
3621 Self::CreatePolicy { .. } => Some("CREATE POLICY"),
3622 Self::AlterPolicy { .. } => Some("ALTER POLICY"),
3623 Self::DropPolicy { .. } => Some("DROP POLICY"),
3624 Self::AlterSystem { .. } => Some("ALTER SYSTEM"),
3625
3626 // ---- SPG's own writers -------------------------------------
3627 // Rewrites cold-tier segments on disk. PG has no equivalent to
3628 // ask, so the test is what it does, not what it is called.
3629 Self::CompactColdSegments => Some("COMPACT COLD SEGMENTS"),
3630
3631 // ---- allowed -----------------------------------------------
3632 // Reads, transaction control, session state, cursors, and the
3633 // maintenance statements PG itself permits. `REINDEX` really is
3634 // allowed in a read-only transaction (measured), which is why
3635 // `Maintain` is here.
3636 //
3637 // `Prepare`, `Execute`, `Call` and `DoBlock` are allowed at
3638 // this level for the reason PG allows them: the write inside
3639 // is refused when it runs, by this same check. Measured:
3640 // `PREPARE p AS INSERT …` succeeds; `DO $$ … INSERT … $$`
3641 // fails with `cannot execute INSERT`.
3642 Self::Explain { .. }
3643 | Self::CopyTo { .. }
3644 | Self::CopyToFile { .. }
3645 | Self::Analyze { .. }
3646 | Self::Maintain { .. }
3647 | Self::Vacuum { .. }
3648 | Self::Begin { .. }
3649 | Self::Commit
3650 | Self::Rollback
3651 | Self::Savepoint { .. }
3652 | Self::RollbackToSavepoint { .. }
3653 | Self::ReleaseSavepoint { .. }
3654 | Self::PrepareTransaction { .. }
3655 | Self::SetTransaction { .. }
3656 | Self::SetConstraints { .. }
3657 | Self::SetParameter { .. }
3658 | Self::SetParameterList { .. }
3659 | Self::SetUserVars { .. }
3660 | Self::SetRole { .. }
3661 | Self::ResetParameter { .. }
3662 | Self::ShowParameter { .. }
3663 | Self::Discard { .. }
3664 | Self::Prepare { .. }
3665 | Self::Execute { .. }
3666 | Self::Deallocate { .. }
3667 | Self::Call { .. }
3668 | Self::DoBlock { .. }
3669 | Self::DeclareCursor { .. }
3670 | Self::FetchCursor { .. }
3671 | Self::MoveCursor { .. }
3672 | Self::CloseCursor { .. }
3673 | Self::Listen { .. }
3674 | Self::Notify { .. }
3675 | Self::Unlisten { .. }
3676 | Self::Kill { .. }
3677 | Self::WaitForWalPosition { .. }
3678 | Self::ValidateOnly { .. }
3679 | Self::NoOpPreventedInTransaction { .. }
3680 | Self::Empty
3681 | Self::ShowTables
3682 | Self::ShowDatabases
3683 | Self::ShowCreateTable { .. }
3684 | Self::ShowIndexes { .. }
3685 | Self::ShowStatus
3686 | Self::ShowVariables
3687 | Self::ShowVariablesLike { .. }
3688 | Self::ShowProcesslist
3689 | Self::ShowColumns { .. }
3690 | Self::ShowUsers
3691 | Self::ShowPublications
3692 | Self::ShowSubscriptions => None,
3693 }
3694 }
3695}
3696
3697#[derive(Debug, Clone, PartialEq, Eq)]
3698pub struct LockingClause {
3699 pub strength: LockStrength,
3700 /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3701 pub of_tables: Vec<String>,
3702 pub policy: LockWait,
3703}
3704
3705/// PG's four tuple-lock strengths, weakest first.
3706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3707pub enum LockStrength {
3708 KeyShare,
3709 Share,
3710 NoKeyUpdate,
3711 Update,
3712}
3713
3714/// What to do when the row is already locked.
3715#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3716pub enum LockWait {
3717 /// Block until it is free — PG's default.
3718 #[default]
3719 Wait,
3720 /// `NOWAIT` — fail the statement with 55P03.
3721 NoWait,
3722 /// `SKIP LOCKED` — leave the row out of the result.
3723 SkipLocked,
3724}
3725
3726#[derive(Debug, Clone, PartialEq, Default)]
3727pub struct SelectStatement {
3728 /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3729 /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3730 /// whole syntax and locked nothing: two workers running the classic
3731 /// `SKIP LOCKED` queue take both took the same row.
3732 /// v7.39 (round 305) — boxed. A locking clause appears on a
3733 /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3734 /// cost every `SelectStatement` 32 bytes, and this struct sits in
3735 /// recursive evaluation frames where the engine already runs close to
3736 /// its stack budget (a 512 KB depth guard is the canary).
3737 pub locking: Option<alloc::boxed::Box<LockingClause>>,
3738 /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3739 /// expressions, materialised once at query start before the
3740 /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3741 /// only — no `WITH RECURSIVE` for v4.x.
3742 pub ctes: Vec<Cte>,
3743 pub distinct: bool,
3744 /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3745 /// keep the first row (per ORDER BY) of each group the
3746 /// expressions define. Empty = no DISTINCT ON.
3747 pub distinct_on: Vec<Expr>,
3748 pub items: Vec<SelectItem>,
3749 pub from: Option<FromClause>,
3750 pub where_: Option<Expr>,
3751 pub group_by: Option<Vec<Expr>>,
3752 /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3753 /// expands `group_by` to every non-aggregate SELECT-list item
3754 /// before the executor runs. Mutually exclusive with an
3755 /// explicit `group_by` list (the parser sets exactly one).
3756 pub group_by_all: bool,
3757 /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3758 /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3759 /// aggregate executor resolves them through the same synthetic
3760 /// schema used for the SELECT items.
3761 pub having: Option<Expr>,
3762 /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3763 /// itself a `SelectStatement` with `order_by = None` and `limit =
3764 /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3765 /// top of the chain).
3766 pub unions: Vec<(UnionKind, SelectStatement)>,
3767 /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3768 /// Keys are matched left-to-right: first key decides, ties break
3769 /// to the second, etc.
3770 pub order_by: Vec<OrderBy>,
3771 /// `LIMIT <n>` — bound on row output. `n` is an integer
3772 /// literal **or** (v7.9.24) a placeholder `$N` resolved
3773 /// against the prepared-statement Bind values. mailrs
3774 /// migration follow-up H2.
3775 pub limit: Option<LimitExpr>,
3776 /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3777 /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3778 pub offset: Option<LimitExpr>,
3779 /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3780 /// (SQL:2008). When true and an ORDER BY is present, the
3781 /// executor extends past the LIMIT-truncated tail to include
3782 /// every row whose ORDER BY key equals the last-kept row's
3783 /// key. Requires an ORDER BY; the executor errors otherwise
3784 /// (matching PG's `WITH TIES` rule). The parser was already
3785 /// accepting `WITH TIES` since Phase 5.1; this field captures
3786 /// the choice so the executor can act on it.
3787 pub limit_with_ties: bool,
3788 /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3789 /// that NOTHING referenced. PG analyses every definition whether
3790 /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3791 /// and silently succeeded here — the referenced ones get their columns
3792 /// resolved through the WindowFunction nodes they were inlined into,
3793 /// and the unreferenced ones used to be dropped at parse, unexamined.
3794 /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3795 ///
3796 /// Not part of `Display`: an unreferenced definition has no effect on
3797 /// the result, so a deparsed body (a stored view) omits it.
3798 pub window_check_exprs: Vec<Expr>,
3799}
3800
3801impl Expr {
3802 /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3803 /// directly inside this expression to `f`. `f` receives each nested
3804 /// statement once; descending further (into that statement's own
3805 /// clauses) is the caller's job, which keeps this walk finite and
3806 /// lets the caller order the recursion.
3807 ///
3808 /// The match is deliberately **wildcard-free**: a new `Expr` variant
3809 /// does not compile until it says whether it can carry a subquery.
3810 /// The row-count resolution pass is built on this, and a shape it
3811 /// silently failed to visit would leave a `LimitExpr::Expr` behind —
3812 /// which every row-count reader would take as "no limit", i.e. the
3813 /// whole table. Compile-time exhaustiveness is what rules that out.
3814 /// Iterative on purpose. Expression trees here get deep (long
3815 /// boolean chains, big IN lists), and this walk is on the path of
3816 /// every statement; recursing would add a frame per node to a stack
3817 /// budget the engine already runs close to — a depth guard that runs
3818 /// on a deliberately small stack caught exactly that. Depth costs
3819 /// heap here instead.
3820 pub fn for_each_subquery_mut<E>(
3821 &mut self,
3822 f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
3823 ) -> Result<(), E> {
3824 let mut stack: Vec<&mut Self> = alloc::vec![self];
3825 while let Some(e) = stack.pop() {
3826 match e {
3827 Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
3828 Self::NamedArg { expr, .. }
3829 | Self::Variadic(expr)
3830 | Self::Unary { expr, .. }
3831 | Self::Cast { expr, .. }
3832 | Self::FieldAccess { base: expr, .. }
3833 | Self::IsNull { expr, .. }
3834 | Self::BoolTest { expr, .. }
3835 | Self::Extract { source: expr, .. } => stack.push(expr),
3836 Self::Binary { lhs, rhs, .. } => {
3837 stack.push(lhs);
3838 stack.push(rhs);
3839 }
3840 Self::Like { expr, pattern, .. } => {
3841 stack.push(expr);
3842 stack.push(pattern);
3843 }
3844 Self::ArraySubscript { target, index } => {
3845 stack.push(target);
3846 stack.push(index);
3847 }
3848 Self::ArraySlice { target, lo, hi } => {
3849 stack.push(target);
3850 stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
3851 }
3852 Self::AnyAll { expr, array, .. } => {
3853 stack.push(expr);
3854 stack.push(array);
3855 }
3856 Self::FunctionCall { args, .. } | Self::Array(args) => {
3857 stack.extend(args.iter_mut());
3858 }
3859 Self::AggregateOrdered {
3860 call,
3861 order_by,
3862 filter,
3863 ..
3864 } => {
3865 stack.push(call);
3866 stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
3867 stack.extend(filter.iter_mut().map(|b| &mut **b));
3868 }
3869 Self::WindowFunction {
3870 args,
3871 partition_by,
3872 order_by,
3873 filter,
3874 ..
3875 } => {
3876 // `frame` bounds hold folded numbers / interval
3877 // parts, never expressions — nothing to visit there.
3878 stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
3879 stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
3880 stack.extend(filter.iter_mut().map(|b| &mut **b));
3881 }
3882 Self::InList { expr, list, .. } => {
3883 stack.push(expr);
3884 stack.extend(list.iter_mut());
3885 }
3886 Self::Case {
3887 operand,
3888 branches,
3889 else_branch,
3890 } => {
3891 stack.extend(
3892 operand
3893 .iter_mut()
3894 .chain(else_branch.iter_mut())
3895 .map(|b| &mut **b),
3896 );
3897 for (when, then) in branches.iter_mut() {
3898 stack.push(when);
3899 stack.push(then);
3900 }
3901 }
3902 Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
3903 Self::InSubquery { expr, subquery, .. } => {
3904 stack.push(expr);
3905 f(subquery)?;
3906 }
3907 Self::RowInSubquery { row, subquery, .. }
3908 | Self::RowCmpSubquery { row, subquery, .. } => {
3909 stack.extend(row.iter_mut());
3910 f(subquery)?;
3911 }
3912 }
3913 }
3914 Ok(())
3915 }
3916}
3917
3918/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
3919/// time or a placeholder `$N` resolved during extended-query
3920/// Bind. mailrs migration follow-up H2.
3921///
3922/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
3923/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
3924/// made the compiler point at every site that used to duplicate a
3925/// row-count out of the AST, which is exactly the set that must not
3926/// bypass the resolution pre-pass.
3927#[derive(Debug, Clone, PartialEq)]
3928pub enum LimitExpr {
3929 /// `LIMIT 10` — value known at parse time.
3930 Literal(u32),
3931 /// `LIMIT $N` — the 1-based parameter index, resolved against
3932 /// the bind values when the prepared statement executes.
3933 Placeholder(u16),
3934 /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
3935 /// greatest(2,3)`: a row-count expression that isn't constant, so
3936 /// it can't be folded at parse time. Evaluated once, before
3937 /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
3938 /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
3939 /// "no limit"). **No execution path may see this variant** —
3940 /// `as_literal` would report `None`, which every row-count reader
3941 /// takes to mean "unlimited", i.e. the whole table.
3942 Expr(alloc::boxed::Box<Expr>),
3943}
3944
3945impl fmt::Display for LimitExpr {
3946 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3947 match self {
3948 Self::Literal(n) => write!(f, "{n}"),
3949 Self::Placeholder(n) => write!(f, "${n}"),
3950 // Parenthesised so the round-trip text re-parses as one
3951 // row-count expression (`LIMIT (SELECT 4)`), which is also
3952 // the only spelling `FETCH FIRST` accepts.
3953 Self::Expr(e) => write!(f, "({e})"),
3954 }
3955 }
3956}
3957
3958impl LimitExpr {
3959 /// Convenience for the simple-query path where no placeholders
3960 /// can possibly exist. Returns the literal value or `None` if
3961 /// this is a placeholder (caller must surface as Unsupported).
3962 ///
3963 /// v7.39 (round 305) — `None` is read by every row-count consumer as
3964 /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
3965 /// therefore silently return the whole table, so the engine's
3966 /// `resolve_limit_exprs` pre-pass rewrites the variant away before
3967 /// dispatch. The assertion makes a missed nesting site fail loudly
3968 /// in every test build rather than quietly widening a result set.
3969 #[must_use]
3970 pub fn as_literal(&self) -> Option<u32> {
3971 match self {
3972 Self::Literal(n) => Some(*n),
3973 Self::Placeholder(_) => None,
3974 Self::Expr(_) => {
3975 debug_assert!(
3976 false,
3977 "LimitExpr::Expr reached execution — resolve_limit_exprs \
3978 missed a nesting site; treating it as `no limit` would \
3979 return every row"
3980 );
3981 None
3982 }
3983 }
3984 }
3985}
3986
3987/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
3988/// the engine's `substitute_placeholders` pass these are
3989/// always Literal; in the simple-query path a Placeholder
3990/// shape returns None (executor surfaces as
3991/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
3992impl SelectStatement {
3993 #[must_use]
3994 pub fn limit_literal(&self) -> Option<u32> {
3995 self.limit.as_ref().and_then(LimitExpr::as_literal)
3996 }
3997 #[must_use]
3998 pub fn offset_literal(&self) -> Option<u32> {
3999 self.offset.as_ref().and_then(LimitExpr::as_literal)
4000 }
4001}
4002
4003#[derive(Debug, Clone, PartialEq)]
4004pub struct Cte {
4005 pub name: String,
4006 /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
4007 /// classical case) or a data-modifying statement
4008 /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
4009 /// CTE semantics. The modifying body's RETURNING projection
4010 /// becomes the materialised CTE table the outer query can
4011 /// reference; the modifying statement runs once before the
4012 /// outer query, within the same transaction.
4013 pub body: CteBody,
4014 /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
4015 /// RECURSIVE keyword. Applies to every CTE in the clause per
4016 /// PG semantics. A non-recursive body in a RECURSIVE WITH is
4017 /// allowed; the engine just runs it once.
4018 pub recursive: bool,
4019 /// v4.22: optional `WITH name(a, b, c)` column-name list. When
4020 /// non-empty, these override the body's output column names
4021 /// position-by-position; the engine errors out if the count
4022 /// doesn't match the body's projection width.
4023 pub column_overrides: Vec<String>,
4024 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
4025 /// SET seqcol` on a recursive CTE. Desugared at parse time into an
4026 /// extra ordering column on the body (see `rewrite_search_and_cycle`).
4027 pub search: Option<SearchClause>,
4028 /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
4029 /// USING pathcol` cycle detection, desugared at parse time.
4030 pub cycle: Option<CycleClause>,
4031}
4032
4033/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
4034#[derive(Debug, Clone, PartialEq)]
4035pub struct SearchClause {
4036 /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
4037 pub depth_first: bool,
4038 /// The CTE output columns the search orders by.
4039 pub by_columns: Vec<String>,
4040 /// The new column holding the ordering key (a row-array for depth,
4041 /// a `(depth, keys…)` row for breadth).
4042 pub set_column: String,
4043}
4044
4045/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
4046#[derive(Debug, Clone, PartialEq)]
4047pub struct CycleClause {
4048 /// Columns whose repetition along a path marks a cycle.
4049 pub columns: Vec<String>,
4050 /// The new boolean-ish column set to `mark_value` on a cycle.
4051 pub mark_column: String,
4052 /// Value written to `mark_column` when a cycle is detected (default
4053 /// `true`); `default_value` otherwise. PG allows any type; SPG carries
4054 /// them as literals.
4055 pub mark_value: Option<Literal>,
4056 pub default_value: Option<Literal>,
4057 /// The new column accumulating the visited-row path array.
4058 pub path_column: String,
4059}
4060
4061/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
4062/// (Insert / Update / Delete with optional RETURNING). The
4063/// data-modifying variants must carry a RETURNING projection for the
4064/// outer query to reference the CTE alias by; an empty RETURNING is
4065/// only valid if no outer reference materialises (rare — typically
4066/// caught at planning).
4067#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
4068#[derive(Debug, Clone, PartialEq)]
4069pub enum CteBody {
4070 Select(SelectStatement),
4071 Insert(Box<InsertStatement>),
4072 Update(Box<UpdateStatement>),
4073 Delete(Box<DeleteStatement>),
4074 /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
4075 /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
4076 Merge(Box<MergeStatement>),
4077}
4078
4079impl CteBody {
4080 /// Convenience accessor used by classical (read-only) CTE
4081 /// callsites that still expect a SELECT body. Returns None for
4082 /// data-modifying CTEs; callers must explicitly route those
4083 /// through `exec_with_ctes`'s modifying branch.
4084 #[must_use]
4085 pub fn as_select(&self) -> Option<&SelectStatement> {
4086 match self {
4087 Self::Select(s) => Some(s),
4088 _ => None,
4089 }
4090 }
4091
4092 #[must_use]
4093 pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
4094 match self {
4095 Self::Select(s) => Some(s),
4096 _ => None,
4097 }
4098 }
4099
4100 #[must_use]
4101 pub fn is_modifying(&self) -> bool {
4102 !matches!(self, Self::Select(_))
4103 }
4104}
4105
4106#[derive(Debug, Clone, PartialEq)]
4107pub struct OrderBy {
4108 pub expr: Expr,
4109 /// `false` = ASC (default), `true` = DESC.
4110 pub desc: bool,
4111 /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
4112 /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
4113 /// NULLS FIRST for DESC); the engine resolves the effective
4114 /// value via `nulls_first.unwrap_or(desc)`.
4115 pub nulls_first: Option<bool>,
4116 /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
4117 /// It lives here rather than in the expression for the same reason
4118 /// `desc` does: at an ORDER BY key a collation is ordering
4119 /// information, and nothing downstream of the sort needs it. A new
4120 /// `Expr` variant would instead put a new arm on `eval_expr`, which
4121 /// this repo has measured to overflow the debug stack.
4122 ///
4123 /// `None` means none was written, and the key falls back to whatever
4124 /// its COLUMN declares — which is every key that existed before this.
4125 pub collation: Option<String>,
4126}
4127
4128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4129pub enum UnionKind {
4130 /// `UNION` — dedupes the combined set.
4131 Distinct,
4132 /// `UNION ALL` — concatenates without dedup.
4133 All,
4134 /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
4135 /// present on both sides.
4136 Intersect,
4137 /// `INTERSECT ALL` — multiset intersection (min per-row count).
4138 IntersectAll,
4139 /// `EXCEPT` — distinct left rows absent from the right.
4140 Except,
4141 /// `EXCEPT ALL` — multiset subtraction.
4142 ExceptAll,
4143}
4144
4145#[derive(Debug, Clone, PartialEq)]
4146pub enum SelectItem {
4147 Wildcard,
4148 /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
4149 /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
4150 /// `NEW` pseudo-relation).
4151 QualifiedWildcard(String),
4152 Expr {
4153 expr: Expr,
4154 alias: Option<String>,
4155 },
4156}
4157
4158#[derive(Debug, Clone, PartialEq)]
4159pub struct TableRef {
4160 pub name: String,
4161 pub alias: Option<String>,
4162 /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
4163 /// children.
4164 ///
4165 /// The keyword used to be absorbed at parse time, on the reasoning
4166 /// that SPG's inheritance children are separate relations a plain
4167 /// scan does not descend into — so ONLY already described what the
4168 /// scan did. That stopped being true when a partition parent
4169 /// started unioning its children: measured, `SELECT count(*) FROM
4170 /// ONLY <partitioned parent>` answered 2 where PG answers 0.
4171 pub only: bool,
4172 /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
4173 /// When `Some(id)`, the scan restricts to rows that live in
4174 /// segment `<id>` only — useful for forensic inspection of a
4175 /// specific freezer-emitted segment without exposing the hot
4176 /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
4177 /// is STABILITY carve-out for v6.10 — needs the freezer to
4178 /// stamp each segment with a wall-clock at creation time.
4179 pub as_of_segment: Option<u32>,
4180 /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
4181 /// source. When `Some`, `name` is the alias (defaulting to
4182 /// `"unnest"` when no `AS` is given) and the engine builds a
4183 /// synthetic single-column table by evaluating the expression
4184 /// once at SELECT entry. Each TEXT[] element becomes one row;
4185 /// NULL elements become NULL cells. v7.11 supported
4186 /// uncorrelated UNNEST only as the FROM primary; v7.13.2
4187 /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
4188 /// position (cross-join with regular tables).
4189 pub unnest_expr: Option<Box<Expr>>,
4190 /// v7.13.2 — mailrs round-6 S5. PG-standard
4191 /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
4192 /// when non-empty, the first entry overrides the projected
4193 /// column name for the unnested column. Empty = fall back to
4194 /// the table alias (pre-v7.13.2 behaviour).
4195 pub unnest_column_aliases: Vec<String>,
4196 /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
4197 /// row-stream gains a trailing BIGINT column counting rows
4198 /// from 1 in element order. PG names it `ordinality`; a second
4199 /// entry in the column-alias list renames it.
4200 pub with_ordinality: bool,
4201 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4202 /// [, step])` set-returning source. When `Some`, the engine
4203 /// materialises a single-column virtual table by stepping
4204 /// `start` to `stop` inclusive. Args are the literal arg list
4205 /// (2 for default-step, 3 for explicit-step). Supports:
4206 /// * SmallInt / Int / BigInt with integer step (default = 1)
4207 /// * Timestamp with INTERVAL step (PG date-range pattern)
4208 /// Mutually exclusive with `unnest_expr` — both populate the
4209 /// same downstream dispatch slot. `name` defaults to
4210 /// `"generate_series"` when no alias is provided.
4211 pub generate_series_args: Option<Vec<Expr>>,
4212 /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
4213 /// table. When `Some`, the TableRef is a parenthesised SELECT
4214 /// that may reference columns from the preceding FROM items
4215 /// (correlated derived table). The executor materialises the
4216 /// subquery per left-row, substituting outer-column references
4217 /// against the current join row's values before running the
4218 /// inner SELECT, then cross-joins the result back.
4219 /// Mutually exclusive with `name` / `unnest_expr` /
4220 /// `generate_series_args`.
4221 pub lateral_subquery: Option<Box<SelectStatement>>,
4222 /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
4223 /// function as a FROM item. PG semantics: for each key/value
4224 /// pair in the JSONB object argument, emit one (key TEXT,
4225 /// value TEXT) row. When prefixed by `LATERAL` and joined via
4226 /// `CROSS JOIN LATERAL`, the argument may reference columns
4227 /// from a preceding FROM item, in which case the executor
4228 /// evaluates `<expr>` per outer row.
4229 /// Mutually exclusive with `unnest_expr` / `generate_series_args`
4230 /// / `lateral_subquery`. The optional `LATERAL` keyword does not
4231 /// require a separate flag — the executor evaluates per-row
4232 /// whenever the join sits in a JoinKind context.
4233 ///
4234 /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4235 /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4236 /// `json_each` / `json_each_text`) so the executor picks the
4237 /// value-column rendering (JSON text vs unwrapped text).
4238 pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4239 /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4240 /// function channel: `(lowercase fn name, args)`. Carries
4241 /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4242 /// dispatches by name.
4243 pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4244 /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4245 /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4246 /// reference to it yields the value, not a one-field composite
4247 /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4248 /// desugared shape is indistinguishable from a hand-written
4249 /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4250 /// only the parser knows which one it built, so it says so here.
4251 pub scalar_fn_item: bool,
4252 /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4253 /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4254 /// target-list SRFs follow — see round 67). The array-returning family keeps
4255 /// its own lowering; this channel carries the ones that have no array form
4256 /// (`generate_series`, a user `RETURNS SETOF` function).
4257 pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4258 /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4259 /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4260 /// tables (implicit LATERAL, like every SRF channel). Executed by
4261 /// walking the row path over the parsed doc, then each column's
4262 /// path per row-item; NESTED expands as a per-parent outer join.
4263 pub json_table: Option<Box<JsonTable>>,
4264}
4265
4266/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4267#[derive(Debug, Clone, PartialEq)]
4268pub struct JsonTable {
4269 /// The document expression (jsonb/json/text). May reference outer
4270 /// columns → implicit LATERAL.
4271 pub doc: Box<Expr>,
4272 /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4273 /// match is one row's context item.
4274 pub row_path: String,
4275 /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4276 pub columns: Vec<JsonTableColumn>,
4277 /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4278 pub passing: Vec<(String, Expr)>,
4279}
4280
4281/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4282#[derive(Debug, Clone, PartialEq)]
4283pub enum JsonTableColumn {
4284 /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4285 Ordinality { name: String },
4286 /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4287 /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4288 /// `<name> <type> EXISTS [PATH '<p>']`.
4289 Regular {
4290 name: String,
4291 ty: ColumnTypeName,
4292 /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4293 path: String,
4294 /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4295 exists: bool,
4296 /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4297 format_json: bool,
4298 /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4299 wrapper: bool,
4300 /// Behaviour when the path matches nothing (default NULL).
4301 on_empty: JsonTableOnBehavior,
4302 /// Behaviour when coercion fails (default NULL).
4303 on_error: JsonTableOnBehavior,
4304 },
4305 /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4306 /// row like a LEFT JOIN (a parent with no nested match still emits one
4307 /// row, nested cols NULL).
4308 Nested {
4309 path: String,
4310 columns: Vec<JsonTableColumn>,
4311 },
4312}
4313
4314/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4315#[derive(Debug, Clone, PartialEq)]
4316pub enum JsonTableOnBehavior {
4317 /// Default: the column value is NULL.
4318 Null,
4319 /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4320 Error,
4321 /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4322 Default(Box<Expr>),
4323}
4324
4325/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4326/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4327/// joins evaluate left-associatively in nested-loop order.
4328#[derive(Debug, Clone, PartialEq)]
4329pub struct FromClause {
4330 pub primary: TableRef,
4331 pub joins: Vec<FromJoin>,
4332}
4333
4334#[derive(Debug, Clone, PartialEq)]
4335pub struct FromJoin {
4336 pub kind: JoinKind,
4337 pub table: TableRef,
4338 /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4339 pub on: Option<Expr>,
4340 /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4341 /// USING column list so the executor can perform PG's column-merge
4342 /// (the join columns collapse to a single unqualified output column,
4343 /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4344 /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4345 /// USING into an equivalent `on` predicate so the join filter/count
4346 /// path works unchanged; `using_cols` drives only the output-shape
4347 /// rewrite. Empty/`None` for `ON` and CROSS joins.
4348 pub using_cols: Option<Vec<String>>,
4349 /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4350 /// column names are not known until the table schemas are available
4351 /// (parse time is schema-less), so the parser only sets this flag and
4352 /// leaves `on`/`using_cols` empty; the engine resolves the common
4353 /// columns at execution time, synthesises the `on` predicate + the
4354 /// USING column-merge, and clears the flag. If there are no common
4355 /// columns PG treats it as a CROSS join.
4356 pub natural: bool,
4357}
4358
4359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4360pub enum JoinKind {
4361 Inner,
4362 Left,
4363 Cross,
4364 /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4365 /// NULL-filling the left (drive) columns on unmatched right rows.
4366 /// The executor runs the LEFT algorithm's mirror: it tracks which
4367 /// peer rows matched and emits the unmatched ones with a NULL-left
4368 /// tuple after the probe loop. Output column order is unchanged
4369 /// (left-table cols then right-table cols).
4370 Right,
4371 /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4372 /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4373 FullOuter,
4374 /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4375 /// once, paired with the first peer row that satisfies the ON. Not
4376 /// reachable from SQL — the EXISTS pull-up emits it, which is what
4377 /// frees positive EXISTS from the round-721 uniqueness gate (an
4378 /// INNER join would multiply the outer rows; a semi join cannot).
4379 Semi,
4380}
4381
4382#[derive(Debug, Clone, PartialEq)]
4383pub enum Expr {
4384 Literal(Literal),
4385 Column(ColumnName),
4386 /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4387 /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4388 /// callee's declared parameter names, and a user function's live in the
4389 /// catalog — which the parser cannot see. So the name rides along in the
4390 /// tree and the evaluator, which has the catalog, does the reordering.
4391 /// Appears only inside a `FunctionCall`'s argument list.
4392 NamedArg {
4393 name: String,
4394 expr: Box<Expr>,
4395 },
4396 /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
4397 /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
4398 /// expression evaluates to an array whose elements the evaluator splices
4399 /// into the call as individual trailing arguments. Appears only inside a
4400 /// `FunctionCall`'s argument list.
4401 Variadic(Box<Expr>),
4402 /// v6.1.1 — `$N` parameter placeholder for the extended query
4403 /// protocol. The number is 1-based per PostgreSQL convention.
4404 /// Evaluation looks up `params[N-1]` from the prepared-statement
4405 /// bind buffer; out-of-range indices raise a runtime error
4406 /// (same shape as a column-not-found miss).
4407 Placeholder(u16),
4408 Binary {
4409 lhs: Box<Expr>,
4410 op: BinOp,
4411 rhs: Box<Expr>,
4412 },
4413 Unary {
4414 op: UnOp,
4415 expr: Box<Expr>,
4416 },
4417 /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
4418 /// TEXT, BOOL targets; engine coerces at evaluation time.
4419 Cast {
4420 expr: Box<Expr>,
4421 target: CastTarget,
4422 },
4423 /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
4424 /// evaluates to a composite/record value (an explicit `ROW(...)`, a
4425 /// whole-row reference, or a composite-returning function); `field` names
4426 /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
4427 /// column names for a whole-row). Only the parenthesised form reaches
4428 /// here — a bare `a.b` is parsed as a qualified column reference.
4429 FieldAccess {
4430 base: Box<Expr>,
4431 field: String,
4432 },
4433 /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
4434 IsNull {
4435 expr: Box<Expr>,
4436 negated: bool,
4437 },
4438 /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
4439 /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
4440 /// `Some(false)` for FALSE and `None` for UNKNOWN.
4441 ///
4442 /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
4443 /// The semantics were right, but the AST then had no way to say what
4444 /// the user wrote, so every renderer printed the lowering:
4445 /// `CHECK ((a > 1) IS TRUE)` came back as
4446 /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
4447 /// dumped view lost the form too.
4448 BoolTest {
4449 expr: Box<Expr>,
4450 value: Option<bool>,
4451 negated: bool,
4452 },
4453 /// Function call `name(args...)`. v1.4 supports a small built-in set
4454 /// (length, upper, lower, abs, coalesce); unknown names error at eval
4455 /// time so the parser stays open for v1.5 aggregates.
4456 FunctionCall {
4457 name: String,
4458 args: Vec<Expr>,
4459 },
4460 /// v7.24 (mailrs round-16 A) — an aggregate call with an
4461 /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
4462 /// Wraps the plain [`Expr::FunctionCall`] so every existing
4463 /// FunctionCall consumer stays untouched; only the aggregate
4464 /// executor (and the expression walkers) know the wrapper.
4465 /// Non-aggregate evaluation contexts reject it at eval time.
4466 AggregateOrdered {
4467 call: Box<Expr>,
4468 order_by: Vec<OrderBy>,
4469 /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
4470 /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
4471 /// aggregate modifier so plain FunctionCall stays untouched.
4472 distinct: bool,
4473 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
4474 /// Only the rows where `cond` is true contribute to this
4475 /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
4476 /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
4477 /// END)`, which is faithful for NULL-ignoring aggregates but
4478 /// WRONG for `array_agg` (it would collect a NULL per excluded
4479 /// row). The executor instead skips excluded rows before
4480 /// accumulation, which is correct for every aggregate.
4481 filter: Option<Box<Expr>>,
4482 },
4483 /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
4484 /// wildcards are `%` (any run) and `_` (one char), backslash escapes
4485 /// the next char (so `\%` matches a literal `%`).
4486 Like {
4487 expr: Box<Expr>,
4488 pattern: Box<Expr>,
4489 negated: bool,
4490 /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
4491 /// match. PG folds both operands.
4492 case_insensitive: bool,
4493 },
4494 /// v4.12 window function call: `name(args) OVER (PARTITION BY
4495 /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
4496 /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
4497 /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
4498 /// unordered windows and "from start of partition through
4499 /// current row" for ordered windows — no explicit ROWS /
4500 /// RANGE clause in v4.12 MVP.
4501 WindowFunction {
4502 name: String,
4503 args: Vec<Expr>,
4504 partition_by: Vec<Expr>,
4505 /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
4506 /// (None = PG default, same contract as [`OrderBy`]).
4507 order_by: Vec<(
4508 Expr,
4509 bool, /* desc */
4510 Option<bool>, /* nulls_first */
4511 )>,
4512 /// v4.20 explicit frame. `None` means "use the default":
4513 /// whole-partition when unordered, running aggregate from
4514 /// partition start through current row when ordered.
4515 frame: Option<WindowFrame>,
4516 /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
4517 /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
4518 /// `Respect` (PG / ANSI default — NULLs participate). Other
4519 /// window functions ignore this flag.
4520 null_treatment: NullTreatment,
4521 /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
4522 /// = no FILTER. Only aggregate window functions honor it; the
4523 /// predicate restricts which peer rows contribute within the frame.
4524 filter: Option<Box<Expr>>,
4525 },
4526 /// v4.10 scalar subquery — `(SELECT ...)` used in expression
4527 /// position. Must return exactly one row × one column at eval
4528 /// time; the engine errors out otherwise. Uncorrelated only —
4529 /// the inner SELECT cannot reference outer columns.
4530 ScalarSubquery(Box<SelectStatement>),
4531 /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
4532 /// projection is ignored; only row-count matters.
4533 Exists {
4534 subquery: Box<SelectStatement>,
4535 negated: bool,
4536 },
4537 /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
4538 /// project exactly one column; membership is tested by Eq
4539 /// against each row's value (NULL handling follows ANSI:
4540 /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
4541 InSubquery {
4542 expr: Box<Expr>,
4543 subquery: Box<SelectStatement>,
4544 negated: bool,
4545 },
4546 /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
4547 /// against a multi-column subquery. Row comparisons against a *list*
4548 /// decompose to OR-of-AND at parse time, but the subquery form can't
4549 /// (its rows are only known at runtime), so this survives as its own
4550 /// node evaluated with PG's row-comparison three-valued logic.
4551 RowInSubquery {
4552 row: Vec<Expr>,
4553 subquery: Box<SelectStatement>,
4554 negated: bool,
4555 },
4556 /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
4557 /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
4558 /// RowInSubquery, the literal-RHS form decomposes at parse time but the
4559 /// subquery form can't, so it survives as its own node. The subquery
4560 /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
4561 RowCmpSubquery {
4562 row: Vec<Expr>,
4563 op: BinOp,
4564 subquery: Box<SelectStatement>,
4565 },
4566 /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
4567 /// list. Both the parser's literal-list path and the engine's
4568 /// IN-subquery materialisation used to desugar into a left-deep
4569 /// OR-Eq chain, so expression depth scaled with the element count
4570 /// — a 24k-row subquery result overflowed the 2 MiB worker stack
4571 /// (recursive eval AND recursive Box drop) and aborted embedding
4572 /// host processes. The flat node keeps depth constant: eval is an
4573 /// iterative scan with PG three-valued logic, drop is a Vec drop.
4574 InList {
4575 expr: Box<Expr>,
4576 list: Vec<Expr>,
4577 negated: bool,
4578 },
4579 /// `EXTRACT(<field> FROM <source>)` — pull an integer component
4580 /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
4581 /// because the `FROM` keyword is what separates the two halves,
4582 /// not a comma.
4583 Extract {
4584 field: ExtractField,
4585 source: Box<Expr>,
4586 },
4587 /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
4588 /// element is evaluated independently; NULLs are allowed.
4589 /// v7.10 supports only single-dimension TEXT[] semantically;
4590 /// non-text elements coerce at engine evaluation time when
4591 /// the surrounding context (column type / cast) makes the
4592 /// target clear.
4593 Array(Vec<Expr>),
4594 /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
4595 /// engine returns NULL for out-of-range indices.
4596 ArraySubscript {
4597 target: Box<Expr>,
4598 index: Box<Expr>,
4599 },
4600 /// Array slice `arr[lo:hi]` — PG 1-based, both ends
4601 /// inclusive; a missing bound extends to that end of the
4602 /// array and out-of-range bounds clamp. Returns an array of
4603 /// the same element type.
4604 ArraySlice {
4605 target: Box<Expr>,
4606 lo: Option<Box<Expr>>,
4607 hi: Option<Box<Expr>>,
4608 },
4609 /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
4610 /// operator is the comparison binary op (Eq / Ne / Lt / …);
4611 /// the engine desugars: `ANY` returns true if any element
4612 /// satisfies; `ALL` returns true only if every element does.
4613 /// NULL handling follows PG's three-valued logic.
4614 AnyAll {
4615 expr: Box<Expr>,
4616 op: BinOp,
4617 array: Box<Expr>,
4618 /// `true` = ANY, `false` = ALL.
4619 is_any: bool,
4620 },
4621 /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
4622 /// (searched form, `operand` is None) and
4623 /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
4624 /// `operand` is the lead expression compared against each
4625 /// branch's match). Each `(when_expr, then_expr)` branch
4626 /// stays as written; engine short-circuits on the first match.
4627 /// `else_branch` is `None` when no ELSE; evaluates to NULL.
4628 /// mailrs round-5 G9.
4629 Case {
4630 operand: Option<Box<Expr>>,
4631 branches: Vec<(Expr, Expr)>,
4632 else_branch: Option<Box<Expr>>,
4633 },
4634}
4635
4636/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
4637/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
4638/// in the offset walk. `Ignore` causes the function to skip NULL
4639/// values in the argument expression, returning the next non-NULL.
4640#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4641pub enum NullTreatment {
4642 #[default]
4643 Respect,
4644 Ignore,
4645}
4646
4647/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
4648/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
4649/// where end implicitly = CURRENT ROW.
4650#[derive(Debug, Clone, PartialEq, Eq)]
4651pub struct WindowFrame {
4652 pub kind: FrameKind,
4653 pub start: FrameBound,
4654 pub end: Option<FrameBound>,
4655 /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
4656 /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
4657 /// no-op; CURRENT ROW drops the current row from the frame.
4658 pub exclude: FrameExclusion,
4659}
4660
4661#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4662pub enum FrameExclusion {
4663 /// Default — exclude nothing.
4664 #[default]
4665 NoOthers,
4666 /// Drop the current row from the frame.
4667 CurrentRow,
4668 /// Drop the current row's whole peer group.
4669 Group,
4670 /// Drop the current row's peers but keep the current row.
4671 Ties,
4672}
4673
4674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4675pub enum FrameKind {
4676 Rows,
4677 Range,
4678 /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
4679 /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
4680 /// bounds (no explicit integer offsets) GROUPS behaves identically
4681 /// to RANGE — both consult the peer-group of the current row.
4682 /// Integer offsets are not yet supported; the executor rejects
4683 /// them at run time.
4684 Groups,
4685}
4686
4687#[derive(Debug, Clone, PartialEq, Eq)]
4688pub enum FrameBound {
4689 UnboundedPreceding,
4690 OffsetPreceding(u64),
4691 CurrentRow,
4692 OffsetFollowing(u64),
4693 UnboundedFollowing,
4694 /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
4695 /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
4696 /// interval is folded to its (months, days, micros) components at
4697 /// parse time.
4698 IntervalPreceding {
4699 months: i32,
4700 days: i32,
4701 micros: i64,
4702 },
4703 IntervalFollowing {
4704 months: i32,
4705 days: i32,
4706 micros: i64,
4707 },
4708}
4709
4710impl fmt::Display for FrameBound {
4711 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4712 match self {
4713 Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
4714 Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
4715 Self::CurrentRow => f.write_str("CURRENT ROW"),
4716 Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
4717 Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
4718 Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
4719 Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
4720 }
4721 }
4722}
4723
4724#[derive(Debug, Clone, PartialEq, Eq)]
4725pub enum ExtractField {
4726 Year,
4727 Month,
4728 Day,
4729 Hour,
4730 Minute,
4731 Second,
4732 Microsecond,
4733 /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
4734 /// SPG keeps the integer convention — truncated seconds).
4735 Epoch,
4736 /// Day of week, 0 = Sunday … 6 = Saturday.
4737 Dow,
4738 /// ISO day of week, 1 = Monday … 7 = Sunday.
4739 Isodow,
4740 /// Day of year, 1-366.
4741 Doy,
4742 /// ISO 8601 week number, 1-53.
4743 Week,
4744 /// ISO 8601 week-numbering year (pairs with `Week`).
4745 Isoyear,
4746 /// Quarter, 1-4.
4747 Quarter,
4748 /// Year divided by 10 (floor).
4749 Decade,
4750 /// Century — 2001-2100 is century 21.
4751 Century,
4752 /// Millennium — 2001-3000 is millennium 3.
4753 Millennium,
4754 /// Julian day number (truncated for timestamps).
4755 Julian,
4756 /// Seconds and fraction in milliseconds (ss·1000 + frac).
4757 Millisecond,
4758 /// UTC offset in seconds — SPG sessions run UTC, so 0.
4759 Timezone,
4760 /// Hour component of the UTC offset — 0.
4761 TimezoneHour,
4762 /// Minute component of the UTC offset — 0.
4763 TimezoneMinute,
4764 /// v7.39 (round 253) — a field name the parser does not know. PG
4765 /// resolves EXTRACT fields at RUNTIME and reports them with the
4766 /// source type (`unit "nosuch" not recognized for type timestamp
4767 /// without time zone`, 22023), so the parser carries the raw name
4768 /// instead of rejecting.
4769 Other(String),
4770}
4771
4772impl fmt::Display for ExtractField {
4773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4774 f.write_str(match self {
4775 Self::Year => "YEAR",
4776 Self::Month => "MONTH",
4777 Self::Day => "DAY",
4778 Self::Hour => "HOUR",
4779 Self::Minute => "MINUTE",
4780 Self::Second => "SECOND",
4781 Self::Microsecond => "MICROSECOND",
4782 Self::Epoch => "EPOCH",
4783 Self::Dow => "DOW",
4784 Self::Isodow => "ISODOW",
4785 Self::Doy => "DOY",
4786 Self::Week => "WEEK",
4787 Self::Isoyear => "ISOYEAR",
4788 Self::Quarter => "QUARTER",
4789 Self::Decade => "DECADE",
4790 Self::Century => "CENTURY",
4791 Self::Millennium => "MILLENNIUM",
4792 Self::Julian => "JULIAN",
4793 Self::Millisecond => "MILLISECOND",
4794 Self::Timezone => "TIMEZONE",
4795 Self::TimezoneHour => "TIMEZONE_HOUR",
4796 Self::TimezoneMinute => "TIMEZONE_MINUTE",
4797 Self::Other(name) => return f.write_str(name),
4798 })
4799 }
4800}
4801
4802#[derive(Debug, Clone, PartialEq, Eq)]
4803pub enum CastTarget {
4804 Int,
4805 BigInt,
4806 Float,
4807 Text,
4808 Bool,
4809 Vector,
4810 Date,
4811 Timestamp,
4812 /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
4813 /// H3a. Engine reuses the existing runtime-interval / timestamp
4814 /// paths (parse the text input, return the matching Value).
4815 Interval,
4816 Timestamptz,
4817 /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
4818 /// types (v7.9.0); the cast just routes Text→Json with the
4819 /// requested OID for the wire layer.
4820 Json,
4821 Jsonb,
4822 /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
4823 /// compatibility; engine surfaces as Unsupported with a
4824 /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
4825 RegType,
4826 RegClass,
4827 /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
4828 /// the PG external array form `{a,b,NULL}`.
4829 TextArray,
4830 /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
4831 /// `{1,2,3}` or widens a `TextArray` whose elements are
4832 /// integer-shaped.
4833 IntArray,
4834 BigIntArray,
4835 /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
4836 /// external form text representation. Used by pg_dump output
4837 /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
4838 TsVector,
4839 TsQuery,
4840 /// v7.17.0 — `::uuid`. Decodes the LHS Text via
4841 /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
4842 /// unhyphenated, uppercase, and brace-wrapped forms); malformed
4843 /// input is a SQL error.
4844 Uuid,
4845 /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
4846 /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
4847 /// inputs pass through unchanged. Closes the mailrs D-pre #3
4848 /// reverse-acceptance gap — anywhere a PG schema writes
4849 /// `expr::bytea`, SPG now matches.
4850 Bytea,
4851 /// v7.37.5 ship triage — generic cast target for the long tail
4852 /// of PG type names the parser meets in `expr::TYPE` shapes that
4853 /// don't deserve their own enum variant. The engine routes these
4854 /// through `column_type_to_data_type` + the existing typed
4855 /// `coerce_value` dispatch, so adding a new PG type to SPG
4856 /// implicitly adds its cast-target form too — no parser change
4857 /// per type. The string carries the lowercase PG type ident
4858 /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
4859 /// a clear message when the type isn't known.
4860 Named(String),
4861}
4862
4863impl fmt::Display for CastTarget {
4864 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4865 f.write_str(match self {
4866 Self::Int => "int",
4867 Self::BigInt => "bigint",
4868 Self::Float => "float",
4869 Self::Text => "text",
4870 Self::Bool => "bool",
4871 Self::Vector => "vector",
4872 Self::Interval => "interval",
4873 Self::Timestamptz => "timestamptz",
4874 Self::Json => "json",
4875 Self::Jsonb => "jsonb",
4876 Self::RegType => "regtype",
4877 Self::RegClass => "regclass",
4878 Self::Date => "date",
4879 Self::Timestamp => "timestamp",
4880 Self::TextArray => "TEXT[]",
4881 Self::IntArray => "INT[]",
4882 Self::BigIntArray => "BIGINT[]",
4883 Self::TsVector => "tsvector",
4884 Self::TsQuery => "tsquery",
4885 Self::Uuid => "uuid",
4886 Self::Bytea => "bytea",
4887 // v7.37.5 — `Self::Named` carries its own canonical name.
4888 Self::Named(name) => return f.write_str(name),
4889 })
4890 }
4891}
4892
4893#[derive(Debug, Clone, PartialEq)]
4894pub enum Literal {
4895 Integer(i64),
4896 Float(f64),
4897 /// Exact decimal literal — a bare `12.34`-style token, kept as
4898 /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
4899 /// before it becomes a `Value::Numeric`. PG parses such literals as
4900 /// `numeric`, not `double precision`. (Scientific/huge literals stay
4901 /// `Float`.)
4902 Numeric {
4903 unscaled: i128,
4904 /// v7.39 (round 271) — widened to u16. At u8 a literal with more
4905 /// than 255 decimal places could not be represented, and the
4906 /// conversion's `.expect("lexer-validated decimal")` aborted the
4907 /// query with an internal error on SQL PG accepts.
4908 scale: u16,
4909 },
4910 /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
4911 /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
4912 /// `Value::NumericBig` at eval; previously such literals fell back to double.
4913 NumericBig(String),
4914 String(String),
4915 /// v7.38.8 — a temporal constant that has already been decoded.
4916 ///
4917 /// Without these the only way to carry one through the AST was as
4918 /// text, and a predicate comparing a `timestamp` column against a
4919 /// literal then coerced that text back into a timestamp ONCE PER
4920 /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
4921 /// profile. `constfold` produced text for the same reason: its exit
4922 /// had nothing else to hand back.
4923 ///
4924 /// `text` keeps the spelling so `Display` round-trips byte for byte,
4925 /// the way `Interval` already does and for the same reason: this
4926 /// node is printed in EXPLAIN, in dumps and in error messages, and
4927 /// none of those should change because the value stopped being
4928 /// carried as a string. The enum already holds a `String` and an
4929 /// `i128`, so neither variant widens it.
4930 Timestamp {
4931 micros: i64,
4932 text: String,
4933 },
4934 /// Days since the epoch `Value::Date` counts from. See
4935 /// [`Literal::Timestamp`].
4936 Date {
4937 days: i32,
4938 text: String,
4939 },
4940 Bool(bool),
4941 Null,
4942 /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
4943 Vector(Vec<f32>),
4944 /// TEXT[] value carried through the prepared-bind path
4945 /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
4946 /// text form, so the array rides the AST natively).
4947 TextArray(Vec<Option<String>>),
4948 /// INT[] value carried through the prepared-bind path.
4949 IntArray(Vec<Option<i32>>),
4950 /// BIGINT[] value carried through the prepared-bind path.
4951 BigIntArray(Vec<Option<i64>>),
4952 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
4953 /// Three independent dimensions: `months` (variable-length;
4954 /// year/month), `days` (fixed 86400 seconds at non-DST, but
4955 /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
4956 /// stays distinguishable), and `micros` (sub-day; can carry).
4957 /// `text` keeps the original spelling so Display round-trips
4958 /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
4959 Interval {
4960 months: i32,
4961 days: i32,
4962 micros: i64,
4963 text: String,
4964 },
4965}
4966
4967#[derive(Debug, Clone, PartialEq, Eq)]
4968pub struct ColumnName {
4969 pub qualifier: Option<String>,
4970 pub name: String,
4971}
4972
4973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4974pub enum BinOp {
4975 Or,
4976 And,
4977 Eq,
4978 NotEq,
4979 /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
4980 /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
4981 /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
4982 /// non-NULL behaviour matches `<>` / `=` exactly. Common in
4983 /// PG-style JOIN ON predicates and pg_dump output.
4984 IsDistinctFrom,
4985 IsNotDistinctFrom,
4986 /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
4987 /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
4988 /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
4989 /// is a real division (round 351).
4990 IntDiv,
4991 Lt,
4992 LtEq,
4993 Gt,
4994 GtEq,
4995 Add,
4996 Sub,
4997 Mul,
4998 Div,
4999 /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
5000 /// precedence as Mul/Div; result type follows left operand.
5001 Mod,
5002 /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
5003 /// operands of equal dimension; engine returns `Value::Float(d)`.
5004 L2Distance,
5005 /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
5006 GeomParallel,
5007 /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
5008 OverLeft,
5009 OverRight,
5010 /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
5011 GeomPerp,
5012 /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
5013 GeomSameAs,
5014 /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
5015 /// object to the left-hand one.
5016 ClosestPoint,
5017 /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
5018 GeomHoriz,
5019 /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
5020 /// more similar" remains true (matches pgvector's published convention).
5021 InnerProduct,
5022 /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
5023 CosineDistance,
5024 /// SQL string concatenation `||`. NULL propagates.
5025 Concat,
5026 /// Bitwise OR `|` on integers.
5027 BitOr,
5028 /// Bitwise AND `&` on integers.
5029 BitAnd,
5030 /// Bitwise XOR `#` on integers and equal-length bit strings.
5031 BitXor,
5032 /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
5033 /// sides as truth values and returns their exclusive-or (`1 XOR 0`
5034 /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
5035 /// MySQL dialect produces it; PG has no logical XOR. Its precedence
5036 /// sits between OR (loosest) and AND.
5037 LogicalXor,
5038 /// v4.14 `json -> key` — element access by string key (object)
5039 /// or integer index (array). Returns a JSON value.
5040 JsonGet,
5041 /// v4.14 `json ->> key` — same access, returns the result as
5042 /// TEXT (unwraps a top-level JSON string; renders other scalars
5043 /// as their canonical text).
5044 JsonGetText,
5045 /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
5046 /// text array literal like `'{a,0,b}'`. Returns JSON.
5047 JsonGetPath,
5048 /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
5049 JsonGetPathText,
5050 /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
5051 /// when every key/value in `sub_json` is structurally present in
5052 /// the left side. Matches PG semantics (top-level + recursive).
5053 JsonContains,
5054 /// `@?` — jsonb path existence (jsonb_path_exists).
5055 JsonPathExists,
5056 /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
5057 /// `a <@ b` is defined as `b @> a` (same semantics, swapped
5058 /// sides). Eval dispatch reuses `JsonContains` with swapped args.
5059 JsonContainedBy,
5060 /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
5061 /// returns BOOL. For an object, true if `key` is an existing
5062 /// member name; for an array, true if any element is the string
5063 /// `key` (PG semantics).
5064 JsonKeyExists,
5065 /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
5066 /// returns BOOL.
5067 JsonKeysAny,
5068 /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
5069 /// returns BOOL.
5070 JsonKeysAll,
5071 /// `jsonb #- path_text[]` — delete the value at a nested path.
5072 /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
5073 JsonDeletePath,
5074 /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
5075 /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
5076 /// tsvector` and engine eval normalises either ordering.
5077 TsMatch,
5078 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
5079 /// `<<`. LHS network is strictly inside RHS network (no equality).
5080 InetContainedBy,
5081 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
5082 /// `<<=`. LHS network ⊆ RHS network.
5083 InetContainedByEq,
5084 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
5085 /// LHS network strictly contains RHS network.
5086 InetContains,
5087 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
5088 /// LHS network ⊇ RHS network.
5089 InetContainsEq,
5090 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
5091 /// True iff either network contains any address of the other.
5092 InetOverlap,
5093 /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
5094 /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
5095 Intersects,
5096 /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
5097 /// (point, box).
5098 IsBelow,
5099 IsAbove,
5100 /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
5101 /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
5102 /// where `'A' < 'a'` is false under a non-C collation, which is the
5103 /// whole reason the operator family exists — it is what makes a LIKE
5104 /// prefix index-usable. pg_dump writes these into index definitions.
5105 PatternLt,
5106 PatternLtEq,
5107 PatternGt,
5108 PatternGtEq,
5109}
5110
5111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5112pub enum UnOp {
5113 Not,
5114 Neg,
5115 /// Bitwise NOT `~` on integers.
5116 BitNot,
5117 /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
5118 /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
5119 /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
5120 /// while PG18 and MariaDB accept every one of them.
5121 ///
5122 /// It is not a no-op to drop at parse time — PG refuses it on
5123 /// non-numeric operands ("operator does not exist: + boolean"), so the
5124 /// operand's type has to be seen at eval.
5125 Plus,
5126}
5127
5128// --- Display impls (round-trip-safe) --------------------------------------
5129
5130impl Statement {
5131 /// v7.18 — classify whether the statement is read-only at
5132 /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
5133 /// route SELECT-shaped traffic through the fan-out
5134 /// `AsyncReadHandle` (no writer-lock contention) while
5135 /// keeping DML / DDL / TX-control on the single-writer path.
5136 ///
5137 /// The classification matches what
5138 /// `Engine::execute_readonly_with_cancel` accepts: anything
5139 /// that does NOT mutate catalog, statistics, session state,
5140 /// or transaction state. WaitForWalPosition is included
5141 /// (engine returns `Unsupported`, but the classification is
5142 /// semantically read-only — no mutation). Empty is excluded
5143 /// out of an abundance of caution — the no-op routes
5144 /// through the writer so any future side effect lands
5145 /// uniformly.
5146 ///
5147 /// **Not connection-state aware**. `SET LOCAL` / `RESET`
5148 /// affect session parameters and must run on the writer
5149 /// engine that owns the session state; they classify as
5150 /// writer-path here. Same for `BEGIN` / `COMMIT` /
5151 /// `ROLLBACK` / `SAVEPOINT` — transaction control is
5152 /// always writer-path.
5153 /// v7.39 (round 435) — does this statement implicitly COMMIT an open
5154 /// transaction under MySQL?
5155 ///
5156 /// PG runs DDL inside the transaction; MySQL commits before (and after)
5157 /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
5158 /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
5159 /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
5160 /// nested START TRANSACTION; and measured NOT to fire for `CREATE
5161 /// TEMPORARY TABLE`, `SET`, or a SELECT.
5162 ///
5163 /// A positive list, not "everything that is not DML": a statement
5164 /// wrongly listed here commits a client's data early, which is as bad as
5165 /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
5166 /// COMPACT) are left out — a MySQL session never sends them.
5167 #[must_use]
5168 pub fn mysql_implicit_commit(&self) -> bool {
5169 match self {
5170 // MySQL's documented exception, measured on MariaDB 11: a
5171 // TEMPORARY table is not DDL for this purpose and does not
5172 // commit. (Round 435 got this for free because the parser then
5173 // lowered that spelling to `Statement::Empty`; round 436 made it
5174 // a real CREATE TABLE, and the round-435 pin caught it.)
5175 Self::CreateTable(c) => !c.temporary,
5176 // MySQL commits the open transaction and opens a fresh one.
5177 Self::Begin { .. }
5178 | Self::DropTable { .. }
5179 | Self::DropIndex { .. }
5180 | Self::CreateIndex(_)
5181 | Self::AlterIndex { .. }
5182 | Self::AlterTable(_)
5183 | Self::Truncate { .. }
5184 | Self::Analyze { .. }
5185 | Self::CreateStatistics { .. }
5186 | Self::DropStatistics { .. }
5187 | Self::CreateView { .. }
5188 | Self::DropView { .. }
5189 | Self::CreateMaterializedView { .. }
5190 | Self::RefreshMaterializedView { .. }
5191 | Self::DropMaterializedView { .. }
5192 | Self::CreateSequence(_)
5193 | Self::AlterSequence { .. }
5194 | Self::DropSequence { .. }
5195 | Self::CreateFunction(_)
5196 | Self::DropFunction { .. }
5197 | Self::CreateTrigger(_)
5198 | Self::DropTrigger { .. }
5199 | Self::CreateRule(_)
5200 | Self::DropRule { .. }
5201 | Self::CreateType(_)
5202 | Self::DropType { .. }
5203 | Self::AlterTypeAddValue { .. }
5204 | Self::AlterTypeRenameValue { .. }
5205 | Self::CreateDomain(_)
5206 | Self::AlterDomain { .. }
5207 | Self::DropDomain { .. }
5208 | Self::CreateSchema { .. }
5209 | Self::DropSchema { .. }
5210 | Self::CreateUser { .. }
5211 | Self::DropUser { .. }
5212 | Self::Grant { .. }
5213 | Self::Revoke { .. }
5214 | Self::CreatePolicy(_)
5215 | Self::AlterPolicy(_)
5216 | Self::DropPolicy { .. }
5217 | Self::CommentOn { .. }
5218 | Self::CreateExtension { .. } => true,
5219 _ => false,
5220 }
5221 }
5222
5223 #[must_use]
5224 pub fn is_readonly(&self) -> bool {
5225 match self {
5226 // v7.39 (round 288) — SET CONSTRAINTS changes transaction
5227 // state, and IMMEDIATE can run the deferred checks there and
5228 // then; writer-path.
5229 Statement::SetConstraints { .. } => false,
5230 // v7.39 (round 695) — it writes nothing (SPG has no
5231 // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
5232 // writer and a read-only session refuses it there too.
5233 Statement::AlterSystem { .. } => false,
5234 // Same shape: a no-op here, a writer to PG, so a read-only
5235 // session refuses it as PG's would.
5236 Statement::NoOpPreventedInTransaction { .. } => false,
5237 Statement::DropDatabase { .. } => false,
5238 // v7.39 (round 696) — they perform nothing, so nothing is
5239 // written; PG classes LOCK and the OWNED BY pair as writers and
5240 // a read-only session refuses them there.
5241 Statement::ValidateOnly { .. } => false,
5242 // v7.39 (round 750) — a credential rotation persists.
5243 Statement::AlterRolePassword { .. } => true,
5244 Statement::DropAggregate { .. } => false,
5245 // v7.39 (round 547) — records a GUC default in the catalog.
5246 Statement::SetDbRoleSetting(_) => false,
5247 // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5248 // but they name a relation and PG refuses one that is not
5249 // there, so they are not read-only in the sense this asks.
5250 Statement::Maintain { .. } => false,
5251 // v7.39 (round 277) — the prepared-statement surface is
5252 // session state, like SET; writer-path so it lands on the
5253 // engine that owns the session. EXECUTE may also run a
5254 // write, and its body is only known at execution time.
5255 Statement::Prepare { .. }
5256 | Statement::Execute { .. }
5257 | Statement::Deallocate(_)
5258 | Statement::Call(_)
5259 | Statement::PrepareTransaction(_)
5260 | Statement::CreateStatistics { .. }
5261 | Statement::DropStatistics { .. }
5262 // v7.39 (round 318, V51) — KILL signals another connection;
5263 // it must run on the writer path that owns the registry hook.
5264 | Statement::Kill { .. }
5265 // v7.39 (round 320, V53) — DISCARD throws session state away;
5266 // writer path, like SET / RESET.
5267 | Statement::Discard(_) => false,
5268 // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5269 // locks MUTATES the lock table, so it is not a read. Left as
5270 // a read it went to the read-only executor and the locking
5271 // pre-pass never ran at all — the clause was honoured only
5272 // inside an explicit transaction, and silently ignored in
5273 // autocommit, which is where a queue worker runs it.
5274 Statement::Select(s) if s.locking.is_some() => false,
5275 Statement::Select(_)
5276 | Statement::CopyTo { .. }
5277 | Statement::CopyToFile { .. }
5278 | Statement::Explain(_)
5279 | Statement::ShowTables
5280 | Statement::ShowDatabases
5281 | Statement::ShowCreateTable(_)
5282 | Statement::ShowIndexes(_)
5283 | Statement::ShowStatus
5284 | Statement::ShowVariables
5285 | Statement::ShowVariablesLike(_)
5286 | Statement::ShowProcesslist
5287 | Statement::ShowColumns(_)
5288 | Statement::ShowUsers
5289 | Statement::ShowPublications
5290 | Statement::ShowSubscriptions
5291 | Statement::WaitForWalPosition { .. } => true,
5292 // Everything else mutates catalog, statistics,
5293 // session state, or transaction state — writer path.
5294 // Listed explicitly so a new Statement variant fails
5295 // the match exhaustiveness check and forces a
5296 // classification decision at add-site.
5297 Statement::Empty
5298 // v7.39 (round 169) — VACUUM mutates storage (reclaims
5299 // tombstoned versions): writer path.
5300 | Statement::Vacuum { .. }
5301 | Statement::DropTable { .. }
5302 | Statement::DropIndex { .. }
5303 | Statement::CreateTable(_)
5304 | Statement::CreateExtension(_)
5305 | Statement::DoBlock(_)
5306 | Statement::CreateIndex(_)
5307 | Statement::Insert(_)
5308 | Statement::Update(_)
5309 | Statement::Delete(_)
5310 | Statement::Merge(_)
5311 | Statement::Begin(_)
5312 | Statement::Commit
5313 | Statement::Rollback
5314 | Statement::Savepoint(_)
5315 | Statement::RollbackToSavepoint(_)
5316 | Statement::ReleaseSavepoint(_)
5317 | Statement::CreateUser(_)
5318 | Statement::DropUser { .. }
5319 | Statement::SetRole(_)
5320 | Statement::Grant(_)
5321 | Statement::Revoke(_)
5322 | Statement::CreatePolicy(_)
5323 | Statement::AlterPolicy(_)
5324 | Statement::DropPolicy(_)
5325 | Statement::AlterIndex(_)
5326 | Statement::AlterTable(_)
5327 | Statement::CreatePublication(_)
5328 | Statement::DropPublication { .. }
5329 | Statement::CreateSubscription(_)
5330 | Statement::DropSubscription { .. }
5331 | Statement::Analyze(_)
5332 | Statement::Truncate { .. }
5333 | Statement::CompactColdSegments
5334 | Statement::SetParameter { .. }
5335 | Statement::SetParameterList(_)
5336 | Statement::SetUserVars(..)
5337 | Statement::SetTransaction { .. }
5338 | Statement::ShowParameter(_)
5339 | Statement::ResetParameter(_)
5340 | Statement::CreateFunction(_)
5341 | Statement::CreateTrigger(_)
5342 | Statement::DropTrigger { .. }
5343 | Statement::CreateRule(_)
5344 | Statement::DropRule { .. }
5345 | Statement::DropFunction { .. }
5346 | Statement::CreateSequence(_)
5347 | Statement::AlterSequence(_)
5348 | Statement::DropSequence { .. }
5349 | Statement::CreateView(_)
5350 | Statement::DropView { .. }
5351 | Statement::CreateMaterializedView(_)
5352 | Statement::RefreshMaterializedView { .. }
5353 | Statement::DropMaterializedView { .. }
5354 | Statement::CreateType(_)
5355 | Statement::AlterTypeAddValue { .. }
5356 | Statement::AlterTypeRenameValue { .. }
5357 | Statement::CommentOn { .. }
5358 | Statement::DropType { .. }
5359 | Statement::CreateDomain(_)
5360 | Statement::DropDomain { .. }
5361 | Statement::CreateSchema { .. }
5362 | Statement::DropSchema { .. }
5363 // v7.39 (round 218) — cursors mutate per-session cursor state
5364 // (open/position/close) on the writer engine: writer path.
5365 | Statement::DeclareCursor { .. }
5366 | Statement::FetchCursor { .. }
5367 | Statement::MoveCursor { .. }
5368 | Statement::CloseCursor { .. }
5369 // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5370 // state / the notification queue: writer path.
5371 | Statement::Listen(_)
5372 | Statement::Notify { .. }
5373 | Statement::Unlisten(_)
5374 | Statement::CopyFromFile { .. }
5375 | Statement::AlterDomain { .. } => false,
5376 }
5377 }
5378}
5379
5380/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5381/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5382#[derive(Debug, Clone, PartialEq, Eq)]
5383pub struct GrantStatement {
5384 /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5385 /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5386 /// is why they keep the case the user typed.
5387 pub privileges: Vec<GrantPriv>,
5388 /// What the privileges are on.
5389 pub object: GrantObject,
5390 /// The roles granted to / revoked from. An empty string entry = PUBLIC.
5391 pub grantees: Vec<String>,
5392 /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
5393 /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
5394 /// privilege itself).
5395 pub grant_option: bool,
5396}
5397
5398/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
5399/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
5400/// An empty column list means the privilege is table-wide.
5401#[derive(Debug, Clone, PartialEq, Eq)]
5402pub struct GrantPriv {
5403 pub word: String,
5404 pub columns: Vec<String>,
5405}
5406
5407/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
5408/// privileges; every other object class parses and is accepted as a no-op, so
5409/// a pg_dump that grants on schemas / sequences / functions still restores.
5410#[derive(Debug, Clone, PartialEq, Eq)]
5411pub enum GrantObject {
5412 /// `ON [TABLE] a, b` — the enforced case.
5413 Tables(Vec<String>),
5414 /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
5415 /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
5416 /// granted roles; the grantees are the members.
5417 Roles(Vec<String>),
5418 /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
5419 /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
5420 Sequences(Vec<String>),
5421 /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
5422 Schemas(Vec<String>),
5423 /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
5424 Databases(Vec<String>),
5425 /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
5426 /// (SPG keys functions by name); the argument list parses and is dropped.
5427 Functions(Vec<(String, Option<Vec<String>>)>),
5428 /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
5429 /// every table at GRANT time, exactly like PG.
5430 AllTablesInSchema,
5431 /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
5432 /// message.
5433 Other(String),
5434}
5435
5436impl GrantStatement {
5437 /// Round-trip text. `grant = false` renders the REVOKE form.
5438 fn render(&self, grant: bool) -> alloc::string::String {
5439 use core::fmt::Write as _;
5440 let mut s = alloc::string::String::new();
5441 let privs = if self.privileges.is_empty() {
5442 alloc::string::String::from("ALL")
5443 } else {
5444 let parts: Vec<_> = self
5445 .privileges
5446 .iter()
5447 .map(|p| {
5448 if p.columns.is_empty() {
5449 p.word.clone()
5450 } else {
5451 let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
5452 alloc::format!("{} ({})", p.word, cols.join(", "))
5453 }
5454 })
5455 .collect();
5456 parts.join(", ")
5457 };
5458 let obj = match &self.object {
5459 GrantObject::Tables(t) => {
5460 let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
5461 alloc::format!("TABLE {}", names.join(", "))
5462 }
5463 GrantObject::Roles(r) => {
5464 let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
5465 names.join(", ")
5466 }
5467 GrantObject::Sequences(n) => {
5468 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5469 alloc::format!("SEQUENCE {}", names.join(", "))
5470 }
5471 GrantObject::Schemas(n) => {
5472 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5473 alloc::format!("SCHEMA {}", names.join(", "))
5474 }
5475 GrantObject::Databases(n) => {
5476 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5477 alloc::format!("DATABASE {}", names.join(", "))
5478 }
5479 GrantObject::Functions(n) => {
5480 let names: Vec<_> = n
5481 .iter()
5482 .map(|(name, args)| match args {
5483 Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
5484 None => quote_ident(name),
5485 })
5486 .collect();
5487 alloc::format!("FUNCTION {}", names.join(", "))
5488 }
5489 GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
5490 GrantObject::Other(k) => k.clone(),
5491 };
5492 let who: Vec<_> = self
5493 .grantees
5494 .iter()
5495 .map(|g| {
5496 if g.is_empty() {
5497 "PUBLIC".into()
5498 } else {
5499 quote_ident(g)
5500 }
5501 })
5502 .collect();
5503 if let GrantObject::Roles(_) = &self.object {
5504 let _ = if grant {
5505 write!(s, "GRANT {obj} TO {}", who.join(", "))
5506 } else {
5507 write!(s, "REVOKE {obj} FROM {}", who.join(", "))
5508 };
5509 return s;
5510 }
5511 if grant {
5512 let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
5513 if self.grant_option {
5514 s.push_str(" WITH GRANT OPTION");
5515 }
5516 } else {
5517 s.push_str("REVOKE ");
5518 if self.grant_option {
5519 s.push_str("GRANT OPTION FOR ");
5520 }
5521 let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
5522 }
5523 s
5524 }
5525}
5526
5527impl fmt::Display for Statement {
5528 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5529 match self {
5530 Self::Empty => Ok(()),
5531 // v7.39 (round 695) — deparsed the way PG writes it.
5532 // v7.39 (round 696) — never deparsed into a dump (nothing is
5533 // stored), so the shortest faithful spelling of what it was.
5534 Self::DropAggregate { if_exists, items } => {
5535 f.write_str("DROP AGGREGATE ")?;
5536 if *if_exists {
5537 f.write_str("IF EXISTS ")?;
5538 }
5539 for (i, (name, args)) in items.iter().enumerate() {
5540 if i > 0 {
5541 f.write_str(", ")?;
5542 }
5543 match args {
5544 Some(a) => write!(f, "{name}({})", a.join(", "))?,
5545 None => write!(f, "{name}(*)")?,
5546 }
5547 }
5548 Ok(())
5549 }
5550 Self::AlterRolePassword { name, password } => {
5551 write!(f, "ALTER ROLE {}", quote_ident(name))?;
5552 match password {
5553 Some(_) => f.write_str(" PASSWORD '<redacted>'"),
5554 None => f.write_str(" PASSWORD NULL"),
5555 }
5556 }
5557 Self::ValidateOnly { kind, names } => match kind {
5558 ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
5559 ValidateOnlyKind::RoleName => {
5560 write!(f, "DROP OWNED BY {}", names.join(", "))
5561 }
5562 ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
5563 ValidateOnlyKind::ExtensionAvailable => {
5564 write!(f, "CREATE EXTENSION {}", names.join(", "))
5565 }
5566 ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
5567 ValidateOnlyKind::CollationName => {
5568 write!(f, "DROP COLLATION {}", names.join(", "))
5569 }
5570 ValidateOnlyKind::TsConfigName => {
5571 write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
5572 }
5573 ValidateOnlyKind::EventTriggerName => {
5574 write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
5575 }
5576 ValidateOnlyKind::TablespaceName => {
5577 write!(f, "DROP TABLESPACE {}", names.join(", "))
5578 }
5579 ValidateOnlyKind::LargeObjectOid => {
5580 write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
5581 }
5582 ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
5583 ValidateOnlyKind::AggregateName => {
5584 write!(f, "ALTER AGGREGATE {}", names.join(", "))
5585 }
5586 ValidateOnlyKind::ConversionName => {
5587 write!(f, "DROP CONVERSION {}", names.join(", "))
5588 }
5589 ValidateOnlyKind::LanguageName => {
5590 write!(f, "DROP LANGUAGE {}", names.join(", "))
5591 }
5592 ValidateOnlyKind::ExtensionInstalled => {
5593 write!(f, "DROP EXTENSION {}", names.join(", "))
5594 }
5595 },
5596 Self::AlterSystem { parameter } => match parameter {
5597 Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
5598 None => f.write_str("ALTER SYSTEM RESET ALL"),
5599 },
5600 // v7.39 (round 547) — round-trips as PG writes it.
5601 Self::SetDbRoleSetting(st) => {
5602 match (&st.database, &st.role) {
5603 (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
5604 (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
5605 (None, None) => f.write_str("ALTER ROLE ALL")?,
5606 }
5607 if let (Some(d), Some(_)) = (&st.database, &st.role) {
5608 write!(f, " IN DATABASE {d}")?;
5609 }
5610 match (&st.param, &st.value) {
5611 (None, _) => f.write_str(" RESET ALL"),
5612 (Some(p), None) => write!(f, " RESET {p}"),
5613 (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
5614 }
5615 }
5616 Self::Maintain {
5617 kind,
5618 concurrently,
5619 target,
5620 } => {
5621 f.write_str(match kind {
5622 crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
5623 _ => "REINDEX ",
5624 })?;
5625 if *concurrently {
5626 f.write_str("CONCURRENTLY ")?;
5627 }
5628 if let Some(t) = target {
5629 f.write_str(t)?;
5630 }
5631 Ok(())
5632 }
5633 Self::DropDatabase { name, if_exists } => {
5634 f.write_str("DROP DATABASE ")?;
5635 if *if_exists {
5636 f.write_str("IF EXISTS ")?;
5637 }
5638 f.write_str(name)
5639 }
5640 Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
5641 Self::SetConstraints { names, deferred } => {
5642 f.write_str("SET CONSTRAINTS ")?;
5643 if names.is_empty() {
5644 f.write_str("ALL")?;
5645 } else {
5646 for (i, n) in names.iter().enumerate() {
5647 if i > 0 {
5648 f.write_str(", ")?;
5649 }
5650 f.write_str(n)?;
5651 }
5652 }
5653 f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
5654 }
5655 // v7.39 (round 277) — the source text is kept verbatim so
5656 // `pg_prepared_statements.statement` can report it the way
5657 // PG does (the whole PREPARE statement, not just the body).
5658 Self::Prepare { source, .. } => f.write_str(source),
5659 Self::Execute { name, args } => {
5660 write!(f, "EXECUTE {}", quote_ident(name))?;
5661 if !args.is_empty() {
5662 f.write_str("(")?;
5663 for (i, a) in args.iter().enumerate() {
5664 if i > 0 {
5665 f.write_str(", ")?;
5666 }
5667 write!(f, "{a}")?;
5668 }
5669 f.write_str(")")?;
5670 }
5671 Ok(())
5672 }
5673 Self::CreateStatistics {
5674 name,
5675 if_not_exists,
5676 kinds,
5677 columns,
5678 table,
5679 } => {
5680 f.write_str("CREATE STATISTICS ")?;
5681 if *if_not_exists {
5682 f.write_str("IF NOT EXISTS ")?;
5683 }
5684 write!(f, "{}", quote_ident(name))?;
5685 if !kinds.is_empty() {
5686 write!(f, " ({})", kinds.join(", "))?;
5687 }
5688 write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
5689 }
5690 Self::DropStatistics { name, if_exists } => {
5691 f.write_str("DROP STATISTICS ")?;
5692 if *if_exists {
5693 f.write_str("IF EXISTS ")?;
5694 }
5695 write!(f, "{}", quote_ident(name))
5696 }
5697 Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
5698 Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
5699 Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
5700 Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
5701 Self::DeclareCursor {
5702 name,
5703 scroll,
5704 hold,
5705 query,
5706 } => {
5707 write!(f, "DECLARE {} ", quote_ident(name))?;
5708 match scroll {
5709 Some(true) => f.write_str("SCROLL ")?,
5710 Some(false) => f.write_str("NO SCROLL ")?,
5711 None => {}
5712 }
5713 f.write_str("CURSOR ")?;
5714 if *hold {
5715 f.write_str("WITH HOLD ")?;
5716 }
5717 write!(f, "FOR {query}")
5718 }
5719 Self::FetchCursor { name, direction } => {
5720 write!(f, "FETCH {direction} FROM {}", quote_ident(name))
5721 }
5722 Self::MoveCursor { name, direction } => {
5723 write!(f, "MOVE {direction} FROM {}", quote_ident(name))
5724 }
5725 Self::CloseCursor { name } => match name {
5726 Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
5727 None => f.write_str("CLOSE ALL"),
5728 },
5729 Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
5730 Self::Notify { channel, payload } => {
5731 write!(f, "NOTIFY {}", quote_ident(channel))?;
5732 if let Some(p) = payload {
5733 write!(f, ", '{}'", p.replace('\'', "''"))?;
5734 }
5735 Ok(())
5736 }
5737 Self::Unlisten(ch) => match ch {
5738 Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
5739 None => f.write_str("UNLISTEN *"),
5740 },
5741 Self::CopyTo {
5742 table,
5743 columns,
5744 query,
5745 options,
5746 } => {
5747 if let Some(q) = query {
5748 write!(f, "COPY ({q})")?;
5749 } else {
5750 write!(f, "COPY {table}")?;
5751 if let Some(cols) = columns {
5752 write!(f, " ({})", cols.join(", "))?;
5753 }
5754 }
5755 write!(f, " TO STDOUT")?;
5756 let mut parts: Vec<String> = Vec::new();
5757 if options.format == CopyFormat::Csv {
5758 parts.push("FORMAT csv".to_string());
5759 }
5760 if options.header {
5761 parts.push("HEADER true".to_string());
5762 }
5763 if let Some(d) = options.delimiter {
5764 parts.push(alloc::format!("DELIMITER '{d}'"));
5765 }
5766 if let Some(n) = &options.null_str {
5767 parts.push(alloc::format!("NULL '{n}'"));
5768 }
5769 if let Some(q) = options.quote {
5770 parts.push(alloc::format!("QUOTE '{q}'"));
5771 }
5772 if !parts.is_empty() {
5773 write!(f, " WITH ({})", parts.join(", "))?;
5774 }
5775 Ok(())
5776 }
5777 Self::CopyFromFile {
5778 table,
5779 columns,
5780 path,
5781 options,
5782 } => {
5783 write!(f, "COPY {table}")?;
5784 if let Some(cols) = columns {
5785 write!(f, " ({})", cols.join(", "))?;
5786 }
5787 write!(f, " FROM '{path}'")?;
5788 let mut parts: Vec<String> = Vec::new();
5789 if options.format == CopyFormat::Csv {
5790 parts.push("FORMAT csv".to_string());
5791 }
5792 if options.header {
5793 parts.push("HEADER true".to_string());
5794 }
5795 if let Some(d) = options.delimiter {
5796 parts.push(alloc::format!("DELIMITER '{d}'"));
5797 }
5798 if let Some(n) = &options.null_str {
5799 parts.push(alloc::format!("NULL '{n}'"));
5800 }
5801 if let Some(q) = options.quote {
5802 parts.push(alloc::format!("QUOTE '{q}'"));
5803 }
5804 if !parts.is_empty() {
5805 write!(f, " WITH ({})", parts.join(", "))?;
5806 }
5807 Ok(())
5808 }
5809 Self::CopyToFile {
5810 table,
5811 columns,
5812 query,
5813 path,
5814 options,
5815 } => {
5816 if let Some(q) = query {
5817 write!(f, "COPY ({q})")?;
5818 } else {
5819 write!(f, "COPY {table}")?;
5820 if let Some(cols) = columns {
5821 write!(f, " ({})", cols.join(", "))?;
5822 }
5823 }
5824 write!(f, " TO '{path}'")?;
5825 let mut parts: Vec<String> = Vec::new();
5826 if options.format == CopyFormat::Csv {
5827 parts.push("FORMAT csv".to_string());
5828 }
5829 if options.header {
5830 parts.push("HEADER true".to_string());
5831 }
5832 if let Some(d) = options.delimiter {
5833 parts.push(alloc::format!("DELIMITER '{d}'"));
5834 }
5835 if let Some(n) = &options.null_str {
5836 parts.push(alloc::format!("NULL '{n}'"));
5837 }
5838 if let Some(q) = options.quote {
5839 parts.push(alloc::format!("QUOTE '{q}'"));
5840 }
5841 if !parts.is_empty() {
5842 write!(f, " WITH ({})", parts.join(", "))?;
5843 }
5844 Ok(())
5845 }
5846 Self::AlterDomain { name, action } => {
5847 write!(f, "ALTER DOMAIN {name} ")?;
5848 match action {
5849 AlterDomainAction::AddConstraint { name: cn, check } => match cn {
5850 Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
5851 None => write!(f, "ADD CHECK ({check})"),
5852 },
5853 AlterDomainAction::DropConstraint {
5854 name: cn,
5855 if_exists,
5856 } => {
5857 if *if_exists {
5858 write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
5859 } else {
5860 write!(f, "DROP CONSTRAINT {cn}")
5861 }
5862 }
5863 AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
5864 AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
5865 AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
5866 AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
5867 AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
5868 }
5869 }
5870 Self::Truncate {
5871 tables,
5872 restart_identity,
5873 cascade,
5874 only,
5875 } => {
5876 f.write_str("TRUNCATE TABLE ")?;
5877 if *only {
5878 f.write_str("ONLY ")?;
5879 }
5880 for (i, t) in tables.iter().enumerate() {
5881 if i > 0 {
5882 f.write_str(", ")?;
5883 }
5884 f.write_str(t)?;
5885 }
5886 if *restart_identity {
5887 f.write_str(" RESTART IDENTITY")?;
5888 }
5889 if *cascade {
5890 f.write_str(" CASCADE")?;
5891 }
5892 Ok(())
5893 }
5894 Self::DropTable { names, if_exists } => {
5895 f.write_str("DROP TABLE ")?;
5896 if *if_exists {
5897 f.write_str("IF EXISTS ")?;
5898 }
5899 for (i, n) in names.iter().enumerate() {
5900 if i > 0 {
5901 f.write_str(", ")?;
5902 }
5903 write!(f, "{}", quote_ident(n))?;
5904 }
5905 Ok(())
5906 }
5907 Self::DropIndex { name, if_exists } => {
5908 f.write_str("DROP INDEX ")?;
5909 if *if_exists {
5910 f.write_str("IF EXISTS ")?;
5911 }
5912 write!(f, "{}", quote_ident(name))
5913 }
5914 Self::Select(s) => s.fmt(f),
5915 Self::CreateTable(s) => s.fmt(f),
5916 Self::CreateIndex(s) => s.fmt(f),
5917 Self::Insert(s) => s.fmt(f),
5918 Self::Update(s) => s.fmt(f),
5919 Self::Delete(s) => s.fmt(f),
5920 Self::Merge(s) => s.fmt(f),
5921 Self::Vacuum { table, analyze } => {
5922 f.write_str("VACUUM")?;
5923 if *analyze {
5924 f.write_str(" ANALYZE")?;
5925 }
5926 if let Some(t) = table {
5927 write!(f, " {}", quote_ident(t))?;
5928 }
5929 Ok(())
5930 }
5931 Self::Begin(modes) => {
5932 f.write_str("BEGIN")?;
5933 if let Some(level) = modes.isolation {
5934 write!(f, " ISOLATION LEVEL {level}")?;
5935 }
5936 match modes.read_only {
5937 Some(true) => f.write_str(" READ ONLY")?,
5938 Some(false) => f.write_str(" READ WRITE")?,
5939 None => {}
5940 }
5941 Ok(())
5942 }
5943 Self::Commit => f.write_str("COMMIT"),
5944 Self::Rollback => f.write_str("ROLLBACK"),
5945 Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
5946 Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
5947 Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
5948 Self::ShowTables => f.write_str("SHOW TABLES"),
5949 Self::ShowDatabases => f.write_str("SHOW DATABASES"),
5950 Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
5951 Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
5952 Self::ShowStatus => f.write_str("SHOW STATUS"),
5953 Self::ShowVariables => f.write_str("SHOW VARIABLES"),
5954 Self::ShowVariablesLike(p) => {
5955 write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
5956 }
5957 Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
5958 Self::Discard(t) => write!(f, "DISCARD {t}"),
5959 Self::Kill { query_only, id } => {
5960 if *query_only {
5961 write!(f, "KILL QUERY {id}")
5962 } else {
5963 write!(f, "KILL CONNECTION {id}")
5964 }
5965 }
5966 Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
5967 Self::CreateUser(s) => write!(
5968 f,
5969 "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
5970 quote_ident(&s.name),
5971 s.role
5972 ),
5973 Self::DropUser { name, if_exists } => {
5974 let ie = if *if_exists { "IF EXISTS " } else { "" };
5975 write!(f, "DROP USER {ie}{}", quote_ident(name))
5976 }
5977 Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
5978 Self::SetRole(None) => f.write_str("RESET ROLE"),
5979 Self::Grant(g) => write!(f, "{}", g.render(true)),
5980 Self::Revoke(g) => write!(f, "{}", g.render(false)),
5981 Self::CreatePolicy(s) => {
5982 write!(
5983 f,
5984 "CREATE POLICY {} ON {}",
5985 quote_ident(&s.name),
5986 quote_ident(&s.table)
5987 )?;
5988 if !s.permissive {
5989 f.write_str(" AS RESTRICTIVE")?;
5990 }
5991 if !matches!(s.cmd, PolicyCmd::All) {
5992 let w = match s.cmd {
5993 PolicyCmd::Select => "SELECT",
5994 PolicyCmd::Insert => "INSERT",
5995 PolicyCmd::Update => "UPDATE",
5996 PolicyCmd::Delete => "DELETE",
5997 PolicyCmd::All => unreachable!(),
5998 };
5999 write!(f, " FOR {w}")?;
6000 }
6001 if !s.roles.is_empty() {
6002 write!(f, " TO {}", s.roles.join(", "))?;
6003 }
6004 if let Some(u) = &s.using {
6005 write!(f, " USING ({u})")?;
6006 }
6007 if let Some(c) = &s.with_check {
6008 write!(f, " WITH CHECK ({c})")?;
6009 }
6010 Ok(())
6011 }
6012 Self::AlterPolicy(s) => {
6013 write!(
6014 f,
6015 "ALTER POLICY {} ON {}",
6016 quote_ident(&s.name),
6017 quote_ident(&s.table)
6018 )?;
6019 if let Some(nn) = &s.rename_to {
6020 return write!(f, " RENAME TO {}", quote_ident(nn));
6021 }
6022 if let Some(roles) = &s.roles {
6023 write!(f, " TO {}", roles.join(", "))?;
6024 }
6025 if let Some(u) = &s.using {
6026 write!(f, " USING ({u})")?;
6027 }
6028 if let Some(c) = &s.with_check {
6029 write!(f, " WITH CHECK ({c})")?;
6030 }
6031 Ok(())
6032 }
6033 Self::DropPolicy(s) => {
6034 f.write_str("DROP POLICY ")?;
6035 if s.if_exists {
6036 f.write_str("IF EXISTS ")?;
6037 }
6038 write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
6039 }
6040 Self::ShowUsers => f.write_str("SHOW USERS"),
6041 Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
6042 Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
6043 Self::CreateSubscription(s) => {
6044 write!(
6045 f,
6046 "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
6047 quote_ident(&s.name),
6048 s.conn_str.replace('\'', "''")
6049 )?;
6050 for (i, p) in s.publications.iter().enumerate() {
6051 if i > 0 {
6052 f.write_str(", ")?;
6053 }
6054 write!(f, "{}", quote_ident(p))?;
6055 }
6056 Ok(())
6057 }
6058 Self::DropSubscription { name, if_exists } => {
6059 let opt = if *if_exists { "IF EXISTS " } else { "" };
6060 write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
6061 }
6062 Self::WaitForWalPosition { pos, timeout_ms } => {
6063 write!(f, "WAIT FOR WAL POSITION {pos}")?;
6064 if let Some(ms) = timeout_ms {
6065 write!(f, " WITH TIMEOUT {ms}")?;
6066 }
6067 Ok(())
6068 }
6069 Self::Analyze(None) => f.write_str("ANALYZE"),
6070 Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
6071 Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
6072 Self::Explain(e) => {
6073 if e.suggest {
6074 write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
6075 } else if e.analyze {
6076 write!(f, "EXPLAIN ANALYZE {}", e.inner)
6077 } else {
6078 write!(f, "EXPLAIN {}", e.inner)
6079 }
6080 }
6081 Self::AlterIndex(a) => {
6082 write!(f, "ALTER INDEX ")?;
6083 match &a.target {
6084 // Parameters are consumed, not stored; the shortest
6085 // faithful spelling.
6086 AlterIndexTarget::StorageParams => {
6087 write!(f, "{} SET ()", quote_ident(&a.name))
6088 }
6089 AlterIndexTarget::Rebuild { encoding } => {
6090 write!(f, "{} REBUILD", quote_ident(&a.name))?;
6091 if let Some(enc) = encoding {
6092 write!(f, " WITH (encoding = {enc})")?;
6093 }
6094 Ok(())
6095 }
6096 AlterIndexTarget::Rename { new, if_exists } => {
6097 if *if_exists {
6098 f.write_str("IF EXISTS ")?;
6099 }
6100 write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
6101 }
6102 }
6103 }
6104 Self::AlterTable(a) => {
6105 write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
6106 for (i, t) in a.targets.iter().enumerate() {
6107 if i > 0 {
6108 f.write_str(", ")?;
6109 }
6110 fmt_alter_target(f, t)?;
6111 }
6112 Ok(())
6113 }
6114 Self::CreatePublication(p) => {
6115 write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
6116 match &p.scope {
6117 PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
6118 PublicationScope::ForTables(ts) => {
6119 f.write_str(" FOR TABLE ")?;
6120 for (i, t) in ts.iter().enumerate() {
6121 if i > 0 {
6122 f.write_str(", ")?;
6123 }
6124 write!(f, "{}", quote_ident(t))?;
6125 }
6126 Ok(())
6127 }
6128 PublicationScope::TablesInSchema(schema) => {
6129 write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
6130 Ok(())
6131 }
6132 PublicationScope::AllTablesExcept(ts) => {
6133 f.write_str(" FOR ALL TABLES EXCEPT ")?;
6134 for (i, t) in ts.iter().enumerate() {
6135 if i > 0 {
6136 f.write_str(", ")?;
6137 }
6138 write!(f, "{}", quote_ident(t))?;
6139 }
6140 Ok(())
6141 }
6142 }
6143 }
6144 Self::CreateExtension(name) => {
6145 write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
6146 }
6147 Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
6148 Self::DropPublication { name, if_exists } => {
6149 let opt = if *if_exists { "IF EXISTS " } else { "" };
6150 write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
6151 }
6152 Self::SetParameter { name, value, local } => {
6153 write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
6154 match value {
6155 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
6156 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
6157 SetValue::Default => f.write_str("DEFAULT"),
6158 }
6159 }
6160 Self::SetTransaction { modes } => {
6161 f.write_str("SET TRANSACTION")?;
6162 if let Some(isolation) = modes.isolation {
6163 let name = match isolation {
6164 IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
6165 IsolationLevel::ReadCommitted => "READ COMMITTED",
6166 IsolationLevel::RepeatableRead => "REPEATABLE READ",
6167 IsolationLevel::Serializable => "SERIALIZABLE",
6168 };
6169 write!(f, " ISOLATION LEVEL {name}")?;
6170 }
6171 match modes.read_only {
6172 Some(true) => f.write_str(" READ ONLY")?,
6173 Some(false) => f.write_str(" READ WRITE")?,
6174 None => {}
6175 }
6176 Ok(())
6177 }
6178 Self::ShowParameter(name) => write!(f, "SHOW {name}"),
6179 Self::SetUserVars(assigns, _) => {
6180 f.write_str("SET ")?;
6181 for (i, (name, value)) in assigns.iter().enumerate() {
6182 if i > 0 {
6183 f.write_str(", ")?;
6184 }
6185 write!(f, "@{name} = {value}")?;
6186 }
6187 Ok(())
6188 }
6189 Self::SetParameterList(pairs) => {
6190 f.write_str("SET ")?;
6191 for (i, (name, value)) in pairs.iter().enumerate() {
6192 if i > 0 {
6193 f.write_str(", ")?;
6194 }
6195 write!(f, "{name} = ")?;
6196 match value {
6197 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
6198 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
6199 SetValue::Default => f.write_str("DEFAULT")?,
6200 }
6201 }
6202 Ok(())
6203 }
6204 Self::ResetParameter(None) => f.write_str("RESET ALL"),
6205 Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
6206 Self::CreateFunction(s) => s.fmt(f),
6207 Self::CreateTrigger(s) => s.fmt(f),
6208 Self::DropTrigger {
6209 name,
6210 table,
6211 if_exists,
6212 } => {
6213 f.write_str("DROP TRIGGER ")?;
6214 if *if_exists {
6215 f.write_str("IF EXISTS ")?;
6216 }
6217 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6218 }
6219 Self::DropFunction {
6220 name,
6221 args,
6222 if_exists,
6223 } => {
6224 f.write_str("DROP FUNCTION ")?;
6225 if *if_exists {
6226 f.write_str("IF EXISTS ")?;
6227 }
6228 write!(f, "{}", quote_ident(name))?;
6229 if let Some(a) = args {
6230 write!(f, "({})", a.join(", "))?;
6231 }
6232 Ok(())
6233 }
6234 Self::CreateSequence(s) => s.fmt(f),
6235 Self::AlterSequence(s) => s.fmt(f),
6236 Self::DropSequence { names, if_exists } => {
6237 f.write_str("DROP SEQUENCE ")?;
6238 if *if_exists {
6239 f.write_str("IF EXISTS ")?;
6240 }
6241 for (i, n) in names.iter().enumerate() {
6242 if i > 0 {
6243 f.write_str(", ")?;
6244 }
6245 write!(f, "{}", quote_ident(n))?;
6246 }
6247 Ok(())
6248 }
6249 Self::CreateView(v) => v.fmt(f),
6250 Self::DropView { names, if_exists } => {
6251 f.write_str("DROP VIEW ")?;
6252 if *if_exists {
6253 f.write_str("IF EXISTS ")?;
6254 }
6255 for (i, n) in names.iter().enumerate() {
6256 if i > 0 {
6257 f.write_str(", ")?;
6258 }
6259 write!(f, "{}", quote_ident(n))?;
6260 }
6261 Ok(())
6262 }
6263 Self::CreateMaterializedView(v) => v.fmt(f),
6264 Self::RefreshMaterializedView { name, with_data } => {
6265 write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6266 if !*with_data {
6267 f.write_str(" WITH NO DATA")?;
6268 }
6269 Ok(())
6270 }
6271 Self::DropMaterializedView { names, if_exists } => {
6272 f.write_str("DROP MATERIALIZED VIEW ")?;
6273 if *if_exists {
6274 f.write_str("IF EXISTS ")?;
6275 }
6276 for (i, n) in names.iter().enumerate() {
6277 if i > 0 {
6278 f.write_str(", ")?;
6279 }
6280 write!(f, "{}", quote_ident(n))?;
6281 }
6282 Ok(())
6283 }
6284 Self::CreateType(t) => t.fmt(f),
6285 Self::CommentOn {
6286 kind,
6287 name,
6288 comment,
6289 } => {
6290 let body = match comment {
6291 Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6292 None => "NULL".into(),
6293 };
6294 write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6295 }
6296 Self::AlterTypeRenameValue {
6297 type_name,
6298 old,
6299 new,
6300 } => write!(
6301 f,
6302 "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6303 quote_ident(type_name),
6304 old.replace('\'', "''"),
6305 new.replace('\'', "''")
6306 ),
6307 Self::AlterTypeAddValue {
6308 type_name,
6309 label,
6310 if_not_exists,
6311 position,
6312 } => {
6313 write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6314 if *if_not_exists {
6315 write!(f, "IF NOT EXISTS ")?;
6316 }
6317 write!(f, "'{label}'")?;
6318 if let Some((is_before, anchor)) = position {
6319 write!(
6320 f,
6321 " {} '{anchor}'",
6322 if *is_before { "BEFORE" } else { "AFTER" }
6323 )?;
6324 }
6325 Ok(())
6326 }
6327 Self::DropType { names, if_exists } => {
6328 f.write_str("DROP TYPE ")?;
6329 if *if_exists {
6330 f.write_str("IF EXISTS ")?;
6331 }
6332 for (i, n) in names.iter().enumerate() {
6333 if i > 0 {
6334 f.write_str(", ")?;
6335 }
6336 write!(f, "{}", quote_ident(n))?;
6337 }
6338 Ok(())
6339 }
6340 Self::CreateDomain(d) => d.fmt(f),
6341 Self::DropDomain { names, if_exists } => {
6342 f.write_str("DROP DOMAIN ")?;
6343 if *if_exists {
6344 f.write_str("IF EXISTS ")?;
6345 }
6346 for (i, n) in names.iter().enumerate() {
6347 if i > 0 {
6348 f.write_str(", ")?;
6349 }
6350 write!(f, "{}", quote_ident(n))?;
6351 }
6352 Ok(())
6353 }
6354 Self::CreateSchema {
6355 name,
6356 if_not_exists,
6357 } => {
6358 f.write_str("CREATE SCHEMA ")?;
6359 if *if_not_exists {
6360 f.write_str("IF NOT EXISTS ")?;
6361 }
6362 write!(f, "{}", quote_ident(name))
6363 }
6364 Self::DropSchema { names, if_exists } => {
6365 f.write_str("DROP SCHEMA ")?;
6366 if *if_exists {
6367 f.write_str("IF EXISTS ")?;
6368 }
6369 for (i, n) in names.iter().enumerate() {
6370 if i > 0 {
6371 f.write_str(", ")?;
6372 }
6373 write!(f, "{}", quote_ident(n))?;
6374 }
6375 Ok(())
6376 }
6377 Self::CreateRule(r) => {
6378 f.write_str("CREATE ")?;
6379 if r.or_replace {
6380 f.write_str("OR REPLACE ")?;
6381 }
6382 write!(
6383 f,
6384 "RULE {} AS ON {} TO {}",
6385 quote_ident(&r.name),
6386 r.event,
6387 quote_ident(&r.table)
6388 )?;
6389 if let Some(w) = &r.when_condition {
6390 write!(f, " WHERE {w}")?;
6391 }
6392 f.write_str(if r.instead {
6393 " DO INSTEAD "
6394 } else {
6395 " DO ALSO "
6396 })?;
6397 if r.commands.is_empty() {
6398 f.write_str("NOTHING")?;
6399 } else if r.commands.len() == 1 {
6400 write!(f, "{}", r.commands[0])?;
6401 } else {
6402 f.write_str("(")?;
6403 for (i, c) in r.commands.iter().enumerate() {
6404 if i > 0 {
6405 f.write_str("; ")?;
6406 }
6407 write!(f, "{c}")?;
6408 }
6409 f.write_str(")")?;
6410 }
6411 Ok(())
6412 }
6413 Self::DropRule {
6414 name,
6415 table,
6416 if_exists,
6417 } => {
6418 f.write_str("DROP RULE ")?;
6419 if *if_exists {
6420 f.write_str("IF EXISTS ")?;
6421 }
6422 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6423 }
6424 }
6425 }
6426}
6427
6428impl fmt::Display for CreateDomainStatement {
6429 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6430 write!(
6431 f,
6432 "CREATE DOMAIN {} AS {}",
6433 quote_ident(&self.name),
6434 self.base_type
6435 )?;
6436 if let Some(d) = &self.default {
6437 write!(f, " DEFAULT {d}")?;
6438 }
6439 if self.not_null {
6440 f.write_str(" NOT NULL")?;
6441 }
6442 for c in &self.checks {
6443 write!(f, " CHECK ({c})")?;
6444 }
6445 Ok(())
6446 }
6447}
6448
6449impl fmt::Display for CreateTypeStatement {
6450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6451 write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
6452 match &self.kind {
6453 TypeKind::Enum { labels } => {
6454 f.write_str("ENUM (")?;
6455 for (i, l) in labels.iter().enumerate() {
6456 if i > 0 {
6457 f.write_str(", ")?;
6458 }
6459 write!(f, "'{}'", l.replace('\'', "''"))?;
6460 }
6461 f.write_str(")")
6462 }
6463 TypeKind::Composite { fields, .. } => {
6464 f.write_str("(")?;
6465 for (i, (n, t)) in fields.iter().enumerate() {
6466 if i > 0 {
6467 f.write_str(", ")?;
6468 }
6469 write!(f, "{} {}", quote_ident(n), t)?;
6470 }
6471 f.write_str(")")
6472 }
6473 }
6474 }
6475}
6476
6477impl fmt::Display for CreateMaterializedViewStatement {
6478 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6479 f.write_str("CREATE MATERIALIZED VIEW ")?;
6480 if self.if_not_exists {
6481 f.write_str("IF NOT EXISTS ")?;
6482 }
6483 write!(f, "{}", quote_ident(&self.name))?;
6484 if !self.columns.is_empty() {
6485 f.write_str(" (")?;
6486 for (i, c) in self.columns.iter().enumerate() {
6487 if i > 0 {
6488 f.write_str(", ")?;
6489 }
6490 write!(f, "{}", quote_ident(c))?;
6491 }
6492 f.write_str(")")?;
6493 }
6494 write!(f, " AS {}", self.body)?;
6495 if !self.with_data {
6496 f.write_str(" WITH NO DATA")?;
6497 }
6498 Ok(())
6499 }
6500}
6501
6502impl fmt::Display for CreateViewStatement {
6503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6504 f.write_str("CREATE ")?;
6505 if self.or_replace {
6506 f.write_str("OR REPLACE ")?;
6507 }
6508 if self.temporary {
6509 f.write_str("TEMPORARY ")?;
6510 }
6511 f.write_str("VIEW ")?;
6512 if self.if_not_exists {
6513 f.write_str("IF NOT EXISTS ")?;
6514 }
6515 write!(f, "{}", quote_ident(&self.name))?;
6516 if !self.columns.is_empty() {
6517 f.write_str(" (")?;
6518 for (i, c) in self.columns.iter().enumerate() {
6519 if i > 0 {
6520 f.write_str(", ")?;
6521 }
6522 write!(f, "{}", quote_ident(c))?;
6523 }
6524 f.write_str(")")?;
6525 }
6526 write!(f, " AS {}", self.body)?;
6527 match self.check_option {
6528 Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
6529 Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
6530 None => Ok(()),
6531 }
6532 }
6533}
6534
6535impl fmt::Display for CreateSequenceStatement {
6536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6537 f.write_str("CREATE ")?;
6538 if self.temporary {
6539 f.write_str("TEMPORARY ")?;
6540 }
6541 f.write_str("SEQUENCE ")?;
6542 if self.if_not_exists {
6543 f.write_str("IF NOT EXISTS ")?;
6544 }
6545 write!(f, "{}", quote_ident(&self.name))?;
6546 if let Some(dt) = self.data_type {
6547 write!(f, " AS {dt}")?;
6548 }
6549 write_sequence_options(f, &self.options)
6550 }
6551}
6552
6553impl fmt::Display for AlterSequenceStatement {
6554 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6555 f.write_str("ALTER SEQUENCE ")?;
6556 if self.if_exists {
6557 f.write_str("IF EXISTS ")?;
6558 }
6559 write!(f, "{}", quote_ident(&self.name))?;
6560 write_sequence_options(f, &self.options)
6561 }
6562}
6563
6564impl fmt::Display for SequenceDataType {
6565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6566 f.write_str(match self {
6567 Self::SmallInt => "smallint",
6568 Self::Int => "integer",
6569 Self::BigInt => "bigint",
6570 })
6571 }
6572}
6573
6574fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
6575 if let Some(n) = o.increment {
6576 write!(f, " INCREMENT BY {n}")?;
6577 }
6578 match o.min_value {
6579 Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
6580 Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
6581 None => {}
6582 }
6583 match o.max_value {
6584 Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
6585 Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
6586 None => {}
6587 }
6588 if let Some(n) = o.start {
6589 write!(f, " START WITH {n}")?;
6590 }
6591 match o.restart {
6592 Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
6593 Some(None) => f.write_str(" RESTART")?,
6594 None => {}
6595 }
6596 if let Some(n) = o.cache {
6597 write!(f, " CACHE {n}")?;
6598 }
6599 match o.cycle {
6600 Some(true) => f.write_str(" CYCLE")?,
6601 Some(false) => f.write_str(" NO CYCLE")?,
6602 None => {}
6603 }
6604 if let Some(ob) = &o.owned_by {
6605 match ob {
6606 SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
6607 SequenceOwnedBy::Column { table, column } => {
6608 write!(
6609 f,
6610 " OWNED BY {}.{}",
6611 quote_ident(table),
6612 quote_ident(column)
6613 )?;
6614 }
6615 }
6616 }
6617 Ok(())
6618}
6619
6620impl fmt::Display for CreateFunctionStatement {
6621 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6622 f.write_str("CREATE ")?;
6623 if self.or_replace {
6624 f.write_str("OR REPLACE ")?;
6625 }
6626 write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
6627 for (i, arg) in self.args.iter().enumerate() {
6628 if i > 0 {
6629 f.write_str(", ")?;
6630 }
6631 match arg.mode {
6632 FunctionArgMode::In => {}
6633 FunctionArgMode::Out => f.write_str("OUT ")?,
6634 FunctionArgMode::InOut => f.write_str("INOUT ")?,
6635 }
6636 if let Some(name) = &arg.name {
6637 write!(f, "{} ", quote_ident(name))?;
6638 }
6639 match &arg.ty {
6640 FunctionArgType::Typed(t) => write!(f, "{t}")?,
6641 FunctionArgType::Raw(s) => f.write_str(s)?,
6642 }
6643 }
6644 f.write_str(") RETURNS ")?;
6645 match &self.returns {
6646 FunctionReturn::Trigger => f.write_str("TRIGGER")?,
6647 FunctionReturn::Void => f.write_str("VOID")?,
6648 FunctionReturn::Type(t) => write!(f, "{t}")?,
6649 FunctionReturn::Other(s) => f.write_str(s)?,
6650 }
6651 write!(f, " LANGUAGE {} AS $$", self.language)?;
6652 match &self.body {
6653 FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
6654 FunctionBody::Raw(s) => f.write_str(s)?,
6655 }
6656 f.write_str("$$")
6657 }
6658}
6659
6660impl fmt::Display for PlPgSqlBlock {
6661 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6662 if !self.declarations.is_empty() {
6663 f.write_str("DECLARE\n")?;
6664 for d in &self.declarations {
6665 write!(f, " {} ", quote_ident(&d.name))?;
6666 match &d.ty {
6667 FunctionArgType::Typed(t) => write!(f, "{t}")?,
6668 FunctionArgType::Raw(s) => f.write_str(s)?,
6669 }
6670 if let Some(e) = &d.default {
6671 write!(f, " := {e}")?;
6672 }
6673 f.write_str(";\n")?;
6674 }
6675 }
6676 f.write_str("BEGIN\n")?;
6677 for stmt in &self.statements {
6678 writeln!(f, " {stmt};")?;
6679 }
6680 // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
6681 // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
6682 // parsed block through it — so every exception handler a function
6683 // declared was thrown away AT STORE TIME. The block executed fine while
6684 // it was still an AST (a DO block never round-trips through text), which
6685 // is why only functions and triggers lost theirs.
6686 if !self.exception_handlers.is_empty() {
6687 f.write_str("EXCEPTION\n")?;
6688 for h in &self.exception_handlers {
6689 writeln!(f, " WHEN {} THEN", h.conditions.join(" OR "))?;
6690 for stmt in &h.body {
6691 writeln!(f, " {stmt};")?;
6692 }
6693 }
6694 }
6695 f.write_str("END")
6696 }
6697}
6698
6699impl fmt::Display for PlPgSqlStmt {
6700 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6701 match self {
6702 Self::Assign { target, value } => write!(f, "{target} := {value}"),
6703 Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
6704 Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
6705 Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
6706 Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
6707 Self::Return(t) => match t {
6708 ReturnTarget::New => f.write_str("RETURN NEW"),
6709 ReturnTarget::Old => f.write_str("RETURN OLD"),
6710 ReturnTarget::Null => f.write_str("RETURN NULL"),
6711 ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
6712 },
6713 Self::If {
6714 branches,
6715 else_branch,
6716 } => {
6717 for (i, (cond, body)) in branches.iter().enumerate() {
6718 if i == 0 {
6719 write!(f, "IF {cond} THEN ")?;
6720 } else {
6721 write!(f, " ELSIF {cond} THEN ")?;
6722 }
6723 for (j, s) in body.iter().enumerate() {
6724 if j > 0 {
6725 f.write_str("; ")?;
6726 }
6727 write!(f, "{s}")?;
6728 }
6729 }
6730 if !else_branch.is_empty() {
6731 f.write_str(" ELSE ")?;
6732 for (j, s) in else_branch.iter().enumerate() {
6733 if j > 0 {
6734 f.write_str("; ")?;
6735 }
6736 write!(f, "{s}")?;
6737 }
6738 }
6739 f.write_str(" END IF")
6740 }
6741 Self::Raise {
6742 level,
6743 message,
6744 args,
6745 } => {
6746 let lvl = match level {
6747 RaiseLevel::Notice => "NOTICE",
6748 RaiseLevel::Warning => "WARNING",
6749 RaiseLevel::Info => "INFO",
6750 RaiseLevel::Log => "LOG",
6751 RaiseLevel::Debug => "DEBUG",
6752 RaiseLevel::Exception => "EXCEPTION",
6753 };
6754 write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
6755 for a in args {
6756 write!(f, ", {a}")?;
6757 }
6758 Ok(())
6759 }
6760 Self::EmbeddedSql(s) => write!(f, "{s}"),
6761 Self::Assert { condition, message } => {
6762 write!(f, "ASSERT {condition}")?;
6763 if let Some(m) = message {
6764 write!(f, ", {m}")?;
6765 }
6766 Ok(())
6767 }
6768 Self::While { condition, body } => {
6769 writeln!(f, "WHILE {condition} LOOP")?;
6770 for s in body {
6771 writeln!(f, " {s};")?;
6772 }
6773 f.write_str("END LOOP")
6774 }
6775 Self::ForRange {
6776 var,
6777 start,
6778 end,
6779 reverse,
6780 body,
6781 } => {
6782 write!(f, "FOR {var} IN ")?;
6783 if *reverse {
6784 f.write_str("REVERSE ")?;
6785 }
6786 writeln!(f, "{start}..{end} LOOP")?;
6787 for s in body {
6788 writeln!(f, " {s};")?;
6789 }
6790 f.write_str("END LOOP")
6791 }
6792 Self::Loop { body } => {
6793 writeln!(f, "LOOP")?;
6794 for s in body {
6795 writeln!(f, " {s};")?;
6796 }
6797 f.write_str("END LOOP")
6798 }
6799 Self::Exit { when } => {
6800 f.write_str("EXIT")?;
6801 if let Some(c) = when {
6802 write!(f, " WHEN {c}")?;
6803 }
6804 Ok(())
6805 }
6806 Self::Continue { when } => {
6807 f.write_str("CONTINUE")?;
6808 if let Some(c) = when {
6809 write!(f, " WHEN {c}")?;
6810 }
6811 Ok(())
6812 }
6813 Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
6814 Self::ForQuery { var, query, body } => {
6815 writeln!(f, "FOR {var} IN ({query}) LOOP")?;
6816 for s in body {
6817 writeln!(f, " {s};")?;
6818 }
6819 f.write_str("END LOOP")
6820 }
6821 Self::ForExecute {
6822 var,
6823 sql_expr,
6824 body,
6825 } => {
6826 writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
6827 for s in body {
6828 writeln!(f, " {s};")?;
6829 }
6830 f.write_str("END LOOP")
6831 }
6832 }
6833 }
6834}
6835
6836impl fmt::Display for AssignTarget {
6837 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6838 match self {
6839 Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
6840 Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
6841 Self::Local(n) => f.write_str(n),
6842 }
6843 }
6844}
6845
6846impl fmt::Display for CreateTriggerStatement {
6847 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6848 f.write_str("CREATE ")?;
6849 if self.or_replace {
6850 f.write_str("OR REPLACE ")?;
6851 }
6852 write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
6853 match self.timing {
6854 TriggerTiming::Before => f.write_str("BEFORE")?,
6855 TriggerTiming::After => f.write_str("AFTER")?,
6856 TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
6857 }
6858 for (i, e) in self.events.iter().enumerate() {
6859 if i == 0 {
6860 f.write_str(" ")?;
6861 } else {
6862 f.write_str(" OR ")?;
6863 }
6864 match e {
6865 TriggerEvent::Insert => f.write_str("INSERT")?,
6866 TriggerEvent::Update => {
6867 f.write_str("UPDATE")?;
6868 if !self.update_columns.is_empty() {
6869 f.write_str(" OF ")?;
6870 for (j, col) in self.update_columns.iter().enumerate() {
6871 if j > 0 {
6872 f.write_str(", ")?;
6873 }
6874 f.write_str("e_ident(col))?;
6875 }
6876 }
6877 }
6878 TriggerEvent::Delete => f.write_str("DELETE")?,
6879 TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
6880 }
6881 }
6882 write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
6883 match self.for_each {
6884 TriggerForEach::Row => f.write_str("ROW")?,
6885 TriggerForEach::Statement => f.write_str("STATEMENT")?,
6886 }
6887 write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
6888 }
6889}
6890
6891impl fmt::Display for CreateIndexStatement {
6892 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6893 if self.is_unique {
6894 f.write_str("CREATE UNIQUE INDEX ")?;
6895 } else {
6896 f.write_str("CREATE INDEX ")?;
6897 }
6898 if self.if_not_exists {
6899 f.write_str("IF NOT EXISTS ")?;
6900 }
6901 write!(
6902 f,
6903 "{} ON {} ",
6904 quote_ident(&self.name),
6905 quote_ident(&self.table)
6906 )?;
6907 match self.method {
6908 IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
6909 IndexMethod::Brin => f.write_str("USING brin ")?,
6910 IndexMethod::Gin => f.write_str("USING gin ")?,
6911 IndexMethod::BTree => {}
6912 }
6913 if let Some(expr) = &self.expression {
6914 write!(f, "({})", expr)?;
6915 } else if self.extra_columns.is_empty() {
6916 // v7.15.0 — preserve operator class on round-trip
6917 // (`(col opclass)`) so WAL replay reconstructs the
6918 // engine-routing intent (e.g. `gin_trgm_ops` →
6919 // trigram-GIN build path).
6920 if let Some(op) = &self.opclass {
6921 write!(f, "({} {})", quote_ident(&self.column), op)?;
6922 } else {
6923 write!(f, "({})", quote_ident(&self.column))?;
6924 }
6925 } else {
6926 // v7.9.14 — multi-column key. Emit each column quoted
6927 // so the round-tripped form re-parses to identical AST.
6928 f.write_str("(")?;
6929 write!(f, "{}", quote_ident(&self.column))?;
6930 for c in &self.extra_columns {
6931 write!(f, ", {}", quote_ident(c))?;
6932 }
6933 f.write_str(")")?;
6934 }
6935 if !self.included_columns.is_empty() {
6936 f.write_str(" INCLUDE (")?;
6937 for (i, c) in self.included_columns.iter().enumerate() {
6938 if i > 0 {
6939 f.write_str(", ")?;
6940 }
6941 write!(f, "{}", quote_ident(c))?;
6942 }
6943 f.write_str(")")?;
6944 }
6945 if let Some(pred) = &self.partial_predicate {
6946 write!(f, " WHERE {}", pred)?;
6947 }
6948 Ok(())
6949 }
6950}
6951
6952impl fmt::Display for CreateTableStatement {
6953 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6954 f.write_str("CREATE TABLE ")?;
6955 if self.if_not_exists {
6956 f.write_str("IF NOT EXISTS ")?;
6957 }
6958 write!(f, "{}", quote_ident(&self.name))?;
6959 // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
6960 // no column list and no constraints; the table inherits its
6961 // columns from the parent at engine-DDL time.
6962 if let Some(spec) = &self.partition_of {
6963 write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
6964 return match &spec.bounds {
6965 PartitionOfBoundsAst::Range { lower, upper } => {
6966 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
6967 }
6968 PartitionOfBoundsAst::List { values } => {
6969 f.write_str("FOR VALUES IN (")?;
6970 for (i, v) in values.iter().enumerate() {
6971 if i > 0 {
6972 f.write_str(", ")?;
6973 }
6974 write!(f, "{}", v)?;
6975 }
6976 f.write_str(")")
6977 }
6978 PartitionOfBoundsAst::Hash { modulus, remainder } => {
6979 write!(
6980 f,
6981 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
6982 modulus, remainder
6983 )
6984 }
6985 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
6986 };
6987 }
6988 f.write_str(" (")?;
6989 for (i, col) in self.columns.iter().enumerate() {
6990 if i > 0 {
6991 f.write_str(", ")?;
6992 }
6993 write!(f, "{col}")?;
6994 }
6995 // v7.6.0 — render FK constraints in table-level form, after
6996 // the column list. WAL replay round-trips through Display, so
6997 // every FK must serialise here for replay to reconstruct the
6998 // schema bit-for-bit.
6999 for fk in &self.foreign_keys {
7000 f.write_str(", ")?;
7001 write!(f, "{fk}")?;
7002 }
7003 // v7.13.0 — render table-level constraints (PRIMARY KEY /
7004 // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
7005 // column-level UNIQUE / CHECK get lifted to this list at
7006 // parse time, so emitting only here avoids double-counting.
7007 for tc in &self.table_constraints {
7008 f.write_str(", ")?;
7009 write!(f, "{tc}")?;
7010 }
7011 f.write_str(")")?;
7012 // v7.37.6-B — partition-parent suffix renders after the
7013 // closing column-list paren, before the optional MySQL
7014 // table-options tail (which Display doesn't currently emit).
7015 if let Some(spec) = &self.partition_by {
7016 f.write_str(" PARTITION BY ")?;
7017 match spec.kind {
7018 PartitionKindAst::Range => f.write_str("RANGE ")?,
7019 PartitionKindAst::List => f.write_str("LIST ")?,
7020 PartitionKindAst::Hash => f.write_str("HASH ")?,
7021 }
7022 f.write_str("(")?;
7023 for (i, col) in spec.key_columns.iter().enumerate() {
7024 if i > 0 {
7025 f.write_str(", ")?;
7026 }
7027 f.write_str("e_ident(col))?;
7028 }
7029 f.write_str(")")?;
7030 }
7031 Ok(())
7032 }
7033}
7034
7035fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
7036 match t {
7037 AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
7038 AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
7039 write!(f, "REPLICA IDENTITY USING INDEX {index}")
7040 }
7041 AlterTableTarget::Inherit { parent, detach } => {
7042 if *detach {
7043 write!(f, "NO INHERIT {parent}")
7044 } else {
7045 write!(f, "INHERIT {parent}")
7046 }
7047 }
7048 AlterTableTarget::SetHotTierBytes(n) => {
7049 write!(f, "SET hot_tier_bytes = {n}")
7050 }
7051 AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
7052 AlterTableTarget::DropForeignKey { name, if_exists } => {
7053 f.write_str("DROP CONSTRAINT ")?;
7054 if *if_exists {
7055 f.write_str("IF EXISTS ")?;
7056 }
7057 write!(f, "{}", quote_ident(name))
7058 }
7059 AlterTableTarget::DropIndex { name, if_exists } => {
7060 f.write_str("DROP INDEX ")?;
7061 if *if_exists {
7062 f.write_str("IF EXISTS ")?;
7063 }
7064 write!(f, "{}", quote_ident(name))
7065 }
7066 AlterTableTarget::AddColumn {
7067 column,
7068 if_not_exists,
7069 } => {
7070 f.write_str("ADD COLUMN ")?;
7071 if *if_not_exists {
7072 f.write_str("IF NOT EXISTS ")?;
7073 }
7074 write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
7075 if !column.nullable {
7076 f.write_str(" NOT NULL")?;
7077 }
7078 if let Some(d) = &column.default {
7079 write!(f, " DEFAULT {d}")?;
7080 }
7081 if column.auto_increment {
7082 f.write_str(" AUTO_INCREMENT")?;
7083 }
7084 if column.is_primary_key {
7085 f.write_str(" PRIMARY KEY")?;
7086 }
7087 Ok(())
7088 }
7089 AlterTableTarget::AlterColumnType {
7090 column,
7091 new_type,
7092 using,
7093 collation,
7094 } => {
7095 write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
7096 if let Some((_, name)) = collation {
7097 write!(f, " COLLATE {}", quote_ident(name))?;
7098 }
7099 if let Some(u) = using {
7100 write!(f, " USING {u}")?;
7101 }
7102 Ok(())
7103 }
7104 AlterTableTarget::DropColumn {
7105 column,
7106 if_exists,
7107 cascade,
7108 } => {
7109 f.write_str("DROP COLUMN ")?;
7110 if *if_exists {
7111 f.write_str("IF EXISTS ")?;
7112 }
7113 write!(f, "{}", quote_ident(column))?;
7114 if *cascade {
7115 f.write_str(" CASCADE")?;
7116 }
7117 Ok(())
7118 }
7119 AlterTableTarget::AddTableConstraint(tc) => {
7120 write!(f, "ADD {tc}")
7121 }
7122 AlterTableTarget::ValidateConstraint { name } => {
7123 write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
7124 }
7125 AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
7126 AlterTableTarget::ClusterOn { index } => match index {
7127 Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
7128 None => f.write_str("SET WITHOUT CLUSTER"),
7129 },
7130 AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
7131 // Round-trip-safe spelling: re-parsing this form lowers
7132 // back to SetColumnAutoIncrement (the nextval default is
7133 // how pg_dump says "serial").
7134 let seq = seq_name
7135 .clone()
7136 .unwrap_or_else(|| alloc::format!("{column}_seq"));
7137 write!(
7138 f,
7139 "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
7140 quote_ident(column)
7141 )
7142 }
7143 AlterTableTarget::RenameColumn { old, new } => {
7144 write!(
7145 f,
7146 "RENAME COLUMN {} TO {}",
7147 quote_ident(old),
7148 quote_ident(new)
7149 )
7150 }
7151 AlterTableTarget::RenameConstraint { old, new } => {
7152 write!(
7153 f,
7154 "RENAME CONSTRAINT {} TO {}",
7155 quote_ident(old),
7156 quote_ident(new)
7157 )
7158 }
7159 AlterTableTarget::RenameTable { new } => {
7160 write!(f, "RENAME TO {}", quote_ident(new))
7161 }
7162 AlterTableTarget::SetTriggerEnabled { which, enabled } => {
7163 f.write_str(if *enabled {
7164 "ENABLE TRIGGER "
7165 } else {
7166 "DISABLE TRIGGER "
7167 })?;
7168 match which {
7169 TriggerSelector::All => f.write_str("ALL"),
7170 TriggerSelector::Named(n) => f.write_str("e_ident(n)),
7171 }
7172 }
7173 AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
7174 (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
7175 (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
7176 (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
7177 (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
7178 (None, None) => Ok(()),
7179 },
7180 AlterTableTarget::AttachPartition { child, bounds } => {
7181 write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
7182 match bounds {
7183 PartitionOfBoundsAst::Range { lower, upper } => {
7184 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7185 }
7186 PartitionOfBoundsAst::List { values } => {
7187 f.write_str("FOR VALUES IN (")?;
7188 for (i, v) in values.iter().enumerate() {
7189 if i > 0 {
7190 f.write_str(", ")?;
7191 }
7192 write!(f, "{}", v)?;
7193 }
7194 f.write_str(")")
7195 }
7196 PartitionOfBoundsAst::Hash { modulus, remainder } => {
7197 write!(
7198 f,
7199 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7200 modulus, remainder
7201 )
7202 }
7203 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7204 }
7205 }
7206 AlterTableTarget::DetachPartition {
7207 child,
7208 concurrently,
7209 finalize,
7210 } => {
7211 write!(f, "DETACH PARTITION {}", quote_ident(child))?;
7212 if *concurrently {
7213 f.write_str(" CONCURRENTLY")?;
7214 }
7215 if *finalize {
7216 f.write_str(" FINALIZE")?;
7217 }
7218 Ok(())
7219 }
7220 AlterTableTarget::AlterColumnSetDefault {
7221 column,
7222 default_expr,
7223 } => write!(
7224 f,
7225 "ALTER COLUMN {} SET DEFAULT {}",
7226 quote_ident(column),
7227 default_expr
7228 ),
7229 AlterTableTarget::AlterColumnDropDefault { column } => {
7230 write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
7231 }
7232 AlterTableTarget::AlterColumnSetNotNull { column } => {
7233 write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
7234 }
7235 AlterTableTarget::AlterColumnDropNotNull { column } => {
7236 write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
7237 }
7238 AlterTableTarget::AlterColumnRestart { column, with } => {
7239 write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
7240 if let Some(n) = with {
7241 write!(f, " WITH {n}")?;
7242 }
7243 Ok(())
7244 }
7245 AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
7246 write!(
7247 f,
7248 "ALTER COLUMN {} DROP EXPRESSION{}",
7249 quote_ident(column),
7250 if *if_exists { " IF EXISTS" } else { "" }
7251 )
7252 }
7253 AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7254 write!(
7255 f,
7256 "ALTER COLUMN {} DROP IDENTITY{}",
7257 quote_ident(column),
7258 if *if_exists { " IF EXISTS" } else { "" }
7259 )
7260 }
7261 AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7262 write!(
7263 f,
7264 "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7265 quote_ident(column)
7266 )
7267 }
7268 }
7269}
7270
7271impl fmt::Display for TableConstraint {
7272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7273 match self {
7274 Self::PrimaryKey { name, columns, .. } => {
7275 if let Some(n) = name {
7276 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7277 }
7278 f.write_str("PRIMARY KEY (")?;
7279 for (i, c) in columns.iter().enumerate() {
7280 if i > 0 {
7281 f.write_str(", ")?;
7282 }
7283 f.write_str("e_ident(c))?;
7284 }
7285 f.write_str(")")
7286 }
7287 Self::Unique {
7288 name,
7289 columns,
7290 nulls_not_distinct,
7291 ..
7292 } => {
7293 if let Some(n) = name {
7294 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7295 }
7296 f.write_str("UNIQUE ")?;
7297 if *nulls_not_distinct {
7298 f.write_str("NULLS NOT DISTINCT ")?;
7299 }
7300 f.write_str("(")?;
7301 for (i, c) in columns.iter().enumerate() {
7302 if i > 0 {
7303 f.write_str(", ")?;
7304 }
7305 f.write_str("e_ident(c))?;
7306 }
7307 f.write_str(")")
7308 }
7309 Self::Check {
7310 name,
7311 expr,
7312 not_valid,
7313 } => {
7314 if let Some(n) = name {
7315 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7316 }
7317 write!(f, "CHECK ({expr})")?;
7318 if *not_valid {
7319 write!(f, " NOT VALID")?;
7320 }
7321 Ok(())
7322 }
7323 Self::Index { name, columns } => {
7324 f.write_str("KEY ")?;
7325 if let Some(n) = name {
7326 write!(f, "{} ", quote_ident(n))?;
7327 }
7328 f.write_str("(")?;
7329 for (i, c) in columns.iter().enumerate() {
7330 if i > 0 {
7331 f.write_str(", ")?;
7332 }
7333 f.write_str("e_ident(c))?;
7334 }
7335 f.write_str(")")
7336 }
7337 Self::FulltextIndex { name, columns } => {
7338 // Mysqldump emits `FULLTEXT KEY name (cols)` —
7339 // Display rounds back to that shape so dump
7340 // replay reproduces the input verbatim.
7341 f.write_str("FULLTEXT KEY ")?;
7342 if let Some(n) = name {
7343 write!(f, "{} ", quote_ident(n))?;
7344 }
7345 f.write_str("(")?;
7346 for (i, c) in columns.iter().enumerate() {
7347 if i > 0 {
7348 f.write_str(", ")?;
7349 }
7350 f.write_str("e_ident(c))?;
7351 }
7352 f.write_str(")")
7353 }
7354 Self::Exclude {
7355 name,
7356 method,
7357 elements,
7358 } => {
7359 if let Some(n) = name {
7360 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7361 }
7362 f.write_str("EXCLUDE ")?;
7363 if let Some(m) = method {
7364 write!(f, "USING {m} ")?;
7365 }
7366 f.write_str("(")?;
7367 for (i, (col, op)) in elements.iter().enumerate() {
7368 if i > 0 {
7369 f.write_str(", ")?;
7370 }
7371 write!(f, "{} WITH {op}", quote_ident(col))?;
7372 }
7373 f.write_str(")")
7374 }
7375 }
7376 }
7377}
7378
7379impl fmt::Display for ForeignKeyConstraint {
7380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7381 if let Some(name) = &self.name {
7382 write!(f, "CONSTRAINT {} ", quote_ident(name))?;
7383 }
7384 f.write_str("FOREIGN KEY (")?;
7385 for (i, c) in self.columns.iter().enumerate() {
7386 if i > 0 {
7387 f.write_str(", ")?;
7388 }
7389 f.write_str("e_ident(c))?;
7390 }
7391 write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
7392 if !self.parent_columns.is_empty() {
7393 f.write_str(" (")?;
7394 for (i, c) in self.parent_columns.iter().enumerate() {
7395 if i > 0 {
7396 f.write_str(", ")?;
7397 }
7398 f.write_str("e_ident(c))?;
7399 }
7400 f.write_str(")")?;
7401 }
7402 // Only render non-default actions to keep Display output
7403 // close to user input. SPG's default is RESTRICT (matches
7404 // SQL spec).
7405 if self.on_delete != FkAction::Restrict {
7406 write!(f, " ON DELETE {}", self.on_delete)?;
7407 }
7408 if self.on_update != FkAction::Restrict {
7409 write!(f, " ON UPDATE {}", self.on_update)?;
7410 }
7411 Ok(())
7412 }
7413}
7414
7415impl fmt::Display for FkAction {
7416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7417 match self {
7418 Self::Restrict => f.write_str("RESTRICT"),
7419 Self::Cascade => f.write_str("CASCADE"),
7420 Self::SetNull => f.write_str("SET NULL"),
7421 Self::SetDefault => f.write_str("SET DEFAULT"),
7422 Self::NoAction => f.write_str("NO ACTION"),
7423 }
7424 }
7425}
7426
7427impl fmt::Display for ColumnDef {
7428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7429 // v7.30.1 (mailrs round-24 class audit) — the type position
7430 // must re-parse to the same ColumnDef: a user-defined type
7431 // reference and the MySQL inline ENUM / SET value lists all
7432 // lower `ty` to Text, so rendering `ty` lost them.
7433 write!(f, "{}", quote_ident(&self.name))?;
7434 if let Some(ut) = &self.user_type_ref {
7435 write!(f, " {}", quote_ident(ut))?;
7436 } else if let Some(variants) = &self.inline_enum_variants {
7437 write_variant_list(f, "ENUM", variants)?;
7438 } else if let Some(variants) = &self.inline_set_variants {
7439 write_variant_list(f, "SET", variants)?;
7440 } else {
7441 write!(f, " {}", self.ty)?;
7442 }
7443 if self.is_unsigned {
7444 f.write_str(" UNSIGNED")?;
7445 }
7446 // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
7447 // DDL. Only emits when non-default so the typical output
7448 // stays unchanged.
7449 match self.collation {
7450 Collation::Binary => {}
7451 Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
7452 }
7453 if let Some(d) = &self.default {
7454 write!(f, " DEFAULT {d}")?;
7455 }
7456 if self.auto_increment {
7457 f.write_str(" AUTO_INCREMENT")?;
7458 }
7459 if !self.nullable {
7460 f.write_str(" NOT NULL")?;
7461 }
7462 // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
7463 // is NOT lifted to a table-level constraint at parse time
7464 // (unlike UNIQUE / CHECK), so the WAL round trip of a
7465 // prepared CREATE TABLE silently dropped the primary key.
7466 if self.is_primary_key {
7467 f.write_str(" PRIMARY KEY")?;
7468 }
7469 // The parser accepts only CURRENT_TIMESTAMP here (stored as
7470 // now()), so that spelling is the lossless round trip.
7471 if self.on_update_runtime.is_some() {
7472 f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
7473 }
7474 // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
7475 // replay reconstructs the computed-column declaration. The
7476 // expression sits inside a single set of parens; STORED is
7477 // the only variant the parser accepts.
7478 if let Some(gen_expr) = &self.generated_stored_expr {
7479 write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
7480 }
7481 Ok(())
7482 }
7483}
7484
7485/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
7486/// types (MySQL flavour; `ty` is Text underneath).
7487fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
7488 write!(f, " {kw}(")?;
7489 for (i, v) in variants.iter().enumerate() {
7490 if i > 0 {
7491 f.write_str(", ")?;
7492 }
7493 write!(f, "'{}'", v.replace('\'', "''"))?;
7494 }
7495 f.write_str(")")
7496}
7497
7498impl fmt::Display for InsertStatement {
7499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7500 write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
7501 if let Some(cols) = &self.columns {
7502 f.write_str(" (")?;
7503 for (i, c) in cols.iter().enumerate() {
7504 if i > 0 {
7505 f.write_str(", ")?;
7506 }
7507 f.write_str("e_ident(c))?;
7508 }
7509 f.write_str(")")?;
7510 }
7511 // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
7512 // skipping the VALUES list (mailrs round-5 G4).
7513 if let Some(sel) = &self.select_source {
7514 write!(f, " {sel}")?;
7515 } else {
7516 f.write_str(" VALUES ")?;
7517 for (ri, row) in self.rows.iter().enumerate() {
7518 if ri > 0 {
7519 f.write_str(", ")?;
7520 }
7521 f.write_str("(")?;
7522 for (i, v) in row.iter().enumerate() {
7523 if i > 0 {
7524 f.write_str(", ")?;
7525 }
7526 write!(f, "{v}")?;
7527 }
7528 f.write_str(")")?;
7529 }
7530 }
7531 // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
7532 // Display round trip: WAL persistence renders the bind-final
7533 // AST through this impl, and a replayed bare INSERT turns a
7534 // legal upsert no-op into a UNIQUE violation that refuses to
7535 // open the catalog.
7536 if let Some(oc) = &self.on_conflict {
7537 write!(f, " {oc}")?;
7538 }
7539 write_returning(self.returning.as_deref(), f)?;
7540 Ok(())
7541 }
7542}
7543
7544/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
7545/// parser produced, so the AST→SQL round trip preserves upsert
7546/// semantics (WAL replay depends on it).
7547impl fmt::Display for OnConflictClause {
7548 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7549 f.write_str("ON CONFLICT")?;
7550 if let Some(name) = &self.constraint_name {
7551 write!(f, " ON CONSTRAINT {name}")?;
7552 }
7553 if !self.target_columns.is_empty() {
7554 f.write_str(" (")?;
7555 for (i, c) in self.target_columns.iter().enumerate() {
7556 if i > 0 {
7557 f.write_str(", ")?;
7558 }
7559 f.write_str("e_ident(c))?;
7560 }
7561 f.write_str(")")?;
7562 }
7563 if let Some(w) = &self.index_where {
7564 write!(f, " WHERE {w}")?;
7565 }
7566 match &self.action {
7567 OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
7568 OnConflictAction::Update {
7569 assignments,
7570 where_,
7571 } => {
7572 f.write_str(" DO UPDATE SET ")?;
7573 for (i, (col, expr)) in assignments.iter().enumerate() {
7574 if i > 0 {
7575 f.write_str(", ")?;
7576 }
7577 write!(f, "{} = {expr}", quote_ident(col))?;
7578 }
7579 if let Some(w) = where_ {
7580 write!(f, " WHERE {w}")?;
7581 }
7582 Ok(())
7583 }
7584 }
7585 }
7586}
7587
7588/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
7589/// tail for the three DML Display impls.
7590fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7591 let Some(items) = ret else {
7592 return Ok(());
7593 };
7594 f.write_str(" RETURNING ")?;
7595 for (i, item) in items.iter().enumerate() {
7596 if i > 0 {
7597 f.write_str(", ")?;
7598 }
7599 write!(f, "{item}")?;
7600 }
7601 Ok(())
7602}
7603
7604impl fmt::Display for UpdateStatement {
7605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7606 write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
7607 for (i, (col, expr)) in self.assignments.iter().enumerate() {
7608 if i > 0 {
7609 f.write_str(", ")?;
7610 }
7611 write!(f, "{} = {expr}", quote_ident(col))?;
7612 }
7613 if let Some(w) = &self.where_ {
7614 write!(f, " WHERE {w}")?;
7615 }
7616 // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
7617 if let Some(ol) = self.order_limit.as_deref() {
7618 if !ol.order_by.is_empty() {
7619 f.write_str(" ORDER BY ")?;
7620 for (i, o) in ol.order_by.iter().enumerate() {
7621 if i > 0 {
7622 f.write_str(", ")?;
7623 }
7624 write!(f, "{}", o.expr)?;
7625 if o.desc {
7626 f.write_str(" DESC")?;
7627 }
7628 match o.nulls_first {
7629 Some(true) => f.write_str(" NULLS FIRST")?,
7630 Some(false) => f.write_str(" NULLS LAST")?,
7631 None => {}
7632 }
7633 }
7634 }
7635 if let Some(n) = ol.limit {
7636 write!(f, " LIMIT {n}")?;
7637 }
7638 }
7639 write_returning(self.returning.as_deref(), f)?;
7640 Ok(())
7641 }
7642}
7643
7644impl fmt::Display for DeleteStatement {
7645 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7646 write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
7647 if let Some(w) = &self.where_ {
7648 write!(f, " WHERE {w}")?;
7649 }
7650 write_returning(self.returning.as_deref(), f)?;
7651 Ok(())
7652 }
7653}
7654
7655impl fmt::Display for CteBody {
7656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7657 match self {
7658 Self::Select(s) => write!(f, "{s}"),
7659 Self::Insert(s) => write!(f, "{s}"),
7660 Self::Update(s) => write!(f, "{s}"),
7661 Self::Delete(s) => write!(f, "{s}"),
7662 Self::Merge(s) => write!(f, "{s}"),
7663 }
7664 }
7665}
7666
7667impl fmt::Display for MergeStatement {
7668 // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
7669 // (it round-trips for the cases tests cover, not for
7670 // round-tripping every edge of the surface).
7671 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7672 fmt_with_clause(&self.ctes, f)?;
7673 f.write_str("MERGE INTO ")?;
7674 write!(f, "{}", quote_ident(&self.target))?;
7675 if let Some(a) = &self.target_alias {
7676 write!(f, " {}", quote_ident(a))?;
7677 }
7678 f.write_str(" USING ")?;
7679 if let Some(sub) = &self.source_select {
7680 write!(f, "({sub})")?;
7681 } else {
7682 write!(f, "{}", quote_ident(&self.source))?;
7683 }
7684 if let Some(a) = &self.source_alias {
7685 write!(f, " {}", quote_ident(a))?;
7686 }
7687 if !self.source_column_aliases.is_empty() {
7688 f.write_str("(")?;
7689 for (i, c) in self.source_column_aliases.iter().enumerate() {
7690 if i > 0 {
7691 f.write_str(", ")?;
7692 }
7693 write!(f, "{}", quote_ident(c))?;
7694 }
7695 f.write_str(")")?;
7696 }
7697 write!(f, " ON {}", self.on)?;
7698 for clause in &self.clauses {
7699 f.write_str(" WHEN ")?;
7700 f.write_str(match clause.matched {
7701 MergeMatched::Matched => "MATCHED",
7702 MergeMatched::NotMatched => "NOT MATCHED",
7703 MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
7704 })?;
7705 if let Some(c) = &clause.condition {
7706 write!(f, " AND {c}")?;
7707 }
7708 f.write_str(" THEN ")?;
7709 match &clause.action {
7710 MergeAction::Insert { columns, values } => {
7711 f.write_str("INSERT ")?;
7712 // A column list is optional (round 146): the bare
7713 // `INSERT VALUES (…)` form maps positionally.
7714 if !columns.is_empty() {
7715 f.write_str("(")?;
7716 for (i, c) in columns.iter().enumerate() {
7717 if i > 0 {
7718 f.write_str(", ")?;
7719 }
7720 write!(f, "{}", quote_ident(c))?;
7721 }
7722 f.write_str(") ")?;
7723 }
7724 f.write_str("VALUES (")?;
7725 for (i, v) in values.iter().enumerate() {
7726 if i > 0 {
7727 f.write_str(", ")?;
7728 }
7729 write!(f, "{v}")?;
7730 }
7731 f.write_str(")")?;
7732 }
7733 MergeAction::Update { assignments } => {
7734 f.write_str("UPDATE SET ")?;
7735 for (i, (c, e)) in assignments.iter().enumerate() {
7736 if i > 0 {
7737 f.write_str(", ")?;
7738 }
7739 write!(f, "{} = {e}", quote_ident(c))?;
7740 }
7741 }
7742 MergeAction::Delete => f.write_str("DELETE")?,
7743 MergeAction::DoNothing => f.write_str("DO NOTHING")?,
7744 }
7745 }
7746 if let Some(items) = &self.returning {
7747 f.write_str(" RETURNING ")?;
7748 for (i, it) in items.iter().enumerate() {
7749 if i > 0 {
7750 f.write_str(", ")?;
7751 }
7752 write!(f, "{it}")?;
7753 }
7754 }
7755 Ok(())
7756 }
7757}
7758
7759/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
7760/// carry a CTE list and must round-trip it identically.
7761fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
7762 if ctes.is_empty() {
7763 return Ok(());
7764 }
7765 f.write_str("WITH ")?;
7766 if ctes.iter().any(|c| c.recursive) {
7767 f.write_str("RECURSIVE ")?;
7768 }
7769 for (i, cte) in ctes.iter().enumerate() {
7770 if i > 0 {
7771 f.write_str(", ")?;
7772 }
7773 f.write_str("e_ident(&cte.name))?;
7774 if !cte.column_overrides.is_empty() {
7775 f.write_str(" (")?;
7776 for (ci, c) in cte.column_overrides.iter().enumerate() {
7777 if ci > 0 {
7778 f.write_str(", ")?;
7779 }
7780 f.write_str("e_ident(c))?;
7781 }
7782 f.write_str(")")?;
7783 }
7784 write!(f, " AS ({})", cte.body)?;
7785 }
7786 f.write_str(" ")
7787}
7788
7789impl fmt::Display for SelectStatement {
7790 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7791 // v7.30.1 (mailrs round-24 class audit) — the WITH clause
7792 // must survive the round trip; a CTE-using statement
7793 // re-parsed without it references undefined tables.
7794 fmt_with_clause(&self.ctes, f)?;
7795 write_bare_select(self, f)?;
7796 for (kind, peer) in &self.unions {
7797 f.write_str(match kind {
7798 UnionKind::Distinct => " UNION ",
7799 UnionKind::All => " UNION ALL ",
7800 UnionKind::Intersect => " INTERSECT ",
7801 UnionKind::IntersectAll => " INTERSECT ALL ",
7802 UnionKind::Except => " EXCEPT ",
7803 UnionKind::ExceptAll => " EXCEPT ALL ",
7804 })?;
7805 write_bare_select(peer, f)?;
7806 }
7807 if !self.order_by.is_empty() {
7808 f.write_str(" ORDER BY ")?;
7809 for (i, o) in self.order_by.iter().enumerate() {
7810 if i > 0 {
7811 f.write_str(", ")?;
7812 }
7813 write!(f, "{}", o.expr)?;
7814 if o.desc {
7815 f.write_str(" DESC")?;
7816 }
7817 match o.nulls_first {
7818 Some(true) => f.write_str(" NULLS FIRST")?,
7819 Some(false) => f.write_str(" NULLS LAST")?,
7820 None => {}
7821 }
7822 }
7823 }
7824 // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
7825 // exists in the FETCH FIRST spelling; rendering it as LIMIT
7826 // dropped the tie-extension semantics on replay. The parser
7827 // accepts OFFSET before FETCH, so keep that order here.
7828 if self.limit_with_ties {
7829 if let Some(o) = &self.offset {
7830 write!(f, " OFFSET {o}")?;
7831 }
7832 if let Some(n) = &self.limit {
7833 write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
7834 }
7835 } else {
7836 if let Some(n) = &self.limit {
7837 write!(f, " LIMIT {n}")?;
7838 }
7839 if let Some(o) = &self.offset {
7840 write!(f, " OFFSET {o}")?;
7841 }
7842 }
7843 Ok(())
7844 }
7845}
7846
7847fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7848 f.write_str("SELECT ")?;
7849 if s.distinct {
7850 f.write_str("DISTINCT ")?;
7851 }
7852 write_bare_select_body(s, f)
7853}
7854
7855fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7856 for (i, item) in s.items.iter().enumerate() {
7857 if i > 0 {
7858 f.write_str(", ")?;
7859 }
7860 write!(f, "{item}")?;
7861 }
7862 if let Some(t) = &s.from {
7863 write!(f, " FROM {t}")?;
7864 }
7865 if let Some(e) = &s.where_ {
7866 write!(f, " WHERE {e}")?;
7867 }
7868 if let Some(gs) = &s.group_by {
7869 f.write_str(" GROUP BY ")?;
7870 for (i, g) in gs.iter().enumerate() {
7871 if i > 0 {
7872 f.write_str(", ")?;
7873 }
7874 write!(f, "{g}")?;
7875 }
7876 } else if s.group_by_all {
7877 // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
7878 // shortcut parses to group_by: None + this flag; dropping
7879 // it turned an aggregate query into a bare projection on
7880 // re-parse.
7881 f.write_str(" GROUP BY ALL")?;
7882 }
7883 if let Some(h) = &s.having {
7884 write!(f, " HAVING {h}")?;
7885 }
7886 Ok(())
7887}
7888
7889impl fmt::Display for SelectItem {
7890 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7891 match self {
7892 Self::Wildcard => f.write_str("*"),
7893 Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
7894 Self::Expr { expr, alias } => {
7895 write!(f, "{expr}")?;
7896 if let Some(a) = alias {
7897 write!(f, " AS {}", quote_ident(a))?;
7898 }
7899 Ok(())
7900 }
7901 }
7902 }
7903}
7904
7905impl fmt::Display for FromClause {
7906 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7907 write!(f, "{}", self.primary)?;
7908 for j in &self.joins {
7909 match j.kind {
7910 JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
7911 JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
7912 JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
7913 JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
7914 JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
7915 JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
7916 }
7917 if let Some(on) = &j.on {
7918 write!(f, " ON {on}")?;
7919 }
7920 }
7921 Ok(())
7922 }
7923}
7924
7925/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
7926/// for NESTED). Kept close to the parser's grammar so it re-parses.
7927fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
7928 for (i, c) in cols.iter().enumerate() {
7929 if i > 0 {
7930 f.write_str(", ")?;
7931 }
7932 match c {
7933 JsonTableColumn::Ordinality { name } => {
7934 write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
7935 }
7936 JsonTableColumn::Nested { path, columns } => {
7937 write!(f, "NESTED PATH '{path}' COLUMNS (")?;
7938 fmt_json_table_columns(f, columns)?;
7939 f.write_str(")")?;
7940 }
7941 JsonTableColumn::Regular {
7942 name,
7943 ty,
7944 path,
7945 exists,
7946 format_json,
7947 wrapper,
7948 on_empty,
7949 on_error,
7950 } => {
7951 write!(f, "{} {ty}", quote_ident(name))?;
7952 if *format_json {
7953 f.write_str(" FORMAT JSON")?;
7954 }
7955 if *exists {
7956 write!(f, " EXISTS PATH '{path}'")?;
7957 } else {
7958 write!(f, " PATH '{path}'")?;
7959 }
7960 if *wrapper {
7961 f.write_str(" WITH WRAPPER")?;
7962 }
7963 if let JsonTableOnBehavior::Error = on_empty {
7964 f.write_str(" ERROR ON EMPTY")?;
7965 } else if let JsonTableOnBehavior::Default(e) = on_empty {
7966 write!(f, " DEFAULT {e} ON EMPTY")?;
7967 }
7968 if let JsonTableOnBehavior::Error = on_error {
7969 f.write_str(" ERROR ON ERROR")?;
7970 } else if let JsonTableOnBehavior::Default(e) = on_error {
7971 write!(f, " DEFAULT {e} ON ERROR")?;
7972 }
7973 }
7974 }
7975 }
7976 Ok(())
7977}
7978
7979impl fmt::Display for TableRef {
7980 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7981 // v7.30.1 (mailrs round-24 class audit) — the dynamic
7982 // table-ref shapes must round-trip: rendering only the
7983 // (synthetic) name turned LATERAL / unnest() /
7984 // generate_series() into references to nonexistent tables
7985 // on re-parse.
7986 // v7.39 (round 205) — JSON_TABLE round-trips through Display
7987 // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
7988 if let Some(jt) = &self.json_table {
7989 write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
7990 if !jt.passing.is_empty() {
7991 f.write_str(" PASSING ")?;
7992 for (i, (n, e)) in jt.passing.iter().enumerate() {
7993 if i > 0 {
7994 f.write_str(", ")?;
7995 }
7996 write!(f, "{e} AS {}", quote_ident(n))?;
7997 }
7998 }
7999 f.write_str(" COLUMNS (")?;
8000 fmt_json_table_columns(f, &jt.columns)?;
8001 f.write_str(")")?;
8002 if let Some(a) = &self.alias {
8003 write!(f, " AS {}", quote_ident(a))?;
8004 }
8005 return Ok(());
8006 }
8007 if let Some(inner) = &self.lateral_subquery {
8008 write!(f, "LATERAL ({inner})")?;
8009 if let Some(a) = &self.alias {
8010 write!(f, " AS {}", quote_ident(a))?;
8011 // v7.37 D.28 — a derived table on the lateral_subquery channel
8012 // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
8013 // lowers here). Rendering the alias without the column list lost
8014 // the column names on re-parse (a view body round-trips through
8015 // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
8016 if !self.unnest_column_aliases.is_empty() {
8017 f.write_str(" (")?;
8018 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8019 if i > 0 {
8020 f.write_str(", ")?;
8021 }
8022 f.write_str("e_ident(c))?;
8023 }
8024 f.write_str(")")?;
8025 }
8026 }
8027 return Ok(());
8028 }
8029 if let Some(expr) = &self.unnest_expr {
8030 write!(f, "UNNEST({expr})")?;
8031 if let Some(a) = &self.alias {
8032 write!(f, " AS {}", quote_ident(a))?;
8033 if !self.unnest_column_aliases.is_empty() {
8034 f.write_str(" (")?;
8035 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8036 if i > 0 {
8037 f.write_str(", ")?;
8038 }
8039 f.write_str("e_ident(c))?;
8040 }
8041 f.write_str(")")?;
8042 }
8043 }
8044 return Ok(());
8045 }
8046 // 7.38.1 S5.1 — a FROM-position table function must re-render
8047 // as the CALL, not its bare name: ARRAY(subquery) desugars by
8048 // re-parsing the subquery's canonical text, and a dropped
8049 // argument list turned `pg_options_to_table(x)` into a
8050 // relation lookup that does not exist.
8051 if let Some(call) = &self.table_fn_call {
8052 let (fn_name, args) = call.as_ref();
8053 write!(f, "{fn_name}(")?;
8054 for (i, a) in args.iter().enumerate() {
8055 if i > 0 {
8056 f.write_str(", ")?;
8057 }
8058 write!(f, "{a}")?;
8059 }
8060 f.write_str(")")?;
8061 if let Some(a) = &self.alias {
8062 write!(f, " AS {}", quote_ident(a))?;
8063 if !self.unnest_column_aliases.is_empty() {
8064 f.write_str("(")?;
8065 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8066 if i > 0 {
8067 f.write_str(", ")?;
8068 }
8069 write!(f, "{}", quote_ident(c))?;
8070 }
8071 f.write_str(")")?;
8072 }
8073 }
8074 return Ok(());
8075 }
8076 if let Some(args) = &self.generate_series_args {
8077 f.write_str("generate_series(")?;
8078 for (i, a) in args.iter().enumerate() {
8079 if i > 0 {
8080 f.write_str(", ")?;
8081 }
8082 write!(f, "{a}")?;
8083 }
8084 f.write_str(")")?;
8085 if let Some(a) = &self.alias {
8086 write!(f, " AS {}", quote_ident(a))?;
8087 }
8088 return Ok(());
8089 }
8090 write!(f, "{}", quote_ident(&self.name))?;
8091 if let Some(seg) = self.as_of_segment {
8092 write!(f, " AS OF SEGMENT {seg}")?;
8093 }
8094 if let Some(a) = &self.alias {
8095 write!(f, " AS {}", quote_ident(a))?;
8096 }
8097 Ok(())
8098 }
8099}
8100
8101impl fmt::Display for ColumnName {
8102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8103 if let Some(q) = &self.qualifier {
8104 write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
8105 } else {
8106 write!(f, "{}", quote_ident(&self.name))
8107 }
8108 }
8109}
8110
8111/// v7.39 (round 311) — render the left spine of an AND / OR chain
8112/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
8113/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
8114/// SAME operator flattens; anything else is an ordinary operand.
8115fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
8116 if let Expr::Binary {
8117 lhs,
8118 op: inner,
8119 rhs,
8120 } = e
8121 && *inner == op
8122 {
8123 write_bool_chain(f, lhs, op)?;
8124 return write!(f, " {op} {rhs}");
8125 }
8126 write!(f, "{e}")
8127}
8128
8129/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
8130/// form `pg_get_constraintdef(oid, true)` and friends return.
8131///
8132/// The default [`fmt::Display`] parenthesises every operator node, which
8133/// is what PG's non-pretty deparse does and what makes the text
8134/// round-trip. Pretty drops the pairs the grammar can put back, and the
8135/// rule is NOT plain precedence minimisation — measured against PG 18.4
8136/// across 37 shapes:
8137///
8138/// * the boolean layer follows precedence (NOT > AND > OR): an OR
8139/// under an AND keeps its parens, an AND under an OR does not, and a
8140/// comparison under any of them does not (`NOT a > 1`);
8141/// * an associative chain flattens completely, even where the source
8142/// nested it to the right (`a AND (b AND c)` prints as one chain);
8143/// * but an operand of a comparison or arithmetic operator keeps its
8144/// parens whenever it is itself an operator expression — so
8145/// `(a + b) > 10` and `(- a) + b`, even though precedence alone
8146/// would not require either. A cast, function call, column or
8147/// literal in that position does not (`a::text = t`,
8148/// `length(code) > 2`); a cast counts as compound exactly when the
8149/// thing it casts is (`((a + b)::text) = t`).
8150///
8151/// Anything outside that layer defers to `Display`, which is never
8152/// wrong — only more parenthesised than PG would print.
8153#[must_use]
8154pub fn pretty_expr(e: &Expr) -> String {
8155 let mut out = String::new();
8156 write_pretty(&mut out, e, PrettyParent::None, false, false);
8157 out
8158}
8159
8160/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
8161/// writes it.
8162///
8163/// MariaDB names the offending expression in its out-of-range message
8164/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
8165/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
8166/// MySQL client, for a cast the client had just written the other way.
8167#[must_use]
8168pub fn pretty_expr_mysql(e: &Expr) -> String {
8169 let mut out = String::new();
8170 write_pretty(&mut out, e, PrettyParent::None, false, true);
8171 out
8172}
8173
8174/// v7.39 (round 505) — how strongly an expression suggests its own column
8175/// name. A cast keeps its argument's name only when that name is STRONG;
8176/// otherwise the cast reports the type it casts to.
8177///
8178/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
8179/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
8180/// itself `text` — so `case` and a function name cannot be the same kind of
8181/// answer, even though a bare `CASE …` does report `case`.
8182#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8183enum NameStrength {
8184 /// Nothing to go on — PG reports `?column?`.
8185 None,
8186 /// A name, but one a cast overrides: `case`, or a type name.
8187 Weak,
8188 /// A name a cast keeps: a column, or the function that produced it.
8189 Strong,
8190}
8191
8192/// v7.39 (round 505) — the column name PG18 gives a projected expression
8193/// that carries no `AS` alias. `None` means `?column?`.
8194///
8195/// SPG used to print the parsed expression back out, which matched neither
8196/// oracle and made name-keyed row access miss on both wires:
8197///
8198/// | query | PG18 | SPG (before) |
8199/// |--------------|------------|--------------|
8200/// | `upper(s)` | `upper` | `upper(s)` |
8201/// | `a+b` | `?column?` | `(a + b)` |
8202/// | `'lit'` | `?column?` | `'lit'` |
8203/// | `CASE …` | `case` | `CASE WHEN (a = 1) THEN …` |
8204///
8205/// Every rule below is one of those measurements, taken with `\gdesc`
8206/// against PG18: a call is named for its function, a cast recurses into its
8207/// argument and falls back to the type, a scalar subquery takes the name of
8208/// the column it selects, and operators have no name at all.
8209#[must_use]
8210pub fn figure_column_name(expr: &Expr) -> Option<String> {
8211 let (name, _) = figure_name_inner(expr);
8212 name
8213}
8214
8215/// The name a function reports, which is not always the name SPG parsed it
8216/// under: `count(*)` is held as `count_star` so the star arity survives the
8217/// AST, and that internal spelling must not reach a client. PG18 reports
8218/// `count`.
8219fn canonical_function_name(name: &str) -> String {
8220 match name {
8221 "count_star" => "count".to_string(),
8222 other => other.to_ascii_lowercase(),
8223 }
8224}
8225
8226/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
8227/// reports when its operand has none of its own. Only the spellings that
8228/// differ from what the user writes need an entry; everything else is
8229/// already its own typname.
8230fn cast_target_typname(target: &CastTarget) -> String {
8231 let written = target.to_string().to_ascii_lowercase();
8232 let base = written.strip_suffix("[]").unwrap_or(&written);
8233 let mapped = match base {
8234 "bigint" => "int8",
8235 "integer" | "int" => "int4",
8236 "smallint" => "int2",
8237 "boolean" => "bool",
8238 "double precision" => "float8",
8239 "real" => "float4",
8240 "character varying" => "varchar",
8241 "character" => "bpchar",
8242 "timestamp with time zone" => "timestamptz",
8243 "timestamp without time zone" => "timestamp",
8244 "time without time zone" => "time",
8245 "decimal" => "numeric",
8246 other => other,
8247 };
8248 if written.ends_with("[]") {
8249 alloc::format!("_{mapped}")
8250 } else {
8251 String::from(mapped)
8252 }
8253}
8254
8255fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8256 let strong = |n: String| (Some(n), NameStrength::Strong);
8257 match expr {
8258 // A column keeps its own name, qualifier and all discarded:
8259 // `lbl.a` reports `a`.
8260 Expr::Column(c) => strong(c.name.clone()),
8261 // Calls are named for the function. This covers the shapes that
8262 // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8263 // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8264 // because PG resolves them to functions before naming them.
8265 Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8266 strong(canonical_function_name(name))
8267 }
8268 Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8269 Expr::Extract { .. } => strong("extract".to_string()),
8270 Expr::Exists { .. } => strong("exists".to_string()),
8271 Expr::Array(_) => strong("array".to_string()),
8272 // `(expr).field` is named for the field, as a column would be.
8273 Expr::FieldAccess { field, .. } => strong(field.clone()),
8274 // A cast prefers its argument's name and settles for the type:
8275 // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8276 Expr::Cast {
8277 expr: inner,
8278 target,
8279 } => match figure_name_inner(inner) {
8280 (Some(n), NameStrength::Strong) => strong(n),
8281 // v7.38.7 — the fallback is the target type's INTERNAL name,
8282 // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8283 // the `bigint` the user typed. Measured on PG18 alongside
8284 // `CAST(7 AS bigint)`, which answers `int8` too.
8285 _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8286 },
8287 // A scalar subquery reports whatever its single output column
8288 // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8289 Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8290 // `CASE …` names itself, but weakly — a cast around it wins.
8291 Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8292 // A literal that carries its own type names itself for that type:
8293 // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8294 // reports nothing. Weak, like any other type name.
8295 Expr::Literal(Literal::Interval { .. }) => {
8296 (Some("interval".to_string()), NameStrength::Weak)
8297 }
8298 // A wrapper that adds no name of its own.
8299 Expr::Variadic(inner) => figure_name_inner(inner),
8300 Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
8301 // Everything else — operators, comparisons, IS NULL, LIKE, IN,
8302 // literals, placeholders — reports `?column?`.
8303 _ => (None, NameStrength::None),
8304 }
8305}
8306
8307/// The name a scalar subquery's single projected column reports.
8308fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
8309 match sel.items.as_slice() {
8310 [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
8311 [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
8312 _ => (None, NameStrength::None),
8313 }
8314}
8315
8316/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
8317fn pretty_prec(e: &Expr) -> u8 {
8318 match e {
8319 Expr::Binary { op, .. } => match op {
8320 // v7.39 (round 407) — this deparse ladder mirrors the parser's:
8321 // OR < XOR < AND < NOT < comparison < additive < multiplicative.
8322 // XOR (MySQL-only) sits between OR and AND, so AND and everything
8323 // above shifted +1 to open rung 2 for it.
8324 BinOp::Or => 1,
8325 BinOp::LogicalXor => 2,
8326 BinOp::And => 3,
8327 BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
8328 BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
8329 // Everything else in this enum is a comparison-shaped
8330 // operator; they share one level, as in the grammar.
8331 _ => 5,
8332 },
8333 Expr::Unary { op, .. } => match op {
8334 UnOp::Not => 4,
8335 UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
8336 },
8337 _ => u8::MAX,
8338 }
8339}
8340
8341/// Is this node an operator expression — the thing an arithmetic or
8342/// comparison parent keeps parentheses around? A cast inherits the
8343/// answer from what it casts.
8344fn pretty_is_compound(e: &Expr) -> bool {
8345 match e {
8346 Expr::Binary { .. } | Expr::Unary { .. } => true,
8347 Expr::Cast { expr, .. } => pretty_is_compound(expr),
8348 _ => false,
8349 }
8350}
8351
8352/// `parent` describes the enclosing operator: its binding power, and
8353/// whether it is a comparison (which keeps parens around any operator
8354/// operand) or a NOT (which keeps them at equal power too).
8355#[derive(Clone, Copy, PartialEq)]
8356enum PrettyParent {
8357 /// Nothing encloses this node.
8358 None,
8359 /// A comparison-shaped operator: an operator operand always keeps
8360 /// its parens, whatever precedence would allow.
8361 Comparison,
8362 /// Arithmetic / concatenation: precedence decides.
8363 Arith(u8),
8364 /// A boolean connective: precedence decides.
8365 Bool(u8),
8366 /// `NOT`: precedence decides, but equal power still needs parens so
8367 /// `NOT (NOT a > 1)` does not collapse.
8368 Not,
8369}
8370
8371fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
8372 let prec = pretty_prec(e);
8373 let is_unary_sign = matches!(
8374 e,
8375 Expr::Unary {
8376 op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
8377 ..
8378 }
8379 );
8380 let needs = match parent {
8381 PrettyParent::None => false,
8382 PrettyParent::Comparison => pretty_is_compound(e),
8383 // A sign always keeps its parens under an operator — PG writes
8384 // `(- a) + b` even though precedence would not require it.
8385 PrettyParent::Arith(p) => {
8386 is_unary_sign
8387 || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
8388 && (prec < p || (prec == p && is_rhs)))
8389 }
8390 PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
8391 PrettyParent::Not => {
8392 matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
8393 }
8394 };
8395 if needs {
8396 out.push('(');
8397 }
8398 match e {
8399 Expr::Binary { lhs, op, rhs } => {
8400 let child = match op {
8401 BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
8402 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
8403 PrettyParent::Arith(prec)
8404 }
8405 _ => PrettyParent::Comparison,
8406 };
8407 write_pretty(out, lhs, child, false, mysql);
8408 out.push(' ');
8409 out.push_str(&alloc::format!("{op}"));
8410 out.push(' ');
8411 // AND / OR are associative, so an explicitly right-nested
8412 // chain still prints as one chain.
8413 let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
8414 write_pretty(out, rhs, child, rhs_is_rhs, mysql);
8415 }
8416 Expr::Unary { op, expr } => match op {
8417 UnOp::Not => {
8418 out.push_str("NOT ");
8419 write_pretty(out, expr, PrettyParent::Not, false, mysql);
8420 }
8421 UnOp::Neg => {
8422 out.push_str("- ");
8423 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8424 }
8425 UnOp::Plus => {
8426 out.push_str("+ ");
8427 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8428 }
8429 UnOp::BitNot => {
8430 out.push('~');
8431 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8432 }
8433 },
8434 Expr::Cast { expr, target } => {
8435 if mysql {
8436 // MySQL's own spelling, which is what its error messages
8437 // quote back.
8438 out.push_str("cast(");
8439 write_pretty(out, expr, PrettyParent::None, false, mysql);
8440 out.push_str(&alloc::format!(
8441 " as {})",
8442 target.to_string().to_lowercase()
8443 ));
8444 } else {
8445 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8446 out.push_str(&alloc::format!("::{target}"));
8447 }
8448 }
8449 Expr::IsNull { expr, negated } => {
8450 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8451 out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
8452 }
8453 other => out.push_str(&alloc::format!("{other}")),
8454 }
8455 if needs {
8456 out.push(')');
8457 }
8458}
8459
8460const fn pretty_prec_not() -> u8 {
8461 // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
8462 // when the XOR insertion shifted the deparse ladder up by one).
8463 4
8464}
8465
8466impl fmt::Display for Expr {
8467 #[allow(clippy::too_many_lines)]
8468 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8469 match self {
8470 Self::Literal(l) => write!(f, "{l}"),
8471 Self::Column(c) => write!(f, "{c}"),
8472 Self::Placeholder(n) => write!(f, "${n}"),
8473 // Round-trips as the spelling PG's docs lead with.
8474 Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
8475 Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
8476 // v7.39 (round 311) — an AND / OR chain that nests to the
8477 // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
8478 // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
8479 // its parentheses, because that is a different grouping as
8480 // written. Both halves measured against PG 18.4's deparse,
8481 // which flattens a same-operator left chain at parse time and
8482 // leaves `a AND (b AND c)` alone.
8483 Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
8484 f.write_str("(")?;
8485 write_bool_chain(f, lhs, *op)?;
8486 write!(f, " {op} {rhs}")?;
8487 f.write_str(")")
8488 }
8489 Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
8490 Self::Unary { op, expr } => match op {
8491 UnOp::Not => write!(f, "(NOT {expr})"),
8492 // A space after the sign, as PG's deparse writes it.
8493 UnOp::Neg => write!(f, "(- {expr})"),
8494 UnOp::Plus => write!(f, "(+ {expr})"),
8495 UnOp::BitNot => write!(f, "(~{expr})"),
8496 },
8497 // The OPERAND carries the parentheses, not the cast:
8498 // `(a)::text`, `((a + b))::text`. PG words it this way, and
8499 // it is what keeps `a::text = t` from reading as a cast of
8500 // the comparison.
8501 Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
8502 Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
8503 Self::AggregateOrdered {
8504 call,
8505 order_by,
8506 distinct,
8507 filter,
8508 } => {
8509 let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8510 for (i, o) in order_by.iter().enumerate() {
8511 if i > 0 {
8512 f.write_str(", ")?;
8513 }
8514 write!(f, "{}", o.expr)?;
8515 if o.desc {
8516 f.write_str(" DESC")?;
8517 }
8518 match o.nulls_first {
8519 Some(true) => f.write_str(" NULLS FIRST")?,
8520 Some(false) => f.write_str(" NULLS LAST")?,
8521 None => {}
8522 }
8523 }
8524 Ok(())
8525 };
8526 // Ordered-set aggregates (`percentile_cont(f) WITHIN
8527 // GROUP (ORDER BY x)`) render the in-parens args as the
8528 // direct argument and the sort spec under WITHIN GROUP —
8529 // not as an in-argument ORDER BY.
8530 let ordered_set = matches!(
8531 call.as_ref(),
8532 Expr::FunctionCall { name, .. }
8533 if matches!(
8534 name.to_ascii_lowercase().as_str(),
8535 "percentile_cont" | "percentile_disc" | "mode"
8536 )
8537 );
8538 if ordered_set {
8539 write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
8540 fmt_order_by(f)?;
8541 f.write_str(")")?;
8542 } else {
8543 // `name([DISTINCT ]args [ORDER BY …])` — peel the
8544 // inner call's parens to splice modifiers.
8545 let inner = alloc::format!("{call}");
8546 let body = inner.strip_suffix(')').unwrap_or(&inner);
8547 let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
8548 write!(f, "{head}(")?;
8549 if *distinct {
8550 f.write_str("DISTINCT ")?;
8551 }
8552 write!(f, "{args_part}")?;
8553 if !order_by.is_empty() {
8554 f.write_str(" ORDER BY ")?;
8555 fmt_order_by(f)?;
8556 }
8557 f.write_str(")")?;
8558 }
8559 if let Some(cond) = filter {
8560 write!(f, " FILTER (WHERE {cond})")?;
8561 }
8562 Ok(())
8563 }
8564 Self::IsNull { expr, negated } => {
8565 if *negated {
8566 write!(f, "({expr} IS NOT NULL)")
8567 } else {
8568 write!(f, "({expr} IS NULL)")
8569 }
8570 }
8571 Self::BoolTest {
8572 expr,
8573 value,
8574 negated,
8575 } => {
8576 let word = match value {
8577 Some(true) => "TRUE",
8578 Some(false) => "FALSE",
8579 None => "UNKNOWN",
8580 };
8581 if *negated {
8582 write!(f, "({expr} IS NOT {word})")
8583 } else {
8584 write!(f, "({expr} IS {word})")
8585 }
8586 }
8587 Self::FunctionCall { name, args } => {
8588 write!(f, "{name}(")?;
8589 for (i, a) in args.iter().enumerate() {
8590 if i > 0 {
8591 f.write_str(", ")?;
8592 }
8593 write!(f, "{a}")?;
8594 }
8595 f.write_str(")")
8596 }
8597 Self::Like {
8598 expr,
8599 pattern,
8600 negated,
8601 case_insensitive,
8602 } => {
8603 let op = match (negated, case_insensitive) {
8604 (false, false) => "LIKE",
8605 (true, false) => "NOT LIKE",
8606 (false, true) => "ILIKE",
8607 (true, true) => "NOT ILIKE",
8608 };
8609 write!(f, "({expr} {op} {pattern})")
8610 }
8611 Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
8612 Self::WindowFunction {
8613 name,
8614 args,
8615 partition_by,
8616 order_by,
8617 frame,
8618 null_treatment,
8619 filter,
8620 } => {
8621 write!(f, "{name}(")?;
8622 for (i, a) in args.iter().enumerate() {
8623 if i > 0 {
8624 f.write_str(", ")?;
8625 }
8626 write!(f, "{a}")?;
8627 }
8628 f.write_str(")")?;
8629 // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
8630 // OVER; it round-trips so a window body's Display re-parses.
8631 if let Some(cond) = filter {
8632 write!(f, " FILTER (WHERE {cond})")?;
8633 }
8634 // v7.30.1 (mailrs round-24 class audit) — IGNORE
8635 // NULLS sits between the arg list and OVER; dropping
8636 // it reverted replayed queries to RESPECT NULLS.
8637 if matches!(null_treatment, NullTreatment::Ignore) {
8638 f.write_str(" IGNORE NULLS")?;
8639 }
8640 f.write_str(" OVER (")?;
8641 if !partition_by.is_empty() {
8642 f.write_str("PARTITION BY ")?;
8643 for (i, p) in partition_by.iter().enumerate() {
8644 if i > 0 {
8645 f.write_str(", ")?;
8646 }
8647 write!(f, "{p}")?;
8648 }
8649 }
8650 if !order_by.is_empty() {
8651 if !partition_by.is_empty() {
8652 f.write_str(" ")?;
8653 }
8654 f.write_str("ORDER BY ")?;
8655 for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
8656 if i > 0 {
8657 f.write_str(", ")?;
8658 }
8659 write!(f, "{e}")?;
8660 if *desc {
8661 f.write_str(" DESC")?;
8662 }
8663 match nulls_first {
8664 Some(true) => f.write_str(" NULLS FIRST")?,
8665 Some(false) => f.write_str(" NULLS LAST")?,
8666 None => {}
8667 }
8668 }
8669 }
8670 if let Some(fr) = frame {
8671 if !partition_by.is_empty() || !order_by.is_empty() {
8672 f.write_str(" ")?;
8673 }
8674 let k = match fr.kind {
8675 FrameKind::Rows => "ROWS",
8676 FrameKind::Range => "RANGE",
8677 FrameKind::Groups => "GROUPS",
8678 };
8679 if let Some(end) = &fr.end {
8680 write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
8681 } else {
8682 write!(f, "{k} {}", fr.start)?;
8683 }
8684 }
8685 f.write_str(")")
8686 }
8687 Self::ScalarSubquery(s) => write!(f, "({s})"),
8688 Self::Exists { subquery, negated } => {
8689 if *negated {
8690 write!(f, "NOT EXISTS ({subquery})")
8691 } else {
8692 write!(f, "EXISTS ({subquery})")
8693 }
8694 }
8695 Self::InSubquery {
8696 expr,
8697 subquery,
8698 negated,
8699 } => {
8700 if *negated {
8701 write!(f, "({expr} NOT IN ({subquery}))")
8702 } else {
8703 write!(f, "({expr} IN ({subquery}))")
8704 }
8705 }
8706 Self::RowInSubquery {
8707 row,
8708 subquery,
8709 negated,
8710 } => {
8711 write!(f, "(")?;
8712 for (i, e) in row.iter().enumerate() {
8713 if i > 0 {
8714 write!(f, ", ")?;
8715 }
8716 write!(f, "{e}")?;
8717 }
8718 let kw = if *negated { ") NOT IN (" } else { ") IN (" };
8719 write!(f, "{kw}{subquery})")
8720 }
8721 Self::RowCmpSubquery { row, op, subquery } => {
8722 write!(f, "(")?;
8723 for (i, e) in row.iter().enumerate() {
8724 if i > 0 {
8725 write!(f, ", ")?;
8726 }
8727 write!(f, "{e}")?;
8728 }
8729 write!(f, ") {op} ({subquery})")
8730 }
8731 Self::InList {
8732 expr,
8733 list,
8734 negated,
8735 } => {
8736 let kw = if *negated { " NOT IN (" } else { " IN (" };
8737 write!(f, "({expr}{kw}")?;
8738 for (i, e) in list.iter().enumerate() {
8739 if i > 0 {
8740 f.write_str(", ")?;
8741 }
8742 write!(f, "{e}")?;
8743 }
8744 f.write_str("))")
8745 }
8746 Self::Array(items) => {
8747 f.write_str("ARRAY[")?;
8748 for (i, e) in items.iter().enumerate() {
8749 if i > 0 {
8750 f.write_str(", ")?;
8751 }
8752 write!(f, "{e}")?;
8753 }
8754 f.write_str("]")
8755 }
8756 Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
8757 Self::ArraySlice { target, lo, hi } => {
8758 write!(f, "({target}[")?;
8759 if let Some(l) = lo {
8760 write!(f, "{l}")?;
8761 }
8762 write!(f, ":")?;
8763 if let Some(h) = hi {
8764 write!(f, "{h}")?;
8765 }
8766 write!(f, "])")
8767 }
8768 Self::AnyAll {
8769 expr,
8770 op,
8771 array,
8772 is_any,
8773 } => {
8774 let kw = if *is_any { "ANY" } else { "ALL" };
8775 write!(f, "({expr} {op} {kw}({array}))")
8776 }
8777 Self::Case {
8778 operand,
8779 branches,
8780 else_branch,
8781 } => {
8782 f.write_str("CASE")?;
8783 if let Some(op) = operand {
8784 write!(f, " {op}")?;
8785 }
8786 for (w, t) in branches {
8787 write!(f, " WHEN {w} THEN {t}")?;
8788 }
8789 if let Some(e) = else_branch {
8790 write!(f, " ELSE {e}")?;
8791 }
8792 f.write_str(" END")
8793 }
8794 }
8795 }
8796}
8797
8798/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
8799/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
8800pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
8801 use alloc::string::ToString;
8802 if scale == 0 {
8803 return alloc::format!("{unscaled}");
8804 }
8805 let neg = unscaled < 0;
8806 let digits = alloc::format!("{}", unscaled.unsigned_abs());
8807 let scale = scale as usize;
8808 let (int_part, frac_part) = if digits.len() > scale {
8809 (
8810 digits[..digits.len() - scale].to_string(),
8811 digits[digits.len() - scale..].to_string(),
8812 )
8813 } else {
8814 ("0".to_string(), alloc::format!("{digits:0>scale$}"))
8815 };
8816 alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
8817}
8818
8819/// A single-quoted SQL string, with an embedded quote doubled.
8820fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
8821 f.write_str("'")?;
8822 for c in s.chars() {
8823 if c == '\'' {
8824 f.write_str("''")?;
8825 } else {
8826 write!(f, "{c}")?;
8827 }
8828 }
8829 f.write_str("'")
8830}
8831
8832impl fmt::Display for Literal {
8833 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8834 match self {
8835 Self::Integer(n) => write!(f, "{n}"),
8836 Self::Float(x) => {
8837 let s = format!("{x}");
8838 // Default Display for an integral f64 (e.g. 1.0) emits "1",
8839 // which would round-trip back to Integer. Force a dot.
8840 if s.contains('.') || s.contains('e') || s.contains('E') {
8841 f.write_str(&s)
8842 } else {
8843 write!(f, "{s}.0")
8844 }
8845 }
8846 Self::Numeric { unscaled, scale } => {
8847 // Render the exact decimal `unscaled / 10^scale`, preserving
8848 // scale (trailing zeros) — round-trips to the same literal.
8849 f.write_str(&render_exact_decimal(*unscaled, *scale))
8850 }
8851 Self::NumericBig(s) => f.write_str(s),
8852 // Printed exactly as the text form was, so a reader cannot
8853 // tell whether the constant was decoded or not.
8854 Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
8855 Self::String(s) => write_quoted(f, s),
8856 Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
8857 Self::Null => f.write_str("NULL"),
8858 // PG external array form. Display round-trip re-enters
8859 // through the column-typed text coerce, same as pgwire.
8860 Self::TextArray(items) => {
8861 f.write_str("'{")?;
8862 for (i, it) in items.iter().enumerate() {
8863 if i > 0 {
8864 f.write_str(",")?;
8865 }
8866 match it {
8867 None => f.write_str("NULL")?,
8868 Some(s) => {
8869 f.write_str("\"")?;
8870 for c in s.chars() {
8871 match c {
8872 // array-element escapes
8873 '"' | '\\' => write!(f, "\\{c}")?,
8874 // the OUTER wrapper is a SQL string
8875 // literal — embedded quotes must
8876 // double, or the rendered form
8877 // (WAL replay parses it back) is
8878 // invalid SQL
8879 '\'' => f.write_str("''")?,
8880 _ => write!(f, "{c}")?,
8881 }
8882 }
8883 f.write_str("\"")?;
8884 }
8885 }
8886 }
8887 f.write_str("}'")
8888 }
8889 Self::IntArray(items) => {
8890 f.write_str("'{")?;
8891 for (i, it) in items.iter().enumerate() {
8892 if i > 0 {
8893 f.write_str(",")?;
8894 }
8895 match it {
8896 None => f.write_str("NULL")?,
8897 Some(n) => write!(f, "{n}")?,
8898 }
8899 }
8900 f.write_str("}'")
8901 }
8902 Self::BigIntArray(items) => {
8903 f.write_str("'{")?;
8904 for (i, it) in items.iter().enumerate() {
8905 if i > 0 {
8906 f.write_str(",")?;
8907 }
8908 match it {
8909 None => f.write_str("NULL")?,
8910 Some(n) => write!(f, "{n}")?,
8911 }
8912 }
8913 f.write_str("}'")
8914 }
8915 Self::Vector(v) => {
8916 f.write_str("[")?;
8917 for (i, x) in v.iter().enumerate() {
8918 if i > 0 {
8919 f.write_str(", ")?;
8920 }
8921 let s = format!("{x}");
8922 // Mirror Float Display: force a dot so re-parse stays
8923 // numerically literal.
8924 if s.contains('.') || s.contains('e') || s.contains('E') {
8925 f.write_str(&s)?;
8926 } else {
8927 write!(f, "{s}.0")?;
8928 }
8929 }
8930 f.write_str("]")
8931 }
8932 Self::Interval { text, .. } => {
8933 f.write_str("INTERVAL '")?;
8934 for c in text.chars() {
8935 if c == '\'' {
8936 f.write_str("''")?;
8937 } else {
8938 write!(f, "{c}")?;
8939 }
8940 }
8941 f.write_str("'")
8942 }
8943 }
8944 }
8945}
8946
8947impl fmt::Display for BinOp {
8948 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8949 f.write_str(match self {
8950 Self::Or => "OR",
8951 Self::And => "AND",
8952 Self::Eq => "=",
8953 Self::NotEq => "<>",
8954 Self::IsDistinctFrom => "IS DISTINCT FROM",
8955 Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
8956 Self::IntDiv => "DIV",
8957 Self::Lt => "<",
8958 Self::LtEq => "<=",
8959 Self::Gt => ">",
8960 Self::GtEq => ">=",
8961 Self::Add => "+",
8962 Self::Sub => "-",
8963 Self::Mul => "*",
8964 Self::Div => "/",
8965 Self::Mod => "%",
8966 Self::L2Distance => "<->",
8967 Self::GeomParallel => "?||",
8968 Self::OverLeft => "&<",
8969 Self::OverRight => "&>",
8970 Self::GeomPerp => "?-|",
8971 Self::GeomSameAs => "~=",
8972 Self::ClosestPoint => "##",
8973 Self::GeomHoriz => "?-",
8974 Self::InnerProduct => "<#>",
8975 Self::CosineDistance => "<=>",
8976 Self::Concat => "||",
8977 Self::BitOr => "|",
8978 Self::BitAnd => "&",
8979 Self::BitXor => "#",
8980 Self::LogicalXor => "xor",
8981 Self::JsonGet => "->",
8982 Self::JsonGetText => "->>",
8983 Self::JsonGetPath => "#>",
8984 Self::JsonGetPathText => "#>>",
8985 Self::JsonContains => "@>",
8986 Self::JsonPathExists => "@?",
8987 Self::JsonContainedBy => "<@",
8988 Self::JsonKeyExists => "?",
8989 Self::JsonKeysAny => "?|",
8990 Self::JsonKeysAll => "?&",
8991 Self::JsonDeletePath => "#-",
8992 Self::TsMatch => "@@",
8993 Self::InetContainedBy => "<<",
8994 Self::InetContainedByEq => "<<=",
8995 Self::InetContains => ">>",
8996 Self::InetContainsEq => ">>=",
8997 Self::InetOverlap => "&&",
8998 Self::Intersects => "?#",
8999 Self::IsBelow => "<^",
9000 Self::IsAbove => ">^",
9001 Self::PatternLt => "~<~",
9002 Self::PatternLtEq => "~<=~",
9003 Self::PatternGt => "~>~",
9004 Self::PatternGtEq => "~>=~",
9005 })
9006 }
9007}
9008
9009/// Quote `s` as a PG double-quoted identifier when required (keyword,
9010/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
9011/// Otherwise return it as-is. Returns an owned `String` to keep the call site
9012/// uniform.
9013pub(crate) fn quote_ident(s: &str) -> String {
9014 let needs_quote = match s.chars().next() {
9015 None => true,
9016 Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
9017 _ => {
9018 s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
9019 || s.chars().any(|c| c.is_ascii_uppercase())
9020 || is_keyword(s)
9021 }
9022 };
9023 if !needs_quote {
9024 return s.to_string();
9025 }
9026 let mut out = String::with_capacity(s.len() + 2);
9027 out.push('"');
9028 for c in s.chars() {
9029 if c == '"' {
9030 out.push_str("\"\"");
9031 } else {
9032 out.push(c);
9033 }
9034 }
9035 out.push('"');
9036 out
9037}
9038
9039fn is_keyword(s: &str) -> bool {
9040 matches!(
9041 &*s.to_ascii_lowercase(),
9042 "select"
9043 | "from"
9044 | "where"
9045 | "as"
9046 | "null"
9047 | "true"
9048 | "false"
9049 | "and"
9050 | "or"
9051 | "not"
9052 | "create"
9053 | "table"
9054 | "insert"
9055 | "into"
9056 | "values"
9057 | "index"
9058 | "on"
9059 | "begin"
9060 | "commit"
9061 | "rollback"
9062 | "is"
9063 | "between"
9064 | "in"
9065 | "like"
9066 | "group"
9067 | "distinct"
9068 | "union"
9069 | "all"
9070 | "join"
9071 | "inner"
9072 | "left"
9073 | "cross"
9074 | "outer"
9075 | "default"
9076 | "savepoint"
9077 | "release"
9078 | "to"
9079 | "having"
9080 | "show"
9081 | "extract"
9082 | "offset"
9083 | "asc"
9084 | "desc"
9085 | "interval"
9086 )
9087}
9088
9089#[cfg(test)]
9090mod tests {
9091 use super::*;
9092 use alloc::vec;
9093
9094 #[test]
9095 fn integer_literal_renders_without_dot() {
9096 assert_eq!(Literal::Integer(42).to_string(), "42");
9097 }
9098
9099 #[test]
9100 fn integral_float_keeps_dot() {
9101 assert_eq!(Literal::Float(1.0).to_string(), "1.0");
9102 assert_eq!(Literal::Float(1.5).to_string(), "1.5");
9103 assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
9104 }
9105
9106 #[test]
9107 fn string_literal_doubles_quote() {
9108 assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
9109 }
9110
9111 #[test]
9112 fn bool_and_null_render_uppercase() {
9113 assert_eq!(Literal::Bool(true).to_string(), "TRUE");
9114 assert_eq!(Literal::Bool(false).to_string(), "FALSE");
9115 assert_eq!(Literal::Null.to_string(), "NULL");
9116 }
9117
9118 #[test]
9119 fn binary_op_always_parenthesised() {
9120 let e = Expr::Binary {
9121 lhs: Box::new(Expr::Literal(Literal::Integer(1))),
9122 op: BinOp::Add,
9123 rhs: Box::new(Expr::Literal(Literal::Integer(2))),
9124 };
9125 assert_eq!(e.to_string(), "(1 + 2)");
9126 }
9127
9128 #[test]
9129 fn select_star_from_table() {
9130 let s = SelectStatement {
9131 locking: None,
9132 items: vec![SelectItem::Wildcard],
9133 from: Some(FromClause {
9134 primary: TableRef {
9135 name: "users".into(),
9136 alias: None,
9137 only: false,
9138 as_of_segment: None,
9139 unnest_expr: None,
9140 unnest_column_aliases: Vec::new(),
9141 with_ordinality: false,
9142 generate_series_args: None,
9143 lateral_subquery: None,
9144 jsonb_each_text_arg: None,
9145 table_fn_call: None,
9146 rows_from: None,
9147 json_table: None,
9148 scalar_fn_item: false,
9149 },
9150 joins: vec![],
9151 }),
9152 where_: None,
9153 group_by: None,
9154 group_by_all: false,
9155 having: None,
9156 unions: vec![],
9157 order_by: Vec::new(),
9158 limit: None,
9159 offset: None,
9160 limit_with_ties: false,
9161 window_check_exprs: Vec::new(),
9162 distinct: false,
9163 distinct_on: Vec::new(),
9164 ctes: vec![],
9165 };
9166 assert_eq!(s.to_string(), "SELECT * FROM users");
9167 }
9168
9169 #[test]
9170 fn quote_ident_for_uppercase_and_keyword() {
9171 assert_eq!(quote_ident("foo"), "foo");
9172 assert_eq!(quote_ident("Foo"), "\"Foo\"");
9173 assert_eq!(quote_ident("select"), "\"select\"");
9174 assert_eq!(quote_ident(""), "\"\"");
9175 assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
9176 }
9177}