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)]
18#[non_exhaustive]
19pub enum CopyFormat {
20 #[default]
21 Text,
22 Csv,
23}
24
25/// Options for `COPY … TO STDOUT [WITH] (…)`. Defaults reproduce the
26/// bare `COPY … TO STDOUT` text-format behaviour, so an empty option
27/// list is a no-op. `delimiter` / `null_str` / `quote` fall back to the
28/// per-format defaults (text: `\t` / `\N`; csv: `,` / `` / `"`) when
29/// unset.
30#[derive(Debug, Clone, PartialEq, Eq, Default)]
31pub struct CopyOptions {
32 pub format: CopyFormat,
33 pub header: bool,
34 pub delimiter: Option<char>,
35 pub null_str: Option<String>,
36 pub quote: Option<char>,
37 /// v7.39 (round 247) — CSV `ESCAPE`: the character that precedes a
38 /// quote (or itself) inside a quoted cell. Defaults to the quote
39 /// character (PG's doubling behavior).
40 pub escape: Option<char>,
41 /// v7.39 (round 247) — CSV `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`:
42 /// columns whose non-NULL cells always quote. `Some(vec![])` is the
43 /// `*` spelling (every column).
44 pub force_quote: Option<Vec<String>>,
45 /// v7.39 (round 265) — CSV `FORCE_NOT_NULL (col, …)`: for these
46 /// columns an UNQUOTED empty field reads as the empty string rather
47 /// than NULL (probed). COPY FROM only.
48 pub force_not_null: Option<Vec<String>>,
49 /// v7.39 (round 265) — CSV `FORCE_NULL (col, …)`: for these columns
50 /// a QUOTED empty field (`""`) also reads as NULL (probed). COPY
51 /// FROM only.
52 pub force_null: Option<Vec<String>>,
53}
54
55/// v7.39 (round 218) — FETCH / MOVE cursor direction. PG grammar: single-row
56/// forms (NEXT / PRIOR / FIRST / LAST / ABSOLUTE n / RELATIVE n) return at
57/// most one row; multi-row forms (bare n / ALL / FORWARD [n|ALL] /
58/// BACKWARD [n|ALL]) stream a run. A negative bare/FORWARD count means
59/// BACKWARD (normalized at execution).
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum CursorDirection {
62 Next,
63 Prior,
64 First,
65 Last,
66 Absolute(i64),
67 Relative(i64),
68 /// Bare `FETCH n` / `FORWARD n` (negative = backward n).
69 Count(i64),
70 /// `ALL` / `FORWARD ALL`.
71 All,
72 Backward(i64),
73 BackwardAll,
74}
75
76impl fmt::Display for CursorDirection {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::Next => f.write_str("NEXT"),
80 Self::Prior => f.write_str("PRIOR"),
81 Self::First => f.write_str("FIRST"),
82 Self::Last => f.write_str("LAST"),
83 Self::Absolute(n) => write!(f, "ABSOLUTE {n}"),
84 Self::Relative(n) => write!(f, "RELATIVE {n}"),
85 Self::Count(n) => write!(f, "FORWARD {n}"),
86 Self::All => f.write_str("ALL"),
87 Self::Backward(n) => write!(f, "BACKWARD {n}"),
88 Self::BackwardAll => f.write_str("BACKWARD ALL"),
89 }
90 }
91}
92
93/// v7.39 (round 320, V53) — what a `DISCARD` throws away.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum DiscardTarget {
96 All,
97 Plans,
98 Sequences,
99 Temp,
100}
101
102impl fmt::Display for DiscardTarget {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(match self {
105 Self::All => "ALL",
106 Self::Plans => "PLANS",
107 Self::Sequences => "SEQUENCES",
108 Self::Temp => "TEMP",
109 })
110 }
111}
112
113/// v7.39 (round 535) — which maintenance statement, and therefore what
114/// its target names. Measured on PG18: INDEX / TABLE / CLUSTER name a
115/// relation, SCHEMA names a schema, and SYSTEM / DATABASE name neither
116/// in a way SPG can refuse.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum MaintainKind {
120 ReindexRelation,
121 ReindexSchema,
122 /// `REINDEX SYSTEM` / `REINDEX DATABASE`, and a bare `CLUSTER`.
123 Whole,
124 ClusterRelation,
125}
126
127/// v7.39 (round 547) — see [`Statement::SetDbRoleSetting`]. Boxed in the
128/// enum so the variant costs one pointer.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct SetDbRoleSettingStatement {
131 pub database: Option<String>,
132 pub role: Option<String>,
133 pub param: Option<String>,
134 pub value: Option<String>,
135}
136
137/// v7.39 (round 696) — which operand a [`Statement::ValidateOnly`] names,
138/// and therefore which catalog answers whether it exists.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum ValidateOnlyKind {
141 /// `LOCK TABLE <t> [, …]` — the relation must exist.
142 LockTable,
143 /// Every role named must exist: `DROP OWNED BY <r> [, …]`,
144 /// `REASSIGN OWNED BY <r> [, …] TO <r>`, `ALTER ROLE <r> …`.
145 RoleName,
146 /// `SECURITY LABEL …` — PG refuses unconditionally, because no label
147 /// provider is loaded. SPG has none either.
148 SecurityLabel,
149 /// v7.39 (round 697) — `CREATE EXTENSION <e>`: the extension must be
150 /// AVAILABLE (PG: `extension "x" is not available`).
151 ExtensionAvailable,
152 /// v7.39 (round 708) — `ALTER TYPE <t> <any no-op form>`: the TYPE must
153 /// exist (PG: `type "x" does not exist`); the action itself stays a
154 /// no-op (PG genuinely renames; that residual is recorded).
155 TypeName,
156 /// v7.39 (round 708) — `ALTER AGGREGATE name(args) …`: names[0] is the
157 /// aggregate, the rest its argument type names (`*` = the `(*)` form).
158 /// Existence only; the action no-ops (PG really renames built-ins —
159 /// measured — and SPG does not model that).
160 AggregateName,
161 /// v7.39 (round 708) — `DROP CONVERSION <c>`: SPG ships no conversions,
162 /// so every name answers PG's `conversion "x" does not exist`.
163 ConversionName,
164 /// v7.39 (round 708) — `DROP LANGUAGE <l>`: an unknown language does
165 /// not exist; a shipped one is required (PG's two wordings, measured).
166 LanguageName,
167 /// v7.39 (round 709) — a collation name: performable or PG's
168 /// `collation "x" for encoding "UTF8" does not exist`.
169 CollationName,
170 /// v7.39 (round 709) — a text search configuration name.
171 TsConfigName,
172 /// v7.39 (round 709) — an event trigger name. SPG has none, so the
173 /// not-found answer is total.
174 EventTriggerName,
175 /// v7.39 (round 709) — a tablespace name. SPG has none beyond PG's two
176 /// built-ins, whose drop PG refuses with `permission denied` (measured).
177 TablespaceName,
178 /// v7.39 (round 709) — a large-object oid (names[0], decimal). The
179 /// registry is real (round 287), so the check is a lookup.
180 LargeObjectOid,
181 /// v7.39 (round 706) — `CREATE SERVER` / `CREATE FOREIGN TABLE` /
182 /// `CREATE FOREIGN DATA WRAPPER`. SPG has no foreign-data
183 /// infrastructure at all, so PG's refusals (`foreign-data wrapper "x"
184 /// does not exist`, `server "x" does not exist`) cannot be copied —
185 /// PG can refuse because the missing piece is installable there.
186 /// Accepted with a WARNING, the extension resolution (round 697):
187 /// refusing turns a dump that restores today into one that needs
188 /// editing, and silent acceptance was the actual defect.
189 ForeignInfra,
190 /// v7.39 (round 697) — `DROP EXTENSION <e>`: it must be installed
191 /// (PG: `extension "x" does not exist`).
192 ExtensionInstalled,
193 /// v7.40.12 — `SET SESSION AUTHORIZATION <r>`. APPENDED, not
194 /// inserted: putting it beside `RoleName` where it belongs read
195 /// better and shifted thirteen discriminants, which
196 /// `cargo-semver-checks` calls out as its own breaking change
197 /// (`enum_no_repr_variant_discriminant_changed`) for anyone casting
198 /// the enum. Reading order is not worth a break nobody asked for.
199 ///
200 /// It used to share `RoleName` with the three role statements, and one question separates
201 /// them: PG does NOT count this statement as a query, so a later
202 /// `SET TRANSACTION ISOLATION LEVEL` still succeeds, while after
203 /// `DROP OWNED BY`, `REASSIGN OWNED BY` or `ALTER ROLE` it is
204 /// refused with 25001. All four measured on PG 18.6. The role check
205 /// is identical; the snapshot answer is not.
206 SessionAuthorization,
207}
208
209#[derive(Debug, Clone, PartialEq)]
210#[allow(clippy::large_enum_variant)] // Statement::Select dominates; Boxing would touch every match site
211pub enum Statement {
212 /// v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET <name>`.
213 ///
214 /// It used to be swallowed with the rest of the ALTER no-ops, which meant
215 /// `ALTER SYSTEM SET nosuch_guc = 1` was ACCEPTED where PG18 answers
216 /// `unrecognized configuration parameter`. SPG still applies nothing —
217 /// there is no postgresql.auto.conf to write — but a name it does not
218 /// know is now refused rather than swallowed.
219 ///
220 /// `None` is `RESET ALL`, which names no parameter.
221 AlterSystem {
222 parameter: Option<String>,
223 },
224 /// `DROP DATABASE [IF EXISTS] <name>`. SPG is single-database, so
225 /// this never succeeds; the name and the flag are carried so the
226 /// engine can answer with PG's wording for the two cases PG itself
227 /// has — an unknown name, or the database you are connected to.
228 DropDatabase {
229 name: String,
230 if_exists: bool,
231 },
232 /// A statement SPG accepts as a no-op but PG refuses inside a
233 /// transaction block — today `CREATE DATABASE` / `DROP DATABASE`,
234 /// which are no-ops here because SPG is single-database.
235 ///
236 /// The no-op path they used to share (`Statement::Empty`) also
237 /// carries CREATE ROLE, CREATE CAST and a dozen others that PG is
238 /// happy to run inside a transaction, so the object has to be named
239 /// to refuse the right ones.
240 NoOpPreventedInTransaction {
241 what: String,
242 /// v7.38.18 — `CREATE DATABASE … LC_COLLATE 'de_DE.utf8'` is in
243 /// every PostgreSQL bootstrap script there is, and SPG threw the
244 /// whole statement away. Being single-database makes the NAME a
245 /// no-op; it does not make the collation one, and a database
246 /// that quietly sorts by the container's `LANG` instead of the
247 /// one the script asked for is a silent difference in every
248 /// `ORDER BY` it will ever run.
249 ///
250 /// `LOCALE` and `LC_COLLATE` both land here; `LC_CTYPE` does
251 /// not, because SPG has no separate ctype.
252 collation: Option<String>,
253 /// v7.38.19 — the database's name, so `pg_database` can list one
254 /// that was created and can be connected to. It was thrown away
255 /// with the rest of the statement.
256 name: Option<String>,
257 },
258 /// v7.39 (round 696) — statements SPG performs nothing for, but whose
259 /// OPERAND PG validates before performing nothing either.
260 ///
261 /// All four used to be consumed whole by `is_dump_noise_statement`,
262 /// which meant `LOCK TABLE nosuch` and `DROP OWNED BY nosuchrole` were
263 /// ACCEPTED where PG18 errors. Accepting a statement that names
264 /// something that does not exist is the F29 shape: the caller is told
265 /// their intent was understood when the object it referred to is not
266 /// there.
267 ///
268 /// They share one variant because they share one rule — resolve the
269 /// name, refuse if absent, otherwise no-op — and four variants would be
270 /// four places for that rule to drift.
271 /// v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS] name(argtypes)[, …]`.
272 /// Consumed whole by the dump-noise list before, so `DROP AGGREGATE
273 /// nosuch(int)` reported success. PG validates every named aggregate's
274 /// EXISTENCE first (measured: a list with one unknown fails on the
275 /// unknown even when an earlier entry exists), renders the signature
276 /// with canonical type names (`int` → `integer`), and refuses to drop a
277 /// built-in (`cannot drop function sum(integer) because it is required
278 /// by the database system`). Every SPG aggregate is a built-in, so the
279 /// outcome is one of those two errors — or the IF EXISTS no-op.
280 ///
281 /// `args` holds the argument type names as written; `None` is the
282 /// `(*)` spelling.
283 DropAggregate {
284 if_exists: bool,
285 items: Vec<(String, Option<Vec<String>>)>,
286 },
287 /// v7.39 (round 750) — `ALTER ROLE|USER <name> … PASSWORD 'x' |
288 /// PASSWORD NULL`. The one attribute of the no-op family with a
289 /// SECURITY consequence: it was silently dropped (ledgered r710),
290 /// so a rotated credential never rotated. `None` = PASSWORD NULL
291 /// (the role keeps existing but can no longer password-auth).
292 AlterRolePassword {
293 name: String,
294 password: Option<String>,
295 },
296 ValidateOnly {
297 kind: ValidateOnlyKind,
298 /// The names the statement referred to. Empty means the form names
299 /// nothing (`SECURITY LABEL`, whose refusal is unconditional).
300 names: Vec<String>,
301 },
302
303 /// v7.39 (round 547) — `ALTER ROLE … SET/RESET` and
304 /// `ALTER DATABASE … SET/RESET`: the GUC defaults a session picks up
305 /// when it starts. Both used to land in the pg_dump no-op tail, so
306 /// the statement reported success and changed nothing.
307 ///
308 /// `database` / `role` are `None` for PG's oid 0 — `ALTER ROLE ALL`
309 /// sets both to None. `param` is `None` for RESET ALL. `value` is
310 /// `None` for RESET of one parameter.
311 SetDbRoleSetting(Box<SetDbRoleSettingStatement>),
312 /// v7.39 (round 288) — `SET CONSTRAINTS { ALL | <name>… }
313 /// { DEFERRED | IMMEDIATE }`. `deferred` carries the timing; the
314 /// name list is not yet honoured (ALL is what pg_dump emits and
315 /// what a circular-FK restore needs), so a named form applies to
316 /// all deferrable constraints too rather than silently doing
317 /// nothing.
318 /// v7.39 (round 308) — `SET CONSTRAINTS { ALL | name [, …] }
319 /// { DEFERRED | IMMEDIATE }`. An empty `names` is the ALL form;
320 /// otherwise the timing applies only to the constraints listed.
321 SetConstraints {
322 names: Vec<String>,
323 deferred: bool,
324 },
325
326 /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
327 /// [CASCADE | RESTRICT]`. Engine removes the matching tables
328 /// (each one) from the catalog; IF EXISTS makes the drop
329 /// idempotent. CASCADE / RESTRICT trailers parsed silently
330 /// (SPG always cascades index drops on table drop).
331 DropTable {
332 names: Vec<String>,
333 if_exists: bool,
334 },
335 /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
336 /// matching index across whichever table holds it.
337 DropIndex {
338 name: String,
339 if_exists: bool,
340 /// v7.39.7 — the table named by MySQL's `DROP INDEX i ON t`.
341 ///
342 /// MySQL keys an index name inside its table and its statement
343 /// says so; PostgreSQL keys it in the schema and has no `ON`
344 /// clause at all. `None` is the PostgreSQL form, which searches
345 /// every table for the name, and is what the MySQL dialect
346 /// refuses — as MySQL does.
347 table: Option<String>,
348 },
349 /// v7.14.0 — empty / comment-only statement. The lexer strips
350 /// `--` line comments and `/* … */` block comments (including
351 /// the MySQL conditional `/*!NNNNN … */` form) before the
352 /// parser ever sees them; a SQL chunk that contains nothing
353 /// else lands here. Engine returns CommandOk no-op so
354 /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
355 /// wrapped in conditional comments, etc.) load cleanly.
356 /// v7.39 (round 277) — SQL-level `PREPARE <name> [(type, …)] AS
357 /// <stmt>`. Session-scoped; the body keeps its `$N` placeholders
358 /// and is substituted at EXECUTE time.
359 Prepare {
360 name: String,
361 /// Declared parameter type names, in order. Empty when the
362 /// `(type, …)` list was omitted (PG infers them).
363 param_types: Vec<String>,
364 body: alloc::boxed::Box<Statement>,
365 /// The statement's own source text, which
366 /// `pg_prepared_statements.statement` reports verbatim.
367 source: String,
368 },
369 /// v7.39 (round 277) — `EXECUTE <name> [(arg, …)]`.
370 Execute {
371 name: String,
372 args: Vec<Expr>,
373 },
374 /// v7.39 (round 277) — `DEALLOCATE {<name> | ALL}`. `None` = ALL.
375 Deallocate(Option<String>),
376 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
377 /// [(kind, …)] ON <col>, … FROM <table>`. SPG records the object so
378 /// dumps restore and reflection is honest; the planner does not
379 /// consult it yet.
380 CreateStatistics {
381 name: String,
382 if_not_exists: bool,
383 /// Requested kinds as PG's single letters (`d` ndistinct,
384 /// `f` dependencies, `m` mcv). Empty = PG's default set.
385 kinds: Vec<String>,
386 columns: Vec<String>,
387 table: String,
388 },
389 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
390 DropStatistics {
391 name: String,
392 if_exists: bool,
393 },
394 /// v7.39 (round 278) — `CALL <proc>(…)`. Parses; the engine
395 /// reports that the procedure does not exist, because SPG has no
396 /// procedure catalog. Carried as a statement rather than raised at
397 /// parse time so the failure is a missing OBJECT (42883), not a
398 /// syntax error.
399 Call(String),
400 /// v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'`. Same shape:
401 /// 2PC is unavailable, which PG itself reports when
402 /// `max_prepared_transactions` is 0.
403 PrepareTransaction(String),
404 Empty,
405 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE]
406 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. The
407 /// canonical driver path for streaming large result sets (psycopg2
408 /// named cursors, JDBC setFetchSize).
409 DeclareCursor {
410 name: String,
411 /// `None` = neither keyword (PG default: backward allowed when the
412 /// plan supports it — always, for SPG's materialized cursors);
413 /// `Some(true)` = SCROLL; `Some(false)` = NO SCROLL (backward
414 /// fetch errors 55000).
415 scroll: Option<bool>,
416 /// `WITH HOLD` — survives the creating transaction's COMMIT.
417 hold: bool,
418 query: Box<Statement>,
419 },
420 /// v7.39 (round 218) — `FETCH [<direction>] [FROM|IN] <name>`.
421 FetchCursor {
422 name: String,
423 direction: CursorDirection,
424 },
425 /// v7.39 (round 218) — `MOVE [<direction>] [FROM|IN] <name>`: FETCH
426 /// without returning rows; the command tag carries the move count.
427 MoveCursor {
428 name: String,
429 direction: CursorDirection,
430 },
431 /// v7.39 (round 218) — `CLOSE <name>` / `CLOSE ALL` (`None` = ALL).
432 CloseCursor {
433 name: Option<String>,
434 },
435 /// v7.39 (round 222) — `LISTEN <channel>`: subscribe this session to
436 /// async notifications on the channel.
437 Listen(String),
438 /// v7.39 (round 222) — `NOTIFY <channel> [, '<payload>']`. Delivered at
439 /// COMMIT (PG semantics: transactional, deduplicated within the tx);
440 /// immediately under autocommit.
441 Notify {
442 channel: String,
443 payload: Option<String>,
444 },
445 /// v7.39 (round 222) — `UNLISTEN <channel>` / `UNLISTEN *` (`None` = *).
446 Unlisten(Option<String>),
447 /// `COPY table [(cols)] TO STDOUT` — the engine renders the
448 /// visible rows in COPY text format (tab-separated, `\N`
449 /// nulls, backslash escapes) as a single-text-column result
450 /// set; the wire layer streams CopyData from it.
451 CopyTo {
452 table: String,
453 columns: Option<Vec<String>>,
454 /// v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT`: an
455 /// arbitrary SELECT/VALUES/CTE (a whole [`Statement`], so set-ops and
456 /// VALUES ride through unchanged) whose result set is streamed in COPY
457 /// format. `Some` overrides `table`/`columns` (which are empty then);
458 /// `None` is the classic `COPY <table> …` shape.
459 query: Option<Box<Statement>>,
460 /// v7.37.x — `WITH (FORMAT csv, HEADER, DELIMITER, NULL, QUOTE)`
461 /// and the legacy `WITH CSV HEADER …` spelling. Default =
462 /// text format, no header (bare `COPY … TO STDOUT`).
463 options: CopyOptions,
464 },
465 /// v7.39 (round 249) — `COPY table [(cols)] FROM '<path>' [(opts)]`.
466 /// The engine is no_std and cannot read the file itself: the host
467 /// (embedded / server / tooling) reads the path and hands the bytes to
468 /// `Engine::copy_from_buffer`. Dispatching this statement straight to
469 /// the engine reports that contract.
470 CopyFromFile {
471 table: String,
472 columns: Option<Vec<String>>,
473 path: String,
474 options: CopyOptions,
475 },
476 /// v7.39 (round 249/252) — `COPY <table> [(cols)] TO '<file>'` (and
477 /// the `COPY (<query>) TO '<file>'` form). The engine is no_std and
478 /// cannot write the file itself: the host renders the payload via
479 /// `Engine::copy_to_buffer` and writes the path.
480 CopyToFile {
481 table: String,
482 columns: Option<Vec<String>>,
483 query: Option<Box<Statement>>,
484 path: String,
485 options: CopyOptions,
486 },
487 Select(SelectStatement),
488 CreateTable(CreateTableStatement),
489 /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
490 /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
491 /// no-op so PG dumps that include extension declarations
492 /// (notably `pgvector`) load against SPG without splitting
493 /// init scripts. mailrs migration follow-up F3.
494 CreateExtension(String),
495 /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
496 /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
497 /// the engine executes it at top level (mailrs round-10
498 /// A.2). Pre-v7.16.2 the parser discarded the body and the
499 /// engine returned CommandOk — a SEV-1 silent no-op that
500 /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
501 /// $$` idempotent migrations into invisible no-ops.
502 DoBlock(PlPgSqlBlock),
503 CreateIndex(CreateIndexStatement),
504 Insert(InsertStatement),
505 /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
506 Update(UpdateStatement),
507 /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
508 Delete(DeleteStatement),
509 /// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ `MERGE` statement.
510 /// `MERGE INTO target [alias] USING source [alias] ON cond
511 /// WHEN MATCHED [AND cond] THEN { UPDATE SET … | DELETE | DO NOTHING }
512 /// WHEN NOT MATCHED [AND cond] THEN { INSERT (cols) VALUES (vals) | DO NOTHING }
513 /// [WHEN …]`. SPG v7.17 supports table-based source (subquery
514 /// source is a follow-up); BY SOURCE / BY TARGET and RETURNING
515 /// are also follow-ups.
516 Merge(MergeStatement),
517 /// v7.39 (round 169) — `VACUUM [(opts)] [FULL|FREEZE|VERBOSE|ANALYZE]
518 /// [<table>]`. Was a parse-time no-op from the pre-MVCC era; with the
519 /// in-place MVCC gate ON, tombstoned versions are REAL bloat and a
520 /// customer's manual VACUUM must actually reclaim. `analyze` mirrors
521 /// the `VACUUM ANALYZE` spelling.
522 Vacuum {
523 table: Option<String>,
524 analyze: bool,
525 },
526 /// `BEGIN` / `START TRANSACTION` — with an optional explicit
527 /// `ISOLATION LEVEL …` mode (`None` = use the session default). PG
528 /// applies the level for the duration of this transaction only.
529 Begin(TransactionModes),
530 Commit,
531 Rollback,
532 /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
533 /// stack so a later `ROLLBACK TO <name>` can undo just the work
534 /// since this point.
535 Savepoint(String),
536 /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
537 /// named savepoint and discard later savepoints. Does not end the
538 /// transaction.
539 RollbackToSavepoint(String),
540 /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
541 /// rolling back. Keeps the work done since then.
542 ReleaseSavepoint(String),
543 /// `SHOW TABLES` — return the list of tables in the catalog.
544 ShowTables,
545 /// v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES` /
546 /// `SHOW SCHEMAS`. SPG is single-database; the executor
547 /// returns the canonical MySQL set so the mysql / MariaDB
548 /// client populates its database selector.
549 ShowDatabases,
550 /// v7.39.2 — MySQL `USE <db>`.
551 ///
552 /// It parsed as `Empty` and did nothing at all, so `USE myapp;
553 /// SELECT DATABASE()` answered the same constant it answered before
554 /// — measured against MySQL 9.7.2, which answers `myapp`. SPG serves
555 /// ONE database and answers to any name (see `CREATE DATABASE`), so
556 /// this does not switch catalogs; it records the NAME, which is the
557 /// half a client can observe and the half the PostgreSQL wire has
558 /// tracked since v7.39 (`current_database()` names what the startup
559 /// message asked for).
560 UseDatabase(String),
561 /// v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE TABLE <t>`
562 /// returns a 2-column row `(Table, "Create Table")` carrying
563 /// the synthesized DDL. mysqldump emits this for every
564 /// table at scrape time.
565 ShowCreateTable(String),
566 /// v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES FROM <t>`
567 /// (also `SHOW INDEX`, `SHOW KEYS`).
568 ShowIndexes(String),
569 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS`.
570 ShowStatus,
571 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW VARIABLES`.
572 ShowVariables,
573 /// r1067 — MySQL `SHOW VARIABLES LIKE 'pattern'` (sysbench-tpcc
574 /// probes isolation with it at connect).
575 ShowVariablesLike(String),
576 /// v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
577 ShowProcesslist,
578 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
579 /// pgbouncer sends `DISCARD ALL` between pooled client sessions to make
580 /// the connection look brand new to the next client; it used to be
581 /// swallowed as dump noise, so nothing was discarded.
582 Discard(DiscardTarget),
583 /// v7.39 (round 318, V51) — MySQL `KILL [CONNECTION | QUERY] <expr>`.
584 /// The id is an expression because MariaDB accepts one
585 /// (`KILL connection_id()` is the documented way to drop your own
586 /// connection). `query_only` is the `QUERY` form: stop the target's
587 /// running statement but leave it connected.
588 Kill {
589 query_only: bool,
590 id: Box<Expr>,
591 },
592 /// `SHOW COLUMNS FROM <table>` — return one row per column with
593 /// its declared name / type / nullability.
594 ShowColumns(String),
595 /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
596 /// Role is optional; defaults to `readonly` when omitted.
597 CreateUser(CreateUserStatement),
598 /// `DROP USER 'name'` (v4.1). v7.39 (read01 round 58) — `IF EXISTS` is
599 /// carried through: PG skips with a NOTICE rather than erroring.
600 DropUser {
601 name: String,
602 if_exists: bool,
603 },
604 /// v7.39 (RLS) — `SET ROLE { name | NONE | DEFAULT }` / `RESET ROLE`.
605 /// `Some(name)` switches the session's effective role (drives
606 /// `current_user` and RLS enforcement); `None` resets to the login
607 /// identity (the Admin superuser).
608 SetRole(Option<String>),
609 /// v7.39 (read01 round 57) — `GRANT <privs> ON <object> TO <roles>`.
610 Grant(GrantStatement),
611 /// v7.39 (read01 round 57) — `REVOKE [GRANT OPTION FOR] <privs> ON
612 /// <object> FROM <roles>`.
613 Revoke(GrantStatement),
614 /// v7.39 (RLS) — `CREATE POLICY name ON table …`.
615 CreatePolicy(CreatePolicyStatement),
616 /// v7.39 (RLS) — `ALTER POLICY name ON table …`.
617 AlterPolicy(AlterPolicyStatement),
618 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
619 DropPolicy(DropPolicyStatement),
620 /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
621 ShowUsers,
622 /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
623 /// single-column text table describing the rewritten plan tree
624 /// for `inner`. `analyze` triggers an actual exec to attach
625 /// observed row counts and elapsed micros to each node.
626 Explain(ExplainStatement),
627 /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
628 /// Synchronous rebuild of an NSW index. With the optional
629 /// encoding clause, every stored cell at the indexed column is
630 /// also re-encoded through `coerce_value` before the new graph
631 /// builds.
632 AlterIndex(AlterIndexStatement),
633 /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
634 /// The only setting in v6.7.2 is `hot_tier_bytes`, which
635 /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
636 /// for the named table.
637 AlterTable(AlterTableStatement),
638 /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
639 /// The catalog row lives in `spg_publications`. Publisher-side
640 /// WAL filtering arrives in v6.1.5.
641 CreatePublication(CreatePublicationStatement),
642 /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
643 /// no-op when the publication does not exist.
644 DropPublication {
645 name: String,
646 /// v7.39 (round 754, F31-B4) — `IF EXISTS` quietly skips a
647 /// missing publication; the bare form refuses with PG's
648 /// sentence (PG18-measured — the old "silent no-op" note on
649 /// the executor was wrong).
650 if_exists: bool,
651 },
652 /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
653 /// publication ordered by name with `(name, scope_summary,
654 /// table_count)` columns. The scope summary is the human-
655 /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
656 /// TABLES EXCEPT …`; `table_count` is `NULL` for the
657 /// `AllTables` scope and the table-list length otherwise.
658 ShowPublications,
659 /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
660 /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
661 /// in `spg_subscriptions`; when the subscription is
662 /// `enabled = true` (default) the server spawns a
663 /// background worker that connects to `conn` and drains the
664 /// requested publication(s) into the local engine.
665 CreateSubscription(CreateSubscriptionStatement),
666 /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
667 /// PUBLICATION, silent no-op when absent. Stops the
668 /// associated worker thread before removing the row.
669 DropSubscription {
670 name: String,
671 /// v7.39 (round 754, F31-B4) — same contract as
672 /// [`Statement::DropPublication`].
673 if_exists: bool,
674 },
675 /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
676 /// subscription ordered by name with `(name, conn_str,
677 /// publications, enabled, last_received_pos)`.
678 ShowSubscriptions,
679 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
680 /// Blocks until the local server's apply position reaches
681 /// `<pos>` or `<ms>` elapses. Server-layer command: the
682 /// engine refuses it (`EngineError::Unsupported`) since
683 /// `lag_state` lives in `spg-server`'s `ServerState`.
684 WaitForWalPosition {
685 pos: u64,
686 /// `None` → wait forever; `Some(ms)` → return after `ms`
687 /// milliseconds even if the target isn't reached.
688 timeout_ms: Option<u64>,
689 },
690 /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
691 /// table; `ANALYZE <name>` re-stats just one. Populates
692 /// `spg_statistic` with per-column null_frac + n_distinct +
693 /// 100-bucket equi-depth histogram.
694 Analyze(Option<String>),
695 /// v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
696 /// PostgreSQL has only `ALTER TABLE … RENAME TO`, so this spelling
697 /// had nowhere to go; it is what a MySQL migration writes.
698 RenameTables(Vec<(String, String)>),
699 /// v7.39 (round 535) — `REINDEX { INDEX | TABLE | SCHEMA | DATABASE
700 /// | SYSTEM } [CONCURRENTLY] <name>` and `CLUSTER [VERBOSE]
701 /// [<table> [USING <index>]]`.
702 ///
703 /// SPG has neither index bloat nor a clustering order to rebuild, so
704 /// the work is a no-op — but PG VALIDATES the target, and both were
705 /// swallowed at parse time, so `REINDEX TABLE typo` reported success.
706 /// The name is carried now so the engine can say what PG says.
707 Maintain {
708 kind: MaintainKind,
709 /// `REINDEX … CONCURRENTLY`. Carried for the same reason as
710 /// [`CreateIndexStatement::concurrently`]: PG bars the
711 /// CONCURRENTLY form inside a transaction block and allows the
712 /// plain one.
713 concurrently: bool,
714 /// `None` for the whole-database forms, which name nothing.
715 target: Option<String>,
716 },
717 /// v7.37.17 (17.6 sibling) — `TRUNCATE [TABLE] [ONLY] <name>
718 /// [, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE |
719 /// RESTRICT]`. Clears every row from each named table. SPG's
720 /// SEQUENCE identity is per-table; RESTART IDENTITY reinitializes
721 /// the associated sequence to its starting value. CASCADE
722 /// currently walks direct FK-referring tables and truncates
723 /// them too (PG's semantics). The ONLY modifier (skip partitions)
724 /// and RESTRICT (default) are accepted with no effect since
725 /// SPG's declarative partitions are always truncated together.
726 Truncate {
727 tables: Vec<String>,
728 restart_identity: bool,
729 cascade: bool,
730 /// v7.39 (round 647) — `TRUNCATE ONLY t`. Absorbed as a no-op
731 /// since v7.14 on the reasoning that SPG's children are separate
732 /// relations a truncate does not descend into. Same reasoning
733 /// round 621 applied to `FROM ONLY`, and it stopped being true
734 /// for the same reason: measured, `TRUNCATE <inheritance parent>`
735 /// leaves the children's rows where PG empties them, and
736 /// `TRUNCATE ONLY <partitioned parent>` is silently accepted
737 /// where PG refuses it outright.
738 only: bool,
739 },
740 /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
741 /// BTree-cold indices and merges small cold-tier segments
742 /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
743 /// 4 MiB) into a single larger segment per (table, index).
744 /// `WHERE` predicate filtering on which tables to compact is
745 /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
746 /// v6.7.3 only supports the bare form.
747 CompactColdSegments,
748 /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
749 /// parameter on the engine; v7.12.1 honours
750 /// `default_text_search_config` (consumed by `to_tsvector` /
751 /// `plainto_tsquery` family when called without an explicit
752 /// config arg). All other names are accepted as a no-op so PG
753 /// dumps with `SET client_encoding`, `SET search_path` etc.
754 /// load cleanly.
755 SetParameter {
756 name: String,
757 value: SetValue,
758 /// v7.38 (read01 P3.19) — `SET LOCAL` scopes the change to the
759 /// current transaction; the engine saves the prior value and
760 /// restores it at COMMIT / ROLLBACK. Plain `SET` (and `SET
761 /// SESSION`) leave this false and persist for the session.
762 local: bool,
763 },
764 /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
765 /// multi-assignment (mysqldump preamble uses
766 /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
767 /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
768 /// source order. Pairs whose LHS is a MySQL session/user
769 /// variable (`@VAR` / `@@VAR`) are recorded with the raw
770 /// name so the engine can ignore them; pairs whose LHS is
771 /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
772 /// go through the regular `set_session_param` path.
773 SetParameterList(Vec<(String, SetValue)>),
774 /// v7.39 (round 430) — MySQL's USER-defined variables:
775 /// `SET @x = 5, @s := CONCAT('a','b')`. Distinct from
776 /// [`Self::SetParameter`] (a `@@`-style engine/session setting) in
777 /// every way that matters: the value is an arbitrary EXPRESSION, the
778 /// name lives in its own per-session namespace, and reading an unset
779 /// one answers NULL rather than raising. `:=` and `=` are the same
780 /// assignment here.
781 ///
782 /// Before this the parser stripped every `@`, so `@x` and `@@x` were
783 /// the same node: `SET @x = 5` silently landed in the session-parameter
784 /// store where nothing could read it back, and `SELECT @x` failed with
785 /// "Unknown system variable".
786 /// v7.39 (round 554) — `SET @a = …, SETTING = …`.
787 ///
788 /// `settings` is the trailing half a mysqldump preamble writes:
789 /// `SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'`
790 /// saves a value and changes it in one statement. The parser used
791 /// to refuse the mixture outright, so no mysqldump could be
792 /// restored past its preamble.
793 SetUserVars(Vec<(String, Expr)>, Vec<(String, Expr)>),
794 /// v7.38 轴 4 — `SET [SESSION] TRANSACTION ISOLATION LEVEL …`
795 /// (plus optional READ ONLY / READ WRITE / DEFERRABLE clauses
796 /// silently accepted). PG-standard surface for picking an
797 /// isolation level. Engine tracks the value on
798 /// `Engine::current_isolation_level()`; actual MVCC / SSI
799 /// semantics implementation lands separately. PG itself maps
800 /// READ UNCOMMITTED to READ COMMITTED; SPG mirrors that —
801 /// effectively every level reads as READ COMMITTED in v7.37.8.
802 SetTransaction {
803 modes: TransactionModes,
804 },
805 /// v7.38 轴 4 — `SHOW <param>` returns a 1-column 1-row result
806 /// with the parameter's current value as TEXT. Today the only
807 /// recognised param is `transaction_isolation`; further
808 /// surfaces (`search_path`, `application_name`, …) land as the
809 /// session-parameter inventory grows.
810 ShowParameter(String),
811 /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
812 /// to its default. No-op for parameters SPG does not track.
813 ResetParameter(Option<String>),
814 /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
815 /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
816 /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
817 /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
818 /// languages parse but error at exec time with a clear
819 /// unsupported message.
820 CreateFunction(CreateFunctionStatement),
821 /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
822 /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
823 /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
824 /// triggers and column-list / WHEN clauses are out of scope
825 /// for v7.12.4.
826 CreateTrigger(CreateTriggerStatement),
827 /// v7.39 (round 139) — `CREATE RULE name AS ON event TO table [WHERE cond]
828 /// DO [ALSO|INSTEAD] { NOTHING | command }` query-rewrite rule.
829 CreateRule(CreateRuleStatement),
830 /// v7.39 (round 139) — `DROP RULE [IF EXISTS] name ON table`.
831 DropRule {
832 name: String,
833 table: String,
834 if_exists: bool,
835 },
836 /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
837 /// no-op when missing if `IF EXISTS` is set.
838 DropTrigger {
839 name: String,
840 table: String,
841 if_exists: bool,
842 },
843 /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
844 /// DROP TRIGGER but global (no table scope).
845 DropFunction {
846 name: String,
847 /// v7.39 (read01 round 62) — the argument TYPES, when the statement gave
848 /// them: `DROP FUNCTION f(int)` drops that overload only. `None` = no
849 /// argument list, which PG accepts only when the name is unambiguous.
850 args: Option<Vec<String>>,
851 if_exists: bool,
852 },
853 /// v7.17.0 — `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] name
854 /// [AS data_type]
855 /// [INCREMENT [BY] n]
856 /// [MINVALUE n | NO MINVALUE]
857 /// [MAXVALUE n | NO MAXVALUE]
858 /// [START [WITH] n]
859 /// [CACHE n]
860 /// [[NO] CYCLE]
861 /// [OWNED BY {table.col | NONE}]`.
862 /// Closes the round-7+ silent-no-op SEQUENCE story so pg_dump
863 /// emits + nextval/currval/setval downstream all work.
864 CreateSequence(CreateSequenceStatement),
865 /// v7.17.0 — `ALTER SEQUENCE [IF EXISTS] name <options>` with
866 /// the same option grammar as CREATE SEQUENCE, plus
867 /// `RESTART [WITH n]` and `OWNED BY ...` re-attach.
868 AlterSequence(AlterSequenceStatement),
869 /// v7.17.0 — `DROP SEQUENCE [IF EXISTS] name [, name…]
870 /// [CASCADE | RESTRICT]`. CASCADE / RESTRICT trailers parsed
871 /// silently (no FK on sequences).
872 DropSequence {
873 names: Vec<String>,
874 if_exists: bool,
875 },
876 /// v7.17.0 Phase 1.2 — `CREATE [OR REPLACE] [TEMPORARY] VIEW
877 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …>`. Closes the
878 /// silent-no-op VIEW story from the v7.17 customer-readiness
879 /// audit: pre-v7.17 SPG parsed CREATE VIEW as Statement::Empty
880 /// so any downstream `SELECT FROM v` errored with table-not-
881 /// found. The view body is stored verbatim; SELECT FROM <v>
882 /// rewrites at exec-time by prepending the view body as a
883 /// synthetic CTE.
884 CreateView(CreateViewStatement),
885 /// v7.17.0 Phase 1.2 — `DROP VIEW [IF EXISTS] name [, name…]
886 /// [CASCADE | RESTRICT]`. Removes the matching view from the
887 /// catalog; CASCADE/RESTRICT parsed silently.
888 DropView {
889 names: Vec<String>,
890 if_exists: bool,
891 },
892 /// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW [IF NOT
893 /// EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
894 /// Closes the silent-no-op MATERIALIZED VIEW story. Storage
895 /// model: the materialised result lives as a regular table
896 /// with the matching name + a parallel
897 /// `materialized_views` registry mapping name → body source
898 /// (used by REFRESH).
899 CreateMaterializedView(CreateMaterializedViewStatement),
900 /// v7.17.0 Phase 1.3 — `REFRESH MATERIALIZED VIEW name [WITH
901 /// [NO] DATA]`. Re-runs the stored body and replaces the
902 /// cached rows. `WITH NO DATA` truncates without re-running.
903 RefreshMaterializedView {
904 name: String,
905 with_data: bool,
906 },
907 /// v7.17.0 Phase 1.3 — `DROP MATERIALIZED VIEW [IF EXISTS]
908 /// name [, name…] [CASCADE | RESTRICT]`. Drops both the
909 /// backing table and the source registry entry.
910 DropMaterializedView {
911 names: Vec<String>,
912 if_exists: bool,
913 },
914 /// v7.17.0 Phase 1.4 — `CREATE TYPE name AS ENUM ('a', 'b',
915 /// …)`. Closes the silent-no-op CREATE TYPE story so PG
916 /// dumps that declare enum types load with real constraints
917 /// instead of becoming free-form TEXT. Future kinds
918 /// (composite / range / domain) extend the inner `kind`
919 /// enum.
920 CreateType(CreateTypeStatement),
921 /// v7.37 D.55 — `ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
922 /// [{BEFORE | AFTER} 'existing']`. Extends an enum's label list so
923 /// enum evolution stops being a silent no-op. `position` is
924 /// `Some((is_before, anchor))`.
925 /// v7.39 (read01 round 49) — `ALTER TYPE t RENAME VALUE 'old' TO 'new'`.
926 /// Used to be swallowed by the ALTER TYPE no-op tail, so the rename was
927 /// accepted and silently ignored.
928 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
929 /// Used to be swallowed as dump noise, so a comment was accepted and lost
930 /// (and obj_description / col_description always returned NULL).
931 /// `kind` is lowercase ("table" / "column" / "index" / …); for a column
932 /// `name` is the dotted `table.column`. `comment: None` = `IS NULL` = remove.
933 CommentOn {
934 kind: String,
935 name: String,
936 comment: Option<String>,
937 },
938 AlterTypeRenameValue {
939 type_name: String,
940 old: String,
941 new: String,
942 },
943 AlterTypeAddValue {
944 type_name: String,
945 label: String,
946 if_not_exists: bool,
947 position: Option<(bool, String)>,
948 },
949 /// v7.17.0 Phase 1.4 — `DROP TYPE [IF EXISTS] name [, name…]
950 /// [CASCADE | RESTRICT]`. Removes the matching enum/domain
951 /// from the catalog.
952 DropType {
953 names: Vec<String>,
954 if_exists: bool,
955 },
956 /// v7.17.0 Phase 1.5 — `CREATE DOMAIN name AS base_type
957 /// [DEFAULT expr] [NOT NULL | NULL] [CHECK (expr)]*`.
958 /// A DOMAIN is a named CHECK-constrained alias over a built-
959 /// in type. The CHECK + NOT NULL + DEFAULT clauses apply to
960 /// every column declared with the domain. Closes the
961 /// silent-no-op CREATE DOMAIN story so PG dumps that ship
962 /// validated identifier types (email, positive_int, …) keep
963 /// their guarantees.
964 CreateDomain(CreateDomainStatement),
965 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`. Every form was
966 /// previously swallowed by the catch-all DDL arm: the statement
967 /// reported success and did nothing, so a migration that dropped a
968 /// constraint kept rejecting the data it had just been told to
969 /// accept.
970 AlterDomain {
971 name: String,
972 action: AlterDomainAction,
973 },
974 /// v7.17.0 Phase 1.5 — `DROP DOMAIN [IF EXISTS] name
975 /// [, name…] [CASCADE | RESTRICT]`. Removes the matching
976 /// domain from the catalog.
977 DropDomain {
978 names: Vec<String>,
979 if_exists: bool,
980 },
981 /// v7.17.0 Phase 1.6 — `CREATE SCHEMA [IF NOT EXISTS]
982 /// name [AUTHORIZATION user]`. SPG is single-database;
983 /// schemas are tracked as a namespace registry so pg_dump
984 /// multi-schema declarations land cleanly and `SELECT *
985 /// FROM information_schema.schemata` returns real entries.
986 /// Schema-qualified `schema.table` references still strip
987 /// the prefix at lookup time per PG (schemas are not
988 /// isolation boundaries in v7.17 — see project-next-docket
989 /// for the v7.18+ isolation tracking).
990 CreateSchema {
991 name: String,
992 if_not_exists: bool,
993 },
994 /// v7.17.0 Phase 1.6 — `DROP SCHEMA [IF EXISTS] name
995 /// [, name…] [CASCADE | RESTRICT]`. Removes the schema
996 /// from the registry; built-in `public` / `pg_catalog` /
997 /// `information_schema` cannot be dropped.
998 DropSchema {
999 names: Vec<String>,
1000 if_exists: bool,
1001 },
1002}
1003
1004/// v7.39 (round 260) — the `ALTER DOMAIN` actions SPG implements.
1005#[derive(Debug, Clone, PartialEq)]
1006pub enum AlterDomainAction {
1007 AddConstraint { name: Option<String>, check: Expr },
1008 DropConstraint { name: String, if_exists: bool },
1009 SetDefault(Expr),
1010 DropDefault,
1011 SetNotNull,
1012 DropNotNull,
1013 RenameTo(String),
1014}
1015
1016/// v7.17.0 Phase 1.5 — `CREATE DOMAIN` AST.
1017#[derive(Debug, Clone, PartialEq)]
1018pub struct CreateDomainStatement {
1019 pub name: String,
1020 /// Base type for the domain (one of the built-in
1021 /// `ColumnTypeName` variants).
1022 pub base_type: ColumnTypeName,
1023 /// v7.39 (round 259) — `CREATE DOMAIN child AS parent …` where
1024 /// `parent` is itself a DOMAIN. The parser already captured the
1025 /// unknown type name; it just was not carried here, so the parent's
1026 /// CHECK constraints were invisible and a value violating them was
1027 /// silently accepted. `base_type` still holds the ultimate scalar
1028 /// type, which is what the storage tier stores.
1029 pub base_domain: Option<String>,
1030 /// Optional `DEFAULT <expr>`. Resolved at engine-side
1031 /// CREATE TABLE time when a column is bound to this domain.
1032 pub default: Option<Expr>,
1033 /// `NOT NULL` from the domain definition. Engine ORs this
1034 /// with the column-level nullability so the strictest of the
1035 /// two wins (i.e. the column is non-nullable if either side
1036 /// says so).
1037 pub not_null: bool,
1038 /// Zero-or-more `CHECK (expr)` predicates. Each one is
1039 /// enforced as part of the column's CHECK list at INSERT /
1040 /// UPDATE time, with `VALUE` substituted for the column's
1041 /// current cell value.
1042 pub checks: Vec<Expr>,
1043}
1044
1045/// v7.17.0 Phase 1.4 — `CREATE TYPE` AST.
1046#[derive(Debug, Clone, PartialEq, Eq)]
1047pub struct CreateTypeStatement {
1048 pub name: String,
1049 pub kind: TypeKind,
1050}
1051
1052/// v7.17.0 Phase 1.4 — flavour of the new type. Only ENUM is
1053/// implemented; the variant set is open so Phase 1.5 (DOMAIN)
1054/// and later (COMPOSITE, RANGE) can land without an AST shape
1055/// migration.
1056///
1057/// v7.37.x (ζ-B Phase 1 composite accept) — added Composite for
1058/// `CREATE TYPE name AS (field_name field_type, …)`. Phase 1
1059/// stores the field list in the catalog so PG dumps that emit
1060/// `CREATE TYPE … AS (…)` don't error out; using a composite type
1061/// as a column type lands in Phase 2 (Value::Composite encoding +
1062/// ROW() literal + field-access syntax).
1063#[derive(Debug, Clone, PartialEq, Eq)]
1064pub enum TypeKind {
1065 /// `AS ENUM ('a', 'b', …)`. Order is preserved (PG enum
1066 /// labels are ordered).
1067 Enum { labels: Vec<String> },
1068 /// `AS (field_name field_type, …)`. Order matters; PG
1069 /// composite literals are positional.
1070 Composite {
1071 fields: Vec<(String, ColumnTypeName)>,
1072 /// v7.39 (round 264) — parallel to `fields`: the raw type NAME
1073 /// when a field's type is not a builtin (i.e. another composite).
1074 /// The parser already captures it; without carrying it here a
1075 /// nested composite field resolved to the Text placeholder and
1076 /// the inner record never became a record.
1077 field_user_types: Vec<Option<String>>,
1078 },
1079}
1080
1081/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
1082/// a string literal, an identifier (often a config name), an
1083/// integer/float, or the bare `DEFAULT` keyword.
1084#[derive(Debug, Clone, PartialEq)]
1085pub enum SetValue {
1086 String(String),
1087 Ident(String),
1088 Number(String),
1089 Default,
1090 /// v7.40.11 — MySQL only. `SET character_set_results = NULL` is the
1091 /// second statement Connector/J sends on every connection, and it
1092 /// means "send results in the column's own charset, do not
1093 /// transcode". PostgreSQL 18.6 answers `syntax error at or near
1094 /// "NULL"` for the same shape (measured), so the parser produces
1095 /// this variant only for a MySQL-dialect session.
1096 Null,
1097}
1098
1099/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1100/// at parse time and tracks the selected value on the engine. The
1101/// actual semantic differentiation (REPEATABLE READ snapshot,
1102/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1103/// today every level reads as effective READ COMMITTED (which is
1104/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1105/// READ COMMITTED). Default = `ReadCommitted`.
1106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1107pub enum IsolationLevel {
1108 ReadUncommitted,
1109 #[default]
1110 ReadCommitted,
1111 RepeatableRead,
1112 Serializable,
1113}
1114
1115impl IsolationLevel {
1116 /// Canonical PG-style display name, as `SHOW transaction_isolation`
1117 /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1118 /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1119 /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1120 /// `read uncommitted`) and only BEHAVES as read committed; the old
1121 /// fold renamed the label too.
1122 /// v7.39 — the MySQL display name, which is NOT the PG one: MySQL
1123 /// hyphenates and upper-cases. Measured on MySQL 9.7.2 by setting
1124 /// each level and reading `@@transaction_isolation` back:
1125 /// `READ-UNCOMMITTED` / `READ-COMMITTED` / `REPEATABLE-READ` /
1126 /// `SERIALIZABLE` (the last has no hyphen because it is one word).
1127 ///
1128 /// This exists so the two MySQL surfaces cannot drift: both
1129 /// `SHOW VARIABLES` and `@@transaction_isolation` used to carry
1130 /// their own hard-coded literal, and the literals disagreed —
1131 /// one said `REPEATABLE-READ` while the engine ran read committed.
1132 /// v7.39 — parse what `default_transaction_isolation` holds. PG
1133 /// accepts the SQL spellings and stores them lower-cased with a
1134 /// space; anything else is not a level this understands and the
1135 /// caller keeps its own default rather than guessing.
1136 #[must_use]
1137 pub fn from_pg_name(name: &str) -> Option<Self> {
1138 match name.trim().to_ascii_lowercase().as_str() {
1139 "read uncommitted" => Some(Self::ReadUncommitted),
1140 "read committed" => Some(Self::ReadCommitted),
1141 "repeatable read" => Some(Self::RepeatableRead),
1142 "serializable" => Some(Self::Serializable),
1143 _ => None,
1144 }
1145 }
1146
1147 #[must_use]
1148 pub fn as_mysql_str(self) -> &'static str {
1149 match self {
1150 Self::ReadUncommitted => "READ-UNCOMMITTED",
1151 Self::ReadCommitted => "READ-COMMITTED",
1152 Self::RepeatableRead => "REPEATABLE-READ",
1153 Self::Serializable => "SERIALIZABLE",
1154 }
1155 }
1156
1157 pub fn as_pg_str(self) -> &'static str {
1158 match self {
1159 Self::ReadUncommitted => "read uncommitted",
1160 Self::ReadCommitted => "read committed",
1161 Self::RepeatableRead => "repeatable read",
1162 Self::Serializable => "serializable",
1163 }
1164 }
1165}
1166
1167impl core::fmt::Display for IsolationLevel {
1168 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1169 f.write_str(self.as_pg_str())
1170 }
1171}
1172
1173/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1174/// single fixed-shape DDL; the WITH-clause options PG supports
1175/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1176/// scope for v6.1.4 — `enabled` defaults to true and there are
1177/// no other knobs to set in v6.1.x.
1178#[derive(Debug, Clone, PartialEq, Eq)]
1179pub struct CreateSubscriptionStatement {
1180 pub name: String,
1181 /// Connection string in PG keyword=value form (e.g.
1182 /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1183 /// `host` and `port` fields; the rest is reserved for
1184 /// future v6.1.x options.
1185 pub conn_str: String,
1186 /// One or more publications on the remote side. Order is
1187 /// preserved verbatim from the DDL; the worker requests them
1188 /// in this order. v6.1.4 records the list; v6.1.5
1189 /// publisher-side filtering enforces it.
1190 pub publications: Vec<String>,
1191}
1192
1193/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1194#[derive(Debug, Clone, PartialEq, Eq)]
1195pub struct CreateSequenceStatement {
1196 pub name: String,
1197 pub if_not_exists: bool,
1198 pub temporary: bool,
1199 /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1200 pub data_type: Option<SequenceDataType>,
1201 pub options: SequenceOptions,
1202}
1203
1204/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1206pub enum SequenceDataType {
1207 SmallInt,
1208 Int,
1209 BigInt,
1210}
1211
1212/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1213/// All fields are optional. `min_value`/`max_value` carry
1214/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1215#[derive(Debug, Clone, Default, PartialEq, Eq)]
1216pub struct SequenceOptions {
1217 pub increment: Option<i64>,
1218 pub min_value: Option<SeqBound>,
1219 pub max_value: Option<SeqBound>,
1220 pub start: Option<i64>,
1221 /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1222 /// RESTART, `Some(Some(n))` = RESTART WITH n.
1223 pub restart: Option<Option<i64>>,
1224 pub cache: Option<i64>,
1225 pub cycle: Option<bool>,
1226 pub owned_by: Option<SequenceOwnedBy>,
1227}
1228
1229/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1231pub enum SeqBound {
1232 Value(i64),
1233 NoBound,
1234}
1235
1236/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1237#[derive(Debug, Clone, PartialEq, Eq)]
1238pub enum SequenceOwnedBy {
1239 None,
1240 Column { table: String, column: String },
1241}
1242
1243/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1244#[derive(Debug, Clone, PartialEq)]
1245pub struct CreateMaterializedViewStatement {
1246 pub name: String,
1247 pub if_not_exists: bool,
1248 /// Optional `(col, col, …)` rename list. Applies to the
1249 /// backing table at CREATE / REFRESH time.
1250 pub columns: Vec<String>,
1251 /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1252 /// the cached rows.
1253 pub body: SelectStatement,
1254 /// `WITH DATA` (default) = materialise the rows at CREATE
1255 /// time. `WITH NO DATA` = create an empty backing table;
1256 /// callers must REFRESH before SELECT returns rows.
1257 pub with_data: bool,
1258 /// v7.38 (read01 P6.49) — when true this node came from
1259 /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1260 /// executor creates a plain table and does NOT register it in the
1261 /// materialized-view registry (no REFRESH semantics).
1262 pub as_plain_table: bool,
1263 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1264 /// meaningful together with `as_plain_table`; the executor puts the
1265 /// resulting table in the creating session's namespace.
1266 pub temporary: bool,
1267}
1268
1269/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1270/// auto-updatable view. `Cascaded` is PG's default when the bare
1271/// `WITH CHECK OPTION` is written.
1272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1273pub enum ViewCheckOption {
1274 Local,
1275 Cascaded,
1276}
1277
1278/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1279#[derive(Debug, Clone, PartialEq)]
1280pub struct CreateViewStatement {
1281 pub name: String,
1282 pub or_replace: bool,
1283 pub if_not_exists: bool,
1284 pub temporary: bool,
1285 /// Optional `(col, col, …)` rename list. When non-empty,
1286 /// these override the body's projected column names per-
1287 /// position at SELECT-from-view time.
1288 pub columns: Vec<String>,
1289 /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1290 /// time to materialise the view as a synthetic CTE.
1291 pub body: SelectStatement,
1292 /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1293 /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1294 /// 44000). `None` = no check option.
1295 pub check_option: Option<ViewCheckOption>,
1296}
1297
1298/// v7.17.0 — `ALTER SEQUENCE` AST node.
1299#[derive(Debug, Clone, PartialEq, Eq)]
1300pub struct AlterSequenceStatement {
1301 pub name: String,
1302 pub if_exists: bool,
1303 pub options: SequenceOptions,
1304 /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1305 /// instead of `options`; the two forms are mutually exclusive in PG.
1306 pub rename_to: Option<String>,
1307}
1308
1309/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1310/// the [`PublicationScope`] shape. v6.1.2 only accepted
1311/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1312/// variants by flipping the parser gate (no AST migration).
1313#[derive(Debug, Clone, PartialEq, Eq)]
1314pub struct CreatePublicationStatement {
1315 pub name: String,
1316 pub scope: PublicationScope,
1317}
1318
1319/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1320/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1321/// variants — the on-disk shape, snapshot serialisation, and the
1322/// AST round-trip Display path were already in place in v6.1.2
1323/// so this is a parser-only widening.
1324#[derive(Debug, Clone, PartialEq, Eq)]
1325pub enum PublicationScope {
1326 AllTables,
1327 ForTables(Vec<String>),
1328 AllTablesExcept(Vec<String>),
1329 /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1330 /// (PG 15+). AST-only: the executor folds `public` to
1331 /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1332 /// and refuses any other schema with PG's sentence, so the
1333 /// catalog / serializer / replication filter never see it.
1334 TablesInSchema(String),
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Eq)]
1338pub struct AlterIndexStatement {
1339 pub name: String,
1340 pub target: AlterIndexTarget,
1341}
1342
1343#[derive(Debug, Clone, PartialEq, Eq)]
1344#[non_exhaustive]
1345pub enum AlterIndexTarget {
1346 /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1347 /// rebuilds the existing graph in place without touching the
1348 /// column encoding; `Some(enc)` re-encodes every cell first.
1349 Rebuild { encoding: Option<VecEncoding> },
1350 /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1351 /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1352 /// uses it to make the migration idempotent (re-running on a
1353 /// DB where the rename already happened is a no-op rather
1354 /// than an error).
1355 Rename { new: String, if_exists: bool },
1356 /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1357 /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1358 /// does not exist`), so the index is validated and the storage
1359 /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1360 /// SET/RESET arms already record).
1361 StorageParams,
1362}
1363
1364/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1365/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1366/// can add more SET subjects without changing the dispatch shape.
1367#[derive(Debug, Clone, PartialEq)]
1368pub struct AlterTableStatement {
1369 pub name: String,
1370 /// v7.13.2 — mailrs round-6 S1. One or more subactions
1371 /// separated by commas in the source SQL. PG-semantic apply
1372 /// is sequential; engine bails on first error (no
1373 /// transactional rollback of completed subactions in v7.13).
1374 /// Single-subaction shape stays a 1-element vec.
1375 pub targets: Vec<AlterTableTarget>,
1376}
1377/// v7.39.9 — the `FIRST` / `AFTER c` trailer, written back the way it
1378/// was read.
1379fn write_column_position(
1380 f: &mut core::fmt::Formatter<'_>,
1381 pos: Option<&ColumnPosition>,
1382) -> core::fmt::Result {
1383 match pos {
1384 Some(ColumnPosition::First) => f.write_str(" FIRST"),
1385 Some(ColumnPosition::After(c)) => write!(f, " AFTER {}", quote_ident(c)),
1386 None => Ok(()),
1387 }
1388}
1389
1390/// v7.39.9 — where MySQL's `ADD` / `MODIFY` / `CHANGE` puts a column.
1391///
1392/// The row encoding is positional and `SELECT *` reads it in order, so
1393/// this is an answer, not a formatting preference.
1394#[derive(Debug, Clone, PartialEq, Eq)]
1395pub enum ColumnPosition {
1396 First,
1397 After(String),
1398}
1399
1400#[derive(Debug, Clone, PartialEq)]
1401#[allow(clippy::large_enum_variant)]
1402pub enum AlterTableTarget {
1403 /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1404 ///
1405 /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1406 /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1407 /// the reasoning went stale: `NO INHERIT` reported success while the
1408 /// child stayed attached, which is the worst kind of answer — the
1409 /// statement says it worked and the catalog disagrees.
1410 Inherit { parent: String, detach: bool },
1411 /// Per-table hot-tier byte budget override. The freezer
1412 /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1413 SetHotTierBytes(u64),
1414 /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1415 /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1416 /// Engine validates existing rows against the new constraint
1417 /// before installing it.
1418 AddForeignKey(ForeignKeyConstraint),
1419 /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1420 /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1421 /// no-op when no FK with that name exists; otherwise raises.
1422 DropForeignKey { name: String, if_exists: bool },
1423 /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1424 /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1425 /// as the standalone `DROP INDEX` statement.
1426 DropIndex { name: String, if_exists: bool },
1427 /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1428 /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1429 /// (20 migrate-*.sql hits). Engine appends the column to the
1430 /// schema and back-fills every existing row with the DEFAULT
1431 /// (or NULL when no DEFAULT and the column is nullable).
1432 AddColumn {
1433 column: ColumnDef,
1434 if_not_exists: bool,
1435 /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>`, which say where
1436 /// the column goes. `None` is the PostgreSQL form and appends.
1437 position: Option<ColumnPosition>,
1438 },
1439 /// v7.39.9 — MySQL's `MODIFY COLUMN c <definition>` and
1440 /// `CHANGE COLUMN old new <definition>`.
1441 ///
1442 /// Both REPLACE the column's definition rather than amending it,
1443 /// which is the part that cannot be expressed by the PostgreSQL
1444 /// spellings SPG already had. Measured on MySQL 9.7.2: a column
1445 /// declared `INT NOT NULL DEFAULT 5`, after `MODIFY COLUMN b
1446 /// BIGINT`, is `bigint` NULLABLE with NO default — restating them
1447 /// keeps them, omitting them drops them. `CHANGE` is the same and
1448 /// also renames.
1449 ModifyColumn {
1450 /// The column as it is named now.
1451 column: String,
1452 /// `CHANGE`'s new name; `None` for `MODIFY`, which keeps it.
1453 rename_to: Option<String>,
1454 /// The whole new definition, exactly as written.
1455 definition: ColumnDef,
1456 position: Option<ColumnPosition>,
1457 },
1458 /// v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
1459 RenameIndex { old: String, new: String },
1460 /// v7.39.9 — MySQL's `ALTER TABLE t AUTO_INCREMENT = n`, which sets
1461 /// the value the NEXT insert takes. Measured on 9.7.2: after
1462 /// `= 100`, the next row's id is 100.
1463 SetTableAutoIncrement(i64),
1464 /// v7.39.9 — MySQL's `ENGINE = <name>`. SPG has one storage engine
1465 /// and substitutes for every name MySQL knows, exactly as
1466 /// `CREATE TABLE` already does; a name MySQL does not know is
1467 /// refused with its 1286, because a typo in a migration must not
1468 /// quietly become SPG's storage.
1469 SetEngine(String),
1470 /// v7.39.9 — MySQL's `CONVERT TO CHARACTER SET <cs> [COLLATE <c>]`.
1471 /// SPG stores UTF-8 throughout, so a charset it can represent is
1472 /// accepted and one it cannot is refused with MySQL's 1115.
1473 ConvertToCharacterSet {
1474 charset: String,
1475 collate: Option<String>,
1476 },
1477 /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1478 /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1479 /// existing row's column value by evaluating the optional
1480 /// USING expression (default `col::<ty>`) and re-coercing
1481 /// against the new column type.
1482 AlterColumnType {
1483 column: String,
1484 new_type: ColumnTypeName,
1485 using: Option<Expr>,
1486 /// v7.39 (round 713) — `COLLATE <name>` between the type and
1487 /// USING. PG re-collates the column, and an ABSENT clause RESETS
1488 /// the collation to the type default (measured round 713) — so
1489 /// `None` is not "leave it alone". The type parser consumed the
1490 /// clause all along and this surface dropped it on the floor:
1491 /// the statement succeeded and the ordering did not change, the
1492 /// silent-divergence shape. Folded variant + the name as written.
1493 collation: Option<(Collation, String)>,
1494 },
1495 /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1496 /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1497 /// every row's value at that position is removed; any index
1498 /// on the column is dropped. `if_exists` makes the drop a
1499 /// no-op when the column is missing. `cascade` removes
1500 /// dependents (FKs referencing the column, partial indexes
1501 /// whose predicate names the column); without it, the engine
1502 /// rejects when dependents exist.
1503 DropColumn {
1504 column: String,
1505 if_exists: bool,
1506 cascade: bool,
1507 },
1508 /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1509 /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1510 /// CONSTRAINT name CHECK (expr)` — table-level constraints
1511 /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1512 /// separate ALTER TABLE statement, so this surface lets the
1513 /// dump load straight through.
1514 AddTableConstraint(TableConstraint),
1515 /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1516 /// there is nothing to record; what PG does that SPG did not is
1517 /// REFUSE a role that does not exist. The name has to reach the
1518 /// engine for that, because only the engine knows the roles.
1519 OwnerTo { role: String },
1520 /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1521 /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1522 /// the hint is still a no-op; naming an index that does not exist is
1523 /// not.
1524 ClusterOn { index: Option<String> },
1525 /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1526 /// already in the table against a constraint added `NOT VALID` and,
1527 /// if they all pass, mark it validated. It used to be swallowed as a
1528 /// no-op on the theory that SPG validated at ADD time; SPG did not.
1529 ValidateConstraint { name: String },
1530 /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1531 /// Renames the column in the schema and propagates the rename
1532 /// to every stored source string that references it as a
1533 /// (potentially-qualified) column identifier: CHECK predicates,
1534 /// partial-index predicates, runtime DEFAULT expressions, and
1535 /// triggers' `UPDATE OF` column lists. Function bodies and
1536 /// trigger bodies are NOT auto-rewritten — they're loose
1537 /// source text and may contain references SPG can't statically
1538 /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1539 /// the column even if dependents exist; users renaming a
1540 /// column referenced by a function body update the function
1541 /// body separately.
1542 RenameColumn { old: String, new: String },
1543 /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1544 /// Reachable now that the schema stores user-supplied constraint names.
1545 RenameConstraint { old: String, new: String },
1546 /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1547 /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1548 /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1549 /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1550 /// (identity); both lower to this. SPG's auto-increment is
1551 /// max+1-scan based, so the dump's `setval(…)` calls stay
1552 /// no-ops without losing the sequence position.
1553 SetColumnAutoIncrement {
1554 column: String,
1555 /// The implicit sequence pg_dump names for an identity
1556 /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1557 /// nextval target for a serial default. The engine creates
1558 /// it if absent so the dump's later `setval(s, …)` lands.
1559 seq_name: Option<String>,
1560 },
1561 /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1562 /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1563 /// migrate-042 uses it). The engine moves the table entry
1564 /// in the catalog under the new name; child catalog state
1565 /// (FKs pointing at this table, triggers watching this
1566 /// table) tracks the rename through the storage layer.
1567 RenameTable { new: String },
1568 /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1569 /// { ALL | <name> }`. Toggles whether row-level triggers
1570 /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1571 /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1572 /// ENABLE epilogue around every table's data block so the
1573 /// rows already-computed in prod don't get re-rewritten
1574 /// (and so trigger-driven side effects like
1575 /// audit/queueing don't re-fire during a bulk reload).
1576 /// `which == TriggerSelector::All` toggles every trigger
1577 /// on the table; `Named(name)` toggles one trigger. The
1578 /// engine persists the disabled state on `TriggerDef.enabled`
1579 /// (catalog FILE_VERSION 25+) and the row-write paths skip
1580 /// the trigger when `!enabled`.
1581 SetTriggerEnabled {
1582 which: TriggerSelector,
1583 enabled: bool,
1584 },
1585 /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1586 /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1587 /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1588 /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1589 SetRowSecurity {
1590 enabled: Option<bool>,
1591 force: Option<bool>,
1592 },
1593 /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1594 /// <bounds>`. Promotes an existing table `child` to a partition
1595 /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1596 /// Engine validates that `child`'s columns are layout-compatible
1597 /// with `parent` and that every row in `child` satisfies the
1598 /// bound before installing the role.
1599 AttachPartition {
1600 child: String,
1601 bounds: PartitionOfBoundsAst,
1602 },
1603 /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1604 /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1605 /// to a standalone table (clears `partition_role`) and removes
1606 /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1607 /// is parser-accepted; engine performs the same atomic detach
1608 /// (single-engine, no replication lag — the PG semantics that
1609 /// require the two-phase split don't apply).
1610 DetachPartition {
1611 child: String,
1612 concurrently: bool,
1613 finalize: bool,
1614 },
1615 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1616 /// <expr>`. Engine re-parses + freezes the literal at this point,
1617 /// matching CREATE TABLE-side default semantics. Volatile shapes
1618 /// (`now()` / `nextval`) take the runtime-default path.
1619 AlterColumnSetDefault { column: String, default_expr: Expr },
1620 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1621 AlterColumnDropDefault { column: String },
1622 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1623 /// Engine validates that no existing row has NULL in that column
1624 /// before flipping the flag (PG semantics — partial NOT NULL
1625 /// would surface inconsistently).
1626 AlterColumnSetNotNull { column: String },
1627 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1628 AlterColumnDropNotNull { column: String },
1629 /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1630 /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1631 /// column's start value = 1). Engine records a next-value floor over
1632 /// SPG's max+1 identity allocation.
1633 AlterColumnRestart { column: String, with: Option<i64> },
1634 /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1635 /// EXPRESSION` turns a stored generated column into a plain column
1636 /// (its generation expression is removed; existing values are kept).
1637 AlterColumnDropExpression { column: String, if_exists: bool },
1638 /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1639 /// de-generate an identity column into a plain column.
1640 AlterColumnDropIdentity { column: String, if_exists: bool },
1641 /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1642 /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1643 /// expression and recomputes every existing row.
1644 AlterColumnSetExpression { column: String, expr: Expr },
1645 /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1646 /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1647 /// (PG: `type "x" does not exist`).
1648 OfType { type_name: String },
1649 /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1650 /// identity setting no-ops (SPG has no logical replication consumer);
1651 /// the INDEX must exist on this table (PG: `index "i" for table "t"
1652 /// does not exist`).
1653 ReplicaIdentityUsingIndex { index: String },
1654}
1655
1656/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1657/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1658/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1659/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1660/// shouldn't surface from a dump.
1661#[derive(Debug, Clone, PartialEq, Eq)]
1662pub enum TriggerSelector {
1663 /// Every trigger on the table.
1664 All,
1665 /// A specific trigger by name.
1666 Named(String),
1667}
1668
1669/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1670/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1671/// bitflags word or a nested options struct would only relocate the lint
1672/// while making the option each caller sets harder to read.
1673#[allow(clippy::struct_excessive_bools)]
1674#[derive(Debug, Clone, PartialEq)]
1675pub struct ExplainStatement {
1676 pub analyze: bool,
1677 /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1678 /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1679 /// `Insert on / Update on / Delete on` trees for them.
1680 pub inner: Box<Statement>,
1681 /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1682 /// advisor pass: after the regular plan tree, the engine
1683 /// emits one suggestion line per column referenced in the
1684 /// query's WHERE / JOIN that has no covering index on the
1685 /// owning table.
1686 pub suggest: bool,
1687 /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1688 /// `elapsed=…us` annotations from the Total line (and any
1689 /// future cost-bearing lines). PG-standard option used by
1690 /// regression suites and diff-friendly EXPLAIN output. When
1691 /// `true`, takes precedence over the per-session
1692 /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1693 pub costs_off: bool,
1694 /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1695 /// option that surfaces hot/cold/shared block counters. SPG's
1696 /// hot-tier scan path counts examined rows; the BUFFERS option
1697 /// makes that an explicit per-operator annotation.
1698 pub buffers: bool,
1699 /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1700 /// uses this to disable per-operator timing while still
1701 /// emitting actual-row counts (cheaper than ANALYZE). Default
1702 /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1703 /// timing portion of the Total line. Decoupled from `costs_off`:
1704 /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1705 /// measured wall-clock.
1706 pub timing_off: bool,
1707 /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1708 /// modified GUC values to the plan output. SPG emits the
1709 /// session params that diverge from default after the main
1710 /// plan body.
1711 pub settings: bool,
1712 /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1713 /// bytes / records / FPI emitted by the query. SPG's
1714 /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1715 /// ANALYZE) report against the engine WAL counter delta.
1716 pub wal: bool,
1717 /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1718 /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1719 /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1720 /// this is set.
1721 pub summary_off: bool,
1722 /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1723 /// PG's standard format selector. Default is text. JSON / XML
1724 /// / YAML emit a single-row TEXT result whose body wraps the
1725 /// existing line-per-operator text in the chosen container —
1726 /// PG-compatible just enough for dashboards that parse those
1727 /// container shapes (pgAdmin's JSON path picker, etc.).
1728 pub format: ExplainFormat,
1729}
1730
1731#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1732pub enum ExplainFormat {
1733 #[default]
1734 Text,
1735 Json,
1736 Xml,
1737 Yaml,
1738}
1739
1740/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1741#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1742pub enum PolicyCmd {
1743 All,
1744 Select,
1745 Insert,
1746 Update,
1747 Delete,
1748}
1749
1750/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1751/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1752#[derive(Debug, Clone, PartialEq)]
1753pub struct CreatePolicyStatement {
1754 pub name: String,
1755 pub table: String,
1756 /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1757 pub permissive: bool,
1758 pub cmd: PolicyCmd,
1759 /// Empty = PUBLIC.
1760 pub roles: Vec<String>,
1761 pub using: Option<Expr>,
1762 pub with_check: Option<Expr>,
1763}
1764
1765/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1766/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1767/// or the command (matches PG).
1768#[derive(Debug, Clone, PartialEq)]
1769pub struct AlterPolicyStatement {
1770 pub name: String,
1771 pub table: String,
1772 pub rename_to: Option<String>,
1773 pub roles: Option<Vec<String>>,
1774 pub using: Option<Expr>,
1775 pub with_check: Option<Expr>,
1776}
1777
1778/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1779#[derive(Debug, Clone, PartialEq, Eq)]
1780pub struct DropPolicyStatement {
1781 pub name: String,
1782 pub table: String,
1783 pub if_exists: bool,
1784}
1785
1786#[derive(Debug, Clone, PartialEq, Eq)]
1787pub struct CreateUserStatement {
1788 pub name: String,
1789 /// Empty when the statement carried no PASSWORD — legal for a bare
1790 /// `CREATE ROLE`, which cannot log in anyway.
1791 pub password: String,
1792 /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1793 /// the parser; the engine validates against `Role::parse` so a
1794 /// typo lands as a runtime error with a clear message rather than
1795 /// a parse failure.
1796 pub role: String,
1797 /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1798 /// statement did not say, so the default for its spelling applies:
1799 /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1800 /// both default to INHERIT and NOSUPERUSER.
1801 pub login: Option<bool>,
1802 pub inherit: Option<bool>,
1803 pub superuser: Option<bool>,
1804 /// `true` when spelled `CREATE USER` (LOGIN by default).
1805 pub is_user: bool,
1806}
1807
1808/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1809/// it tells the planner how far a call may be moved or folded. SPG records
1810/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1811/// yet exploit it for constant folding.
1812#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1813pub enum FunctionVolatility {
1814 Immutable,
1815 Stable,
1816 #[default]
1817 Volatile,
1818}
1819
1820impl FunctionVolatility {
1821 /// PG's one-character `pg_proc.provolatile` code.
1822 #[must_use]
1823 pub const fn as_pg_char(self) -> &'static str {
1824 match self {
1825 Self::Immutable => "i",
1826 Self::Stable => "s",
1827 Self::Volatile => "v",
1828 }
1829 }
1830
1831 #[must_use]
1832 pub const fn as_sql(self) -> &'static str {
1833 match self {
1834 Self::Immutable => "IMMUTABLE",
1835 Self::Stable => "STABLE",
1836 Self::Volatile => "VOLATILE",
1837 }
1838 }
1839}
1840
1841/// v7.39 (round 322, V46) — PG's parallel-safety class.
1842#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1843pub enum FunctionParallel {
1844 #[default]
1845 Unsafe,
1846 Restricted,
1847 Safe,
1848}
1849
1850impl FunctionParallel {
1851 /// PG's one-character `pg_proc.proparallel` code.
1852 #[must_use]
1853 pub const fn as_pg_char(self) -> &'static str {
1854 match self {
1855 Self::Unsafe => "u",
1856 Self::Restricted => "r",
1857 Self::Safe => "s",
1858 }
1859 }
1860
1861 #[must_use]
1862 pub const fn as_sql(self) -> &'static str {
1863 match self {
1864 Self::Unsafe => "PARALLEL UNSAFE",
1865 Self::Restricted => "PARALLEL RESTRICTED",
1866 Self::Safe => "PARALLEL SAFE",
1867 }
1868 }
1869}
1870
1871/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1872/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1873/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1874/// language's default cost / rows.
1875#[derive(Debug, Clone, Copy, PartialEq, Default)]
1876pub struct FunctionAttrs {
1877 pub volatility: FunctionVolatility,
1878 /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1879 /// argument returns NULL without running the body.
1880 pub strict: bool,
1881 pub security_definer: bool,
1882 pub leakproof: bool,
1883 pub parallel: FunctionParallel,
1884 /// `COST n` — `None` leaves PG's per-language default.
1885 pub cost: Option<f64>,
1886 /// `ROWS n` — set-returning functions only; `None` = default.
1887 pub rows: Option<f64>,
1888}
1889
1890impl FunctionAttrs {
1891 /// The attribute words `pg_get_functiondef` puts on their own line,
1892 /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1893 /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1894 /// at its default — PG then emits no such line at all.
1895 #[must_use]
1896 pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1897 let mut out = alloc::vec::Vec::new();
1898 if self.volatility != FunctionVolatility::Volatile {
1899 out.push(alloc::string::String::from(self.volatility.as_sql()));
1900 }
1901 if self.parallel != FunctionParallel::Unsafe {
1902 out.push(alloc::string::String::from(self.parallel.as_sql()));
1903 }
1904 if self.strict {
1905 out.push(alloc::string::String::from("STRICT"));
1906 }
1907 if self.security_definer {
1908 out.push(alloc::string::String::from("SECURITY DEFINER"));
1909 }
1910 if self.leakproof {
1911 out.push(alloc::string::String::from("LEAKPROOF"));
1912 }
1913 if let Some(c) = self.cost {
1914 out.push(alloc::format!("COST {}", render_attr_number(c)));
1915 }
1916 if let Some(r) = self.rows {
1917 out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1918 }
1919 out
1920 }
1921}
1922
1923/// PG prints a whole-numbered cost / rows without a decimal point.
1924fn render_attr_number(v: f64) -> alloc::string::String {
1925 // no_std: `f64::fract` lives in std, so compare against the truncation.
1926 let whole = v as i64;
1927 if v.abs() < 1e15 && (whole as f64) == v {
1928 alloc::format!("{whole}")
1929 } else {
1930 alloc::format!("{v}")
1931 }
1932}
1933
1934/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1935/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1936/// (the row-level trigger body the CREATE TRIGGER below references).
1937/// Non-trigger user-defined functions parse but error at execution
1938/// time with a clear unsupported message; that surface lands in
1939/// v7.12.5+.
1940#[derive(Debug, Clone, PartialEq)]
1941pub struct CreateFunctionStatement {
1942 pub name: String,
1943 /// `OR REPLACE` was present; an existing function with the
1944 /// same name is overwritten instead of erroring.
1945 pub or_replace: bool,
1946 /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1947 /// list `()` (sufficient for trigger functions). Other shapes
1948 /// parse and store the args but the executor refuses to call
1949 /// them.
1950 pub args: Vec<FunctionArg>,
1951 /// `RETURNS <type>` — `trigger` is the supported shape for
1952 /// v7.12.4; arbitrary return types parse to
1953 /// [`FunctionReturn::Other`].
1954 pub returns: FunctionReturn,
1955 /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1956 /// side of `AS $$...$$`; the parser canonicalises to one slot.
1957 /// `plpgsql` and `sql` are the two interesting values.
1958 pub language: String,
1959 /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1960 /// a structured AST; non-trigger / non-plpgsql bodies stay as
1961 /// the raw source text so the v7.12.5+ executor can pick them
1962 /// up without a parser rev.
1963 pub body: FunctionBody,
1964 /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1965 /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1966 /// on either side of the body; before this they were a parse error, so
1967 /// PG's own `pg_dump` output would not restore.
1968 pub attrs: FunctionAttrs,
1969}
1970
1971/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1972#[derive(Debug, Clone, PartialEq)]
1973pub struct FunctionArg {
1974 /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1975 /// (the default); `OUT` / `INOUT` parse but the executor
1976 /// refuses them.
1977 pub mode: FunctionArgMode,
1978 /// Optional arg name. Trigger functions traditionally don't
1979 /// name their args (they read NEW/OLD instead), so `None` is
1980 /// the common case.
1981 pub name: Option<String>,
1982 /// Declared type, normalised to the SPG `DataType` mapping
1983 /// where one exists. Unknown / extension types parse as a
1984 /// raw string under [`FunctionArgType::Raw`].
1985 pub ty: FunctionArgType,
1986}
1987
1988#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1989pub enum FunctionArgMode {
1990 In,
1991 Out,
1992 InOut,
1993}
1994
1995#[derive(Debug, Clone, PartialEq)]
1996pub enum FunctionArgType {
1997 Typed(ColumnTypeName),
1998 /// Unknown / extension types — kept as the parser-side raw
1999 /// identifier so error messages can name them precisely.
2000 Raw(String),
2001}
2002
2003#[derive(Debug, Clone, PartialEq)]
2004pub enum FunctionReturn {
2005 /// `RETURNS TRIGGER` — the row-level trigger function shape.
2006 /// v7.12.4 ships exactly this for execution.
2007 Trigger,
2008 /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
2009 /// the function is unused (since v7.12.4 doesn't ship scalar
2010 /// function invocation).
2011 Void,
2012 /// `RETURNS <type>` for any concrete data type. Reserved for
2013 /// v7.12.5+'s scalar UDF surface.
2014 Type(ColumnTypeName),
2015 /// `RETURNS <ident>` for types SPG doesn't know — extension
2016 /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
2017 Other(String),
2018}
2019
2020#[derive(Debug, Clone, PartialEq)]
2021pub enum FunctionBody {
2022 /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
2023 /// trigger-function executor walks this directly without
2024 /// re-parsing.
2025 PlPgSql(PlPgSqlBlock),
2026 /// Raw source text — parser couldn't (or didn't try to)
2027 /// structure-parse the body. Used for `LANGUAGE sql`
2028 /// functions and any PL/pgSQL body that contains v7.12.5+
2029 /// features the v7.12.4 parser doesn't yet recognise. The
2030 /// executor returns an unsupported error when invoked.
2031 Raw(String),
2032}
2033
2034/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
2035/// from assignment + return to a real-PL/pgSQL surface:
2036/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
2037/// control flow, `RAISE` diagnostics, and embedded SQL
2038/// statements that execute through the regular engine path.
2039/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
2040/// which mailrs's trigger doesn't need but other PG customers
2041/// may; deferred to a future minor release.
2042#[derive(Debug, Clone, PartialEq)]
2043pub struct PlPgSqlBlock {
2044 /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
2045 /// preceding `BEGIN`. Empty when the body opens directly with
2046 /// `BEGIN`. Declarations execute in order; each may reference
2047 /// earlier-declared locals in its init expression.
2048 pub declarations: Vec<PlPgSqlDeclare>,
2049 pub statements: Vec<PlPgSqlStmt>,
2050 /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
2051 /// <body>` handlers appended to the block. Empty when no
2052 /// EXCEPTION clause is present. When a body statement raises
2053 /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
2054 /// handlers are tried in order; the first matching condition
2055 /// runs its body and the block terminates cleanly. `OTHERS`
2056 /// matches any exception. Unhandled exceptions propagate.
2057 pub exception_handlers: Vec<ExceptionHandler>,
2058}
2059
2060/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
2061/// arm inside an EXCEPTION block.
2062#[derive(Debug, Clone, PartialEq)]
2063pub struct ExceptionHandler {
2064 /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
2065 /// conditions joined by `OR` share one handler body.
2066 pub conditions: Vec<String>,
2067 /// Statements to run when a matching exception is caught.
2068 pub body: Vec<PlPgSqlStmt>,
2069}
2070
2071/// v7.12.6 — single `DECLARE` entry: variable name + declared
2072/// type + optional initialiser. Variables default to SQL NULL
2073/// when no init is given (matches PG).
2074#[derive(Debug, Clone, PartialEq)]
2075pub struct PlPgSqlDeclare {
2076 pub name: String,
2077 /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
2078 /// knows it; raw text otherwise).
2079 pub ty: FunctionArgType,
2080 pub default: Option<Expr>,
2081}
2082
2083#[derive(Debug, Clone, PartialEq)]
2084pub enum PlPgSqlStmt {
2085 /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
2086 /// for clarity in error reporting (PG also forbids it) — the
2087 /// executor errors with a clear "OLD is read-only" message.
2088 Assign { target: AssignTarget, value: Expr },
2089 /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
2090 /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
2091 /// the SELECT statement with the INTO clause stripped; the
2092 /// engine runs it via `Engine::execute`, takes the first
2093 /// row's first column, and assigns to the local variable
2094 /// in the DECLARE scope. Single-column / single-row
2095 /// queries only at v7.16.2; multi-target (`INTO a, b`) is
2096 /// a v7.16.x follow-up.
2097 SelectInto {
2098 var: String,
2099 body: Box<SelectStatement>,
2100 },
2101 /// `RETURN <target>;` — trigger functions canonically return
2102 /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
2103 /// expression for forward compatibility with scalar UDFs.
2104 Return(ReturnTarget),
2105 /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
2106 /// set a SETOF function is building, and KEEP GOING. Not a return.
2107 ReturnNext(Expr),
2108 /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
2109 /// query yields, and keep going. It used to desugar to a side-effect
2110 /// statement whose result was DISCARDED — in a SETOF function that is the
2111 /// whole answer thrown away.
2112 ReturnQuery(Box<SelectStatement>),
2113 /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
2114 /// twin. Its rows go to the set too; it used to run and discard them.
2115 ReturnQueryExecute { sql: Expr },
2116 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
2117 /// [ELSE body] END IF;`. Branches are tried in order; first
2118 /// truthy condition wins; the optional ELSE runs when no
2119 /// condition matched.
2120 If {
2121 branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
2122 else_branch: Vec<PlPgSqlStmt>,
2123 },
2124 /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
2125 /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
2126 /// (logging — observable side effect only) or `EXCEPTION`
2127 /// (aborts the trigger and propagates as an error). v7.12.6
2128 /// supports the basic format-string substitution PG uses
2129 /// (`%` placeholders consumed positionally).
2130 Raise {
2131 level: RaiseLevel,
2132 message: String,
2133 args: Vec<Expr>,
2134 },
2135 /// v7.12.6 — embedded SQL statement inside the trigger body
2136 /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
2137 /// NEW.col / OLD.col references inside the embedded
2138 /// statement's expression tree are substituted with the
2139 /// current trigger context before the engine re-executes the
2140 /// statement. Recursion depth into nested triggers is
2141 /// bounded by the engine's existing trigger-fire guard.
2142 EmbeddedSql(Box<Statement>),
2143 /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
2144 /// the condition evaluates falsy the trigger / DO block aborts
2145 /// with the message (defaulting to a generic shape when none
2146 /// is provided). Same propagation shape as `RAISE EXCEPTION`
2147 /// — the error reaches the caller's query path. PG's behaviour
2148 /// is identical except for a `plpgsql.check_asserts` GUC that
2149 /// can disable the check globally; SPG always evaluates.
2150 Assert {
2151 condition: Expr,
2152 message: Option<Expr>,
2153 },
2154 /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2155 /// Iterate the body while condition evaluates truthy. Iteration
2156 /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2157 /// loops; the executor errors out when reached. EXIT / CONTINUE
2158 /// inside the body queue with 20.2.
2159 While {
2160 condition: Expr,
2161 body: Vec<PlPgSqlStmt>,
2162 },
2163 /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2164 /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2165 /// bounds inclusive on both sides. REVERSE walks backward.
2166 /// Iteration budget guards runaway.
2167 ForRange {
2168 var: String,
2169 start: Expr,
2170 end: Expr,
2171 reverse: bool,
2172 body: Vec<PlPgSqlStmt>,
2173 },
2174 /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2175 /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2176 /// budget guards runaway.
2177 Loop { body: Vec<PlPgSqlStmt> },
2178 /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2179 /// Unconditional (no WHEN) or conditional (only breaks when
2180 /// condition is truthy). Bubbles up as BodyOutcome::Break which
2181 /// the enclosing loop catches. Outside a loop it's a no-op.
2182 Exit { when: Option<Expr> },
2183 /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2184 /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2185 /// which the enclosing loop catches, skipping the remainder of
2186 /// the body and jumping to the next iteration.
2187 Continue { when: Option<Expr> },
2188 /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2189 /// computed SQL statement. The expression is evaluated to a
2190 /// text value, the resulting string is parsed and dispatched
2191 /// through the engine like an EmbeddedSql. USING <param_list>
2192 /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2193 ExecuteDynamic { sql: Expr },
2194 /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2195 /// END LOOP;`. Runs the SELECT once, iterates the resulting
2196 /// rows, binds the first column of each row to `var` as a
2197 /// scalar Value, then runs the body per iteration. EXIT /
2198 /// CONTINUE / ASSERT / RAISE etc. propagate through the
2199 /// enclosing loop's BodyOutcome discipline the same way
2200 /// FOR range and WHILE do. Full record-binding (var as
2201 /// composite carrying all columns) queues with v7.40 record
2202 /// type infrastructure.
2203 ForQuery {
2204 var: String,
2205 query: Box<SelectStatement>,
2206 body: Vec<PlPgSqlStmt>,
2207 },
2208 /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2209 /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2210 /// computed at runtime from a text expression, parsed on the
2211 /// fly, then iterated. Enables dynamic queries where the
2212 /// projection / FROM / WHERE clauses depend on runtime values.
2213 ForExecute {
2214 var: String,
2215 sql_expr: Expr,
2216 body: Vec<PlPgSqlStmt>,
2217 },
2218}
2219
2220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2221#[non_exhaustive]
2222pub enum RaiseLevel {
2223 /// `RAISE NOTICE` — diagnostic message, observable in the
2224 /// server log. Does not affect the trigger's outcome.
2225 Notice,
2226 /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2227 Warning,
2228 /// `RAISE INFO` — like NOTICE, slightly quieter.
2229 Info,
2230 /// `RAISE LOG` — like NOTICE, lower priority.
2231 Log,
2232 /// `RAISE DEBUG` — like NOTICE, lowest priority.
2233 Debug,
2234 /// `RAISE EXCEPTION` — aborts the trigger function with the
2235 /// given message, propagating up to the caller as a query-
2236 /// level error.
2237 Exception,
2238}
2239
2240#[derive(Debug, Clone, PartialEq)]
2241pub enum AssignTarget {
2242 NewColumn(String),
2243 OldColumn(String),
2244 /// Reserved for v7.12.5 DECLARE'd local variables.
2245 Local(String),
2246}
2247
2248#[derive(Debug, Clone, PartialEq)]
2249pub enum ReturnTarget {
2250 /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2251 /// actually gets written (possibly with NEW.col mutations
2252 /// applied). For AFTER triggers, the return value is ignored.
2253 New,
2254 /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2255 /// the delete proceed; for BEFORE UPDATE / INSERT it's
2256 /// equivalent to dropping the write.
2257 Old,
2258 /// `RETURN NULL;` — for BEFORE triggers, skips the write
2259 /// entirely. For AFTER, the return value is ignored.
2260 Null,
2261 /// `RETURN <expr>;` — non-row return shape; reserved for the
2262 /// scalar UDF surface in v7.12.5+. Executor errors when used
2263 /// inside a trigger function.
2264 Expr(Expr),
2265}
2266
2267/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2268/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2269/// but the executor refuses them. `WHEN (cond)` clauses are out
2270/// of scope; the trigger function can short-circuit on a leading
2271/// IF inside its body once v7.12.5 lands IF.
2272#[derive(Debug, Clone, PartialEq)]
2273pub struct CreateTriggerStatement {
2274 pub name: String,
2275 pub or_replace: bool,
2276 pub timing: TriggerTiming,
2277 /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2278 /// three entries in order.
2279 pub events: Vec<TriggerEvent>,
2280 pub table: String,
2281 /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2282 /// only `Row`; `Statement` parses but the executor refuses.
2283 pub for_each: TriggerForEach,
2284 /// Name of the function to invoke. The function must exist at
2285 /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2286 /// forward reference (`function no_such_fn() does not exist`), so
2287 /// requiring it IS the PG behaviour (the old note claimed the
2288 /// opposite).
2289 pub function: String,
2290 /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2291 /// (mailrs round-5 G7). Non-empty only when the events list
2292 /// contains UPDATE and the user wrote the column-list filter.
2293 /// PG fires the trigger only when at least one of these
2294 /// columns appears in the SET clause; SPG conservatively
2295 /// fires on any UPDATE matching the listed columns or
2296 /// rewriting them at the row level. Empty vec = no filter
2297 /// (fire on every UPDATE).
2298 pub update_columns: Vec<String>,
2299 /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2300 /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2301 /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2302 pub when_condition: Option<Expr>,
2303}
2304
2305/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2306#[derive(Debug, Clone, PartialEq)]
2307pub struct CreateRuleStatement {
2308 pub name: String,
2309 pub or_replace: bool,
2310 /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2311 pub event: String,
2312 pub table: String,
2313 /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2314 /// (run alongside; PG's default when neither keyword is written).
2315 pub instead: bool,
2316 /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2317 pub when_condition: Option<Expr>,
2318 /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2319 pub commands: Vec<Statement>,
2320}
2321
2322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2323pub enum TriggerTiming {
2324 /// Fires before the row is written; the trigger function's
2325 /// return value (NEW or NULL) decides the row content and
2326 /// whether the write proceeds at all.
2327 Before,
2328 /// Fires after the row is written; the return value is
2329 /// ignored.
2330 After,
2331 /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2332 /// v7.12.4 (SPG has no updatable-view surface).
2333 InsteadOf,
2334}
2335
2336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2337pub enum TriggerEvent {
2338 Insert,
2339 Update,
2340 Delete,
2341 /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2342 /// so the trigger never fires.
2343 Truncate,
2344}
2345
2346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2347pub enum TriggerForEach {
2348 Row,
2349 Statement,
2350}
2351
2352/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2353///
2354/// SPG's index does not scan in a direction, but `indexdef` reproduces
2355/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2356/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2357/// which case PG's default applies — LAST for ascending, FIRST for
2358/// descending, and neither is rendered.
2359#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2360pub struct IndexColumnOrder {
2361 pub descending: bool,
2362 pub nulls_first: Option<bool>,
2363}
2364
2365#[derive(Debug, Clone, PartialEq)]
2366pub struct CreateIndexStatement {
2367 pub name: String,
2368 /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2369 /// either way, so this changes nothing about how the index is made
2370 /// — it is carried because PG refuses the CONCURRENTLY form inside
2371 /// a transaction block and accepts the plain one, and the engine
2372 /// cannot tell them apart without it.
2373 pub concurrently: bool,
2374 /// v7.39 (round 537) — the leading key column's ordering clause,
2375 /// which is the column SPG indexes.
2376 pub key_order: IndexColumnOrder,
2377 /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2378 /// written. SPG orders text by bytes, so honouring it changes
2379 /// nothing; PG prints it, because an explicitly named collation and
2380 /// the one a column inherits are different objects.
2381 pub key_collation: Option<String>,
2382 pub table: String,
2383 pub column: String,
2384 /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2385 /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2386 /// any NULL in the key exempts the row from the uniqueness check.
2387 pub nulls_not_distinct: bool,
2388 /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2389 /// graph for vector kNN); unspecified is the default B-tree index.
2390 pub method: IndexMethod,
2391 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2392 /// index name already exists, instead of raising `DuplicateIndex`.
2393 pub if_not_exists: bool,
2394 /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2395 /// non-key columns the planner should treat as "covered" by
2396 /// this index when checking whether a query can run as an
2397 /// index-only scan. Empty when no `INCLUDE` clause was given.
2398 pub included_columns: Vec<String>,
2399 /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2400 /// for which `<expr>` evaluates truthy enter the index;
2401 /// queries whose `WHERE` clause's canonical Display form
2402 /// matches this expression's Display form can be served by the
2403 /// partial index. Stored as a parsed `Expr` so the engine
2404 /// re-uses the existing evaluation path; storage persists the
2405 /// Display form on the catalog snapshot.
2406 pub partial_predicate: Option<Expr>,
2407 /// v6.8.2 — expression-based index. When `Some(expr)`, the
2408 /// index key is the result of `expr` evaluated on each row
2409 /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2410 /// field still names the *primary* column the expression
2411 /// touches so existing planner shortcuts that resolve a
2412 /// column position stay valid. `None` = plain
2413 /// column-reference index (the legacy shape).
2414 pub expression: Option<Expr>,
2415 /// v7.9.14 — extra column names after the leading column in a
2416 /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2417 /// planner today still only uses the leading column for index
2418 /// seeks; the extras are tracked verbatim so the same DDL
2419 /// round-trips through WAL replay + catalog snapshot, and so
2420 /// the engine can emit a clear warning at INDEX CREATE time
2421 /// that only the leading column is currently honoured.
2422 /// Composite BTree index keys land in v7.10.
2423 pub extra_columns: Vec<String>,
2424 /// v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
2425 /// / `NULLS LAST`, positionally aligned with `extra_columns`. The
2426 /// parser used to discard these, so a composite index's direction
2427 /// survived only on the leading column and `pg_get_indexdef`
2428 /// rendered `(a, b DESC)` back as `(a, b)`.
2429 pub extra_orders: Vec<IndexColumnOrder>,
2430 /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2431 /// enforces uniqueness on the indexed key (combined with the
2432 /// `partial_predicate` filter — only rows where the predicate
2433 /// evaluates truthy enter the uniqueness check). Standard SQL
2434 /// and PG's canonical way to express conditional uniqueness.
2435 /// mailrs K1.
2436 pub is_unique: bool,
2437 /// v7.15.0 — operator class on the leading column, when the
2438 /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2439 /// Lower-cased. Most opclasses are still informational; the
2440 /// engine routes on `gin_trgm_ops` specifically to build a
2441 /// trigram-shingle GIN over a TEXT column, and otherwise
2442 /// keeps the current "accepted and discarded" behaviour for
2443 /// pg_dump compatibility.
2444 pub opclass: Option<String>,
2445 /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2446 /// there was no `USING` clause.
2447 ///
2448 /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2449 /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2450 /// implementation for still load. That degradation is deliberate, but
2451 /// it loses the name — and the operator-class check needs it, both to
2452 /// look the class up under the AM the user actually named and to say
2453 /// which AM it was missing from, the way PG's message does.
2454 pub method_name: Option<String>,
2455}
2456
2457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2458pub enum IndexMethod {
2459 /// Default — B-tree over `IndexKey`. Used for equality / range
2460 /// lookups on scalar columns.
2461 BTree,
2462 /// `USING hnsw` — NSW graph for kNN over a vector column.
2463 Hnsw,
2464 /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2465 /// metadata that records (min_key, max_key) for each page in a
2466 /// cold-tier segment, on the indexed column. The optimizer
2467 /// can use these summaries to skip pages whose range does NOT
2468 /// overlap a query's WHERE predicate. BRIN indexes carry no
2469 /// in-memory data — the summaries live in the segment v2
2470 /// envelope's sidecar. Created via the standard
2471 /// `CREATE INDEX … USING brin (col)` syntax.
2472 Brin,
2473 /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2474 /// column. Posting lists map `lexeme word` → row locators; the
2475 /// planner uses them to narrow `WHERE col @@ tsquery` to the
2476 /// candidate rows whose vectors contain a matching term, then
2477 /// re-evaluates the full `@@` semantics on each candidate.
2478 /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2479 /// silently degraded to a full scan at query time.
2480 Gin,
2481}
2482
2483/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2484/// inside a CREATE TABLE column list.
2485///
2486/// The source table's shape can only be read from the catalog, so the
2487/// parser records the clause and the engine expands it. `at` is how many
2488/// explicit columns preceded it: PG keeps the written order, so
2489/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2490#[derive(Debug, Clone, PartialEq)]
2491pub struct LikeSpec {
2492 pub source: String,
2493 pub at: usize,
2494 pub options: LikeOptions,
2495 /// v7.40.0 — MySQL's `CREATE TABLE b LIKE a` keeps the source's
2496 /// index names (`PRIMARY`, `ks`); PostgreSQL's
2497 /// `CREATE TABLE b (LIKE a INCLUDING ALL)` renames the copies after
2498 /// the new table (`lb_pkey`, `lb_s_idx`). Both measured. The
2499 /// spelling says which engine's rule applies.
2500 pub keep_index_names: bool,
2501}
2502
2503/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2504/// types and NOT NULL and nothing else — measured on PG18, where a
2505/// copied generated column becomes a plain one and a copied identity
2506/// column loses its identity.
2507#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2508pub struct LikeOptions {
2509 pub defaults: bool,
2510 pub constraints: bool,
2511 pub identity: bool,
2512 pub generated: bool,
2513 pub indexes: bool,
2514 pub comments: bool,
2515}
2516
2517#[derive(Debug, Clone, PartialEq)]
2518pub struct CreateTableStatement {
2519 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2520 /// creating session's own namespace: it shadows a permanent table of the
2521 /// same name, other sessions never see it, and it is dropped when the
2522 /// session ends. A `bool` here lands in the struct's existing padding.
2523 pub temporary: bool,
2524 pub name: String,
2525 /// v7.39 — the `ENGINE=` a MySQL dump names. Consumed and discarded
2526 /// before, so `ENGINE=NONSUCH` built a table where MySQL 9.7.2
2527 /// answers `ERROR 1286`, and `sql_mode` claimed
2528 /// `NO_ENGINE_SUBSTITUTION` while doing it.
2529 pub engine: Option<String>,
2530 /// v7.40.0 — the `AUTO_INCREMENT=N` table option: the next value
2531 /// the table hands out. It was consumed and dropped, so the first
2532 /// row of a table declared `AUTO_INCREMENT=100` got 1 where MySQL
2533 /// 9.7.2 gives it 100 — and `SHOW CREATE TABLE`, which reproduces
2534 /// the option from the counter, round-tripped a different number.
2535 pub auto_increment: Option<i64>,
2536 pub columns: Vec<ColumnDef>,
2537 /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2538 /// the order written. Empty for a table that has none.
2539 pub like_specs: Vec<LikeSpec>,
2540 /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2541 /// Empty for a table that inherits from nothing. Order matters:
2542 /// the child takes each parent's columns in this order before its
2543 /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2544 pub inherits: Vec<String>,
2545 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2546 /// table name already exists, instead of raising `DuplicateTable`.
2547 pub if_not_exists: bool,
2548 /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2549 /// constraints. Column-level `REFERENCES` (single-column inline
2550 /// form) is normalised into this vec at parse time so the engine
2551 /// sees one uniform list.
2552 pub foreign_keys: Vec<ForeignKeyConstraint>,
2553 /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2554 /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2555 /// Engine resolves each into a BTree index named after the
2556 /// constraint's leading column at CREATE TABLE time; INSERT
2557 /// path enforces composite uniqueness via row scan on the
2558 /// leading column index.
2559 pub table_constraints: Vec<TableConstraint>,
2560 /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2561 /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2562 /// the engine creates a parent table whose own rows stay
2563 /// empty and routes INSERT/SELECT through children. Mutually
2564 /// exclusive with `partition_of` (parser enforces).
2565 pub partition_by: Option<PartitionBySpec>,
2566 /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2567 /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2568 /// the table inherits its column list from `parent` (the
2569 /// parser rejects an explicit column list when this is set);
2570 /// engine routes child rows back to the parent at INSERT.
2571 pub partition_of: Option<PartitionOfSpec>,
2572}
2573
2574/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2575/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2576/// future LIST / HASH without breaking the public AST shape.
2577#[derive(Debug, Clone, PartialEq)]
2578pub struct PartitionBySpec {
2579 pub kind: PartitionKindAst,
2580 /// One or more ident references into the parent's column list.
2581 /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2582 /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2583 /// shape PG-compatible.
2584 pub key_columns: Vec<String>,
2585}
2586
2587#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2588pub enum PartitionKindAst {
2589 Range,
2590 /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2591 /// `FOR VALUES IN (lit, lit, …)`.
2592 List,
2593 /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2594 /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2595 Hash,
2596}
2597
2598/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2599/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2600/// or the catch-all `DEFAULT` partition.
2601#[derive(Debug, Clone, PartialEq)]
2602pub struct PartitionOfSpec {
2603 pub parent_name: String,
2604 pub bounds: PartitionOfBoundsAst,
2605}
2606
2607#[derive(Debug, Clone, PartialEq)]
2608pub enum PartitionOfBoundsAst {
2609 /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2610 /// (lits include vector bodies), so we box both bounds to keep
2611 /// the variant size in line with `Default` for clippy and to
2612 /// minimise per-statement footprint when the partition shape
2613 /// isn't in use.
2614 Range {
2615 lower: Box<Expr>,
2616 upper: Box<Expr>,
2617 },
2618 /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2619 /// expr resolves to a typed literal at child-create time.
2620 List {
2621 values: Vec<Expr>,
2622 },
2623 /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2624 /// PG enforces `0 ≤ r < m`; m must be positive.
2625 Hash {
2626 modulus: u32,
2627 remainder: u32,
2628 },
2629 Default,
2630}
2631
2632/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2633/// column list. Either a composite PRIMARY KEY or a UNIQUE
2634/// (single- or multi-column).
2635#[derive(Debug, Clone, PartialEq)]
2636pub enum TableConstraint {
2637 /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2638 /// referenced column. Engine builds a BTree index named
2639 /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2640 PrimaryKey {
2641 name: Option<String>,
2642 columns: Vec<String>,
2643 /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2644 /// Round 621 consumed the clauses; these carry them.
2645 deferrable: bool,
2646 initially_deferred: bool,
2647 },
2648 /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2649 /// named `<table>_<leading_col>_key` (single-column) or
2650 /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2651 /// uniqueness on INSERT.
2652 Unique {
2653 name: Option<String>,
2654 columns: Vec<String>,
2655 /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2656 /// G10). PG 15+ flips the NULL handling so any number of
2657 /// NULL rows collide on the constraint. Default is
2658 /// `false` (NULLS DISTINCT, standard SQL behaviour).
2659 nulls_not_distinct: bool,
2660 /// v7.39 (round 711) — see PrimaryKey.
2661 deferrable: bool,
2662 initially_deferred: bool,
2663 /// v7.40.0 — MySQL's per-column index prefix on a
2664 /// `UNIQUE KEY k (b(4))`, aligned with `columns`. Unlike a
2665 /// plain KEY's, this one CHANGES what the constraint accepts:
2666 /// MySQL rejects two rows sharing the first four characters.
2667 /// Empty for every PostgreSQL spelling.
2668 prefix_lengths: Vec<Option<u32>>,
2669 },
2670 /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2671 /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2672 /// this same variant at parse time. Engine evaluates the
2673 /// predicate against each INSERT/UPDATE candidate row; a
2674 /// false / NULL result rejects the mutation.
2675 /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2676 /// PG adds such a constraint without scanning the existing rows: new
2677 /// rows are checked, the ones already there are grandfathered in, and
2678 /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2679 /// scans and flips it. pg_dump emits the suffix for exactly those, so
2680 /// validating them on restore would refuse a dump PG itself produced.
2681 Check {
2682 name: Option<String>,
2683 expr: Expr,
2684 not_valid: bool,
2685 },
2686 /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2687 /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2688 /// every element (the booking/scheduling non-overlap constraint,
2689 /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2690 /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2691 /// enforcement doesn't build the index yet). Each element pairs a
2692 /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2693 Exclude {
2694 name: Option<String>,
2695 method: Option<String>,
2696 elements: Vec<(String, String)>,
2697 },
2698 /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2699 /// non-unique secondary-index declaration inline in CREATE
2700 /// TABLE. Engine builds a BTree index on the leading column
2701 /// (composite columns parse but only the leading column is
2702 /// honoured at v7.15 — matches the existing
2703 /// `CreateIndexStatement::extra_columns` semantics). Useful
2704 /// for `mysql/blog`-style schemas that lean on routine
2705 /// secondary indexes for ORM lookups.
2706 Index {
2707 name: Option<String>,
2708 columns: Vec<String>,
2709 /// v7.40.0 — MySQL's per-column index prefix, `KEY k (b(4))`,
2710 /// positionally aligned with `columns`. `None` for a column
2711 /// written without one, which is every PostgreSQL index key.
2712 ///
2713 /// It was skipped by the parser and dropped, so the declaration
2714 /// was accepted and the index built over the whole column with
2715 /// nothing recording that a prefix had been asked for —
2716 /// `SHOW INDEX` then reported `Sub_part` NULL and
2717 /// `SHOW CREATE TABLE` printed `(b)` where MySQL 9.7.2 prints
2718 /// `(b(4))`.
2719 prefix_lengths: Vec<Option<u32>>,
2720 },
2721 /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2722 /// (cols)` inline declaration. Pre-v7.17 the parser
2723 /// silently dropped these so MyISAM-imported FULLTEXT
2724 /// indexes vanished; v7.17 routes them through the
2725 /// existing tsvector-GIN engine path so MATCH AGAINST
2726 /// queries get a real inverted index instead of falling
2727 /// back to a full scan. Multi-column FULLTEXT KEYs build
2728 /// one GIN per column at v7.17 (per-column posting lists);
2729 /// the leading column drives query planning.
2730 FulltextIndex {
2731 name: Option<String>,
2732 columns: Vec<String>,
2733 },
2734}
2735
2736#[derive(Debug, Clone, PartialEq)]
2737#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2738pub struct ColumnDef {
2739 pub name: String,
2740 pub ty: ColumnTypeName,
2741 pub nullable: bool,
2742 /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2743 /// evaluates this once (with an empty row) and caches the resulting
2744 /// `Value` on the column schema.
2745 pub default: Option<Expr>,
2746 /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2747 /// per such column and fills the slot when INSERT leaves it
2748 /// unbound (omitted from a column-list INSERT or explicitly NULL).
2749 pub auto_increment: bool,
2750 /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2751 /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2752 /// an implicit BTree index named `<table>_pkey` over this
2753 /// column at CREATE TABLE time, satisfying the parent-side
2754 /// index requirement for any FOREIGN KEY pointing at it.
2755 pub is_primary_key: bool,
2756 /// v7.13.0 — inline `UNIQUE` column constraint
2757 /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2758 /// into a single-column `TableConstraint::Unique` so the
2759 /// engine path stays uniform with table-level UNIQUE.
2760 pub is_unique: bool,
2761 /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2762 /// inline column constraint: treat NULL keys as equal so only one NULL
2763 /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2764 /// `TableConstraint::Unique { nulls_not_distinct }`.
2765 pub unique_nulls_not_distinct: bool,
2766 /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2767 /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2768 /// since this round so the fold into the table-level constraint keeps it.
2769 pub constraint_deferrable: bool,
2770 pub constraint_initially_deferred: bool,
2771 /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2772 /// (mailrs round-5 G3). Stored alongside the column so the
2773 /// CREATE TABLE handler can fold these into table-level
2774 /// CHECK constraints. Multiple inline CHECKs on the same
2775 /// column are concatenated with AND at the table level.
2776 pub check: Option<Expr>,
2777 /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2778 /// parser sees an unknown column-type ident (anything not in
2779 /// the built-in `parse_column_type_name` table), it sets
2780 /// `ty = ColumnTypeName::Text` and records the original name
2781 /// here. The engine resolves at CREATE TABLE time: if a
2782 /// catalog enum/domain with this name exists, the column is
2783 /// bound to it (label-checked on INSERT for enums; CHECK-
2784 /// constrained for domains); otherwise the CREATE TABLE
2785 /// errors with "unknown type".
2786 pub user_type_ref: Option<String>,
2787 /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2788 /// CURRENT_TIMESTAMP` column attribute. When set, an
2789 /// UPDATE that does NOT explicitly bind this column
2790 /// overrides the new value with `now()` (engine clock).
2791 /// Pre-v7.17 SPG silently accepted the syntax and never
2792 /// fired the override — `updated_at` columns from mysqldump
2793 /// stayed pinned at their initial DEFAULT forever, an
2794 /// audit Tier-S silent-failure. Generalised as a stored
2795 /// expression source so future shapes (`ON UPDATE
2796 /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2797 /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2798 pub on_update_runtime: Option<Expr>,
2799 /// v7.17.0 Phase 2.5 — text collation derived from the
2800 /// post-fix `COLLATE <name>` clause (and / or the table-level
2801 /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2802 /// per column). Pre-2.5 SPG accepted the clause and
2803 /// discarded the name, leaving every column byte-compared
2804 /// — a Tier-S silent failure when the customer expected
2805 /// `_ci` / `case_insensitive` semantics. Parser normalises
2806 /// the raw collation name into the variants in `Collation`.
2807 /// Default `Binary` preserves the legacy compare path.
2808 pub collation: Collation,
2809 /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2810 /// explicit `COLLATE <name>` clause rather than the default. Under the
2811 /// MySQL dialect a text column with NO explicit clause takes the
2812 /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2813 /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2814 /// flag is the only thing that tells them apart.
2815 pub collation_explicit: bool,
2816 /// v7.39 (round 676) — the collation name AS WRITTEN, because
2817 /// `collation` above cannot carry it: `Collation` is a two-variant
2818 /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2819 /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2820 /// tell them apart.
2821 pub collation_name: Option<String>,
2822 /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2823 /// 4.4 SPG accepted and discarded the keyword, leaving
2824 /// negative values silently accepted on a column the
2825 /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2826 /// rejects negative INSERT / UPDATE values on UNSIGNED int
2827 /// columns. SPG widening to `u64`-shaped storage is out of
2828 /// v7.17 scope; the upper bound remains the signed-type max
2829 /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2830 /// exceeds what every mailrs / Rails app actually uses.
2831 pub is_unsigned: bool,
2832 /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2833 /// value list captured at parse time. When `Some`, the parser
2834 /// recognised `ENUM(...)` in the type slot; the engine
2835 /// validates INSERT cells against this list at
2836 /// column_def_to_schema time and persists the variants on
2837 /// `ColumnSchema.inline_enum_variants`. None for all
2838 /// non-ENUM columns.
2839 pub inline_enum_variants: Option<Vec<String>>,
2840 /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2841 /// value list. Distinct from ENUM (subset semantics rather
2842 /// than pick-one). None for all non-SET columns.
2843 pub inline_set_variants: Option<Vec<String>>,
2844 /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2845 /// STORED` computed-column source. When `Some`, the engine
2846 /// stores the Display-form of the parsed expression on
2847 /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2848 /// and re-evaluates the expression against every INSERT /
2849 /// UPDATE candidate row, overwriting whatever the caller
2850 /// supplied for this column. Boxed to keep `ColumnDef` from
2851 /// blowing past the `large_enum_variant` clippy ceiling
2852 /// (`Expr` widens with vector literals).
2853 pub generated_stored_expr: Option<Box<Expr>>,
2854 /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2855 /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2856 /// `auto_increment`; this additionally marks the ALWAYS one, whose
2857 /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2858 /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2859 /// VALUE`. Only meaningful when the column is also an identity column.
2860 pub identity_always: bool,
2861 /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2862 /// integer width (TINYINT / MEDIUMINT), captured before the type
2863 /// collapses to SmallInt / Int. The engine copies it to
2864 /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2865 /// path can enforce the real range. None for every other column and
2866 /// under the PG dialect.
2867 pub mysql_int_width: Option<MysqlIntWidth>,
2868 /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2869 /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2870 /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2871 /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2872 /// CREATE TABLE time so the write path can truncate and the render path
2873 /// can pad. None under the PG dialect, where temporal columns keep full
2874 /// microseconds.
2875 pub mysql_fsp: Option<u8>,
2876 /// v7.39.2 — the column was written `TIMESTAMP` rather than
2877 /// `DATETIME` in a MySQL session. The engine copies it to
2878 /// `ColumnSchema.mysql_declared_timestamp` at CREATE TABLE.
2879 pub mysql_declared_timestamp: bool,
2880 /// v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair, copied to
2881 /// `ColumnSchema.mysql_float_md` at CREATE TABLE.
2882 pub mysql_float_md: Option<(u8, u8)>,
2883}
2884
2885/// v7.17.0 Phase 2.5 — text collation classification surfaced
2886/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2887/// engine bridges between the two at CREATE TABLE time.
2888///
2889/// Recognised collation-name patterns (case-insensitive):
2890/// * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase` → CaseInsensitive
2891/// * Everything else (`C`, `POSIX`, `default`,
2892/// `pg_catalog.default`, `*_cs`, `*_bin`, unknown names) → Binary
2893#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2894pub enum Collation {
2895 Binary,
2896 CaseInsensitive,
2897}
2898
2899/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2900/// integer width for a column whose `ColumnTypeName` is too wide to carry
2901/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2902/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2903/// TABLE time. Only recorded under the MySQL dialect.
2904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2905pub enum MysqlIntWidth {
2906 Tiny,
2907 Small,
2908 Medium,
2909 Int,
2910 /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2911 Big,
2912}
2913
2914#[allow(clippy::derivable_impls)]
2915impl Default for Collation {
2916 fn default() -> Self {
2917 Self::Binary
2918 }
2919}
2920
2921impl Collation {
2922 /// Classify a `COLLATE <name>` ident into one of the supported
2923 /// variants. Empty / unknown names fall back to `Binary` —
2924 /// matches the pre-2.5 silent-accept behaviour for snapshots
2925 /// that load through but don't actually depend on the
2926 /// collation semantics.
2927 #[must_use]
2928 pub fn from_collation_name(name: &str) -> Self {
2929 let lc = name.trim().to_ascii_lowercase();
2930 // Strip any quotes / schema-qualifier the parser left on
2931 // (e.g. `pg_catalog.default`).
2932 let bare = lc
2933 .trim_matches(|c: char| c == '"' || c == '\'')
2934 .rsplit('.')
2935 .next()
2936 .unwrap_or("");
2937 if bare.is_empty() {
2938 return Self::Binary;
2939 }
2940 if bare == "case_insensitive" || bare == "nocase" {
2941 return Self::CaseInsensitive;
2942 }
2943 // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2944 // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2945 if bare.ends_with("_ci") {
2946 return Self::CaseInsensitive;
2947 }
2948 Self::Binary
2949 }
2950}
2951
2952/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2953/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2954/// parse into this shape — the column-level form has a single-entry
2955/// `columns` / `parent_columns`.
2956#[derive(Debug, Clone, PartialEq)]
2957pub struct ForeignKeyConstraint {
2958 /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2959 /// today but parses + stores it so a future ALTER TABLE DROP
2960 /// CONSTRAINT can target by name (v7.6.8).
2961 pub name: Option<String>,
2962 /// Local columns participating in the FK (≥ 1).
2963 pub columns: Vec<String>,
2964 /// Referenced parent table.
2965 pub parent_table: String,
2966 /// Referenced parent columns. Must have the same arity as
2967 /// `columns`; engine validates parent has a PK / UNIQUE index
2968 /// on exactly this column set (v7.6.1).
2969 pub parent_columns: Vec<String>,
2970 /// `ON DELETE` action. Defaults to `Restrict` if absent.
2971 pub on_delete: FkAction,
2972 /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2973 pub on_update: FkAction,
2974 /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2975 pub match_type: MatchType,
2976 /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2977 /// dropped on the floor, so a constraint declared DEFERRABLE was
2978 /// enforced immediately and a circular-FK migration could not load.
2979 pub deferrable: bool,
2980 /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2981 /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2982 pub initially_deferred: bool,
2983}
2984
2985/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2986/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2987/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2988#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2989pub enum MatchType {
2990 #[default]
2991 Simple,
2992 Full,
2993}
2994
2995/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2996#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2997pub enum FkAction {
2998 /// Reject the parent mutation if any child row references it.
2999 /// SQL spec default; SPG default when no clause is given.
3000 Restrict,
3001 /// Recursively propagate the parent's delete / update to the
3002 /// child rows. Same TX.
3003 Cascade,
3004 /// Set the child FK column(s) to NULL. Requires the FK columns
3005 /// to be NULL-able.
3006 SetNull,
3007 /// Set the child FK column(s) to their declared DEFAULT.
3008 /// Requires the child column(s) to have DEFAULT.
3009 SetDefault,
3010 /// SQL spec `NO ACTION` (deferred check). SPG treats this as
3011 /// `Restrict` because the single-writer model has no deferred
3012 /// constraint window; the keyword is accepted for compatibility.
3013 NoAction,
3014}
3015
3016/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
3017/// optional `USING <encoding>` clause; omitting it keeps the
3018/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
3019/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
3020/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
3021/// binary16 (2× compression, ~3 decimal digits of precision).
3022#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3023pub enum VecEncoding {
3024 /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
3025 /// uncompressed `vector` type wire / storage layout.
3026 #[default]
3027 F32,
3028 /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
3029 /// `spg_storage::quantize::Sq8Vector` for the math + recall
3030 /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
3031 /// dim ≥ 32).
3032 Sq8,
3033 /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
3034 /// per-element. DDL keyword `HALF` (pgvector convention).
3035 /// Bit-exact dequantise to f32 at the storage layer; no
3036 /// rerank pass needed for kNN search.
3037 F16,
3038}
3039
3040impl fmt::Display for VecEncoding {
3041 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3042 match self {
3043 Self::F32 => f.write_str("F32"),
3044 Self::Sq8 => f.write_str("SQ8"),
3045 // pgvector convention: DDL keyword is `HALF`, not `F16`.
3046 Self::F16 => f.write_str("HALF"),
3047 }
3048 }
3049}
3050
3051/// SQL-level type names. The mapping to the storage runtime's `DataType`
3052/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
3053#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3054pub enum ColumnTypeName {
3055 /// v7.39 (round 291) — PG's `name`, the identifier type its
3056 /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
3057 /// answered `type "name" does not exist` to.
3058 Name,
3059 /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
3060 /// 32-bit wrapping counter the row header carries; `xid8` is the
3061 /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
3062 /// SPG answered `type "xid" does not exist` to.
3063 Xid,
3064 Xid8,
3065 /// v7.39 (round 667) — `OID`. `XID` was already a column type here
3066 /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
3067 /// `type "oid" does not exist` while `t(x XID)` built fine.
3068 Oid,
3069 SmallInt,
3070 Int,
3071 BigInt,
3072 Float,
3073 /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
3074 /// IEEE. It used to map to [`Self::Float`] on the theory that a
3075 /// wider float is harmless, but the width is observable: a `real`
3076 /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
3077 /// answered false where PG answers true.
3078 Real,
3079 Text,
3080 /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
3081 Varchar(u32),
3082 /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
3083 Char(u32),
3084 Bool,
3085 /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
3086 /// `USING <encoding>` clause; omitting it surfaces as
3087 /// `encoding = VecEncoding::F32` (the pre-v6 default).
3088 Vector {
3089 dim: u32,
3090 encoding: VecEncoding,
3091 },
3092 /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
3093 /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
3094 /// v7.39 (round 271) — scale widened to u16 alongside the value's.
3095 /// v7.39 (round 272) — precision too: PG's runs to 1000.
3096 /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
3097 /// a negative one rounds to tens / hundreds. A VALUE's display scale
3098 /// stays unsigned.
3099 Numeric(u16, i16),
3100 /// `DATE` — calendar day, no time-of-day component.
3101 Date,
3102 /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
3103 /// precision.
3104 Timestamp,
3105 /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
3106 /// stores all timestamps as UTC microseconds-since-epoch and
3107 /// does not carry per-row offset (PG's internal representation
3108 /// is the same — TZ is a display convention). The distinction
3109 /// from `TIMESTAMP` exists for the PG-wire layer to advertise
3110 /// OID 1184 so sqlx-style clients decode into
3111 /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
3112 Timestamptz,
3113 /// v4.9 `JSON` — text-backed JSON document. No parse-time
3114 /// validation; the engine round-trips the literal verbatim.
3115 /// PG OID 114 on the wire.
3116 Json,
3117 /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
3118 /// PG OID 3802 on the wire so sqlx-style binary-typed clients
3119 /// decode without a custom type registration.
3120 Jsonb,
3121 /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
3122 /// Literal forms (decoded by the engine at coercion time):
3123 /// - PG hex form: `'\xDEADBEEF'`
3124 /// - Escape form: `'foo\\000bar'` (backslash octal triples)
3125 Bytes,
3126 /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
3127 /// OID 1009. Literal forms accepted by the parser:
3128 /// - `ARRAY['a', 'b', NULL]`
3129 /// - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
3130 /// form at coerce time)
3131 TextArray,
3132 /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
3133 /// 1007. Same literal forms as TEXT[] (substituting integer
3134 /// elements).
3135 IntArray,
3136 /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
3137 /// OID 1016.
3138 BigIntArray,
3139 /// v7.40.0 `OID[]` — single-dimension oid array. PG wire OID
3140 /// 1028.
3141 ///
3142 /// `DataType::OidArray` and its value, codec tag, wire encoding
3143 /// and every naming surface have existed since v7.39 (round 694);
3144 /// what was missing was only the DDL spelling, so
3145 /// `CREATE TABLE t (c oid[])` answered `Oid[] not yet supported`
3146 /// while PostgreSQL 18.6 accepts it. Capability present, routing
3147 /// absent — the same shape as this repository's other hand-kept
3148 /// lists.
3149 OidArray,
3150 /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
3151 /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
3152 /// external form). G-CRIT-3.
3153 TsVector,
3154 /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
3155 /// wire OID 3615.
3156 TsQuery,
3157 /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
3158 /// Literal input accepts canonical hyphenated, unhyphenated,
3159 /// uppercase, and `{...}`-braced forms; display normalises to
3160 /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
3161 /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
3162 /// gen_random_uuid()`.
3163 Uuid,
3164 /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
3165 /// microseconds since 00:00:00. PG wire OID 1083. Literal
3166 /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
3167 /// (6-digit microsecond precision). Display normalises to
3168 /// the canonical `HH:MM:SS[.ffffff]`.
3169 Time,
3170 /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
3171 /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
3172 /// PG OID; advertised as INT4 on the wire. Display always
3173 /// 4 digits zero-padded.
3174 Year,
3175 /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
3176 /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
3177 /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
3178 /// Offset range: ±14 hours.
3179 TimeTz,
3180 /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
3181 /// (locale-independent storage). Wire OID 790. Literal input
3182 /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
3183 /// major units), optional leading `-`. Display: en_US locale.
3184 Money,
3185 /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
3186 /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
3187 /// — the engine bridges to `DataType::Range(RangeKind)`.
3188 Range(RangeKindAst),
3189 /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
3190 /// `text => text` map with NULL value support.
3191 Hstore,
3192 /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
3193 IntArray2D,
3194 BigIntArray2D,
3195 TextArray2D,
3196 /// v7.39 (read01 round 75) — `bool[][]`.
3197 BoolArray2D,
3198 /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
3199 /// three-field {months, days, micros} struct (PG-byte-equal),
3200 /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
3201 /// β-P2 `INTERVAL` was runtime-only — literal in expression
3202 /// position but rejected at CREATE TABLE.
3203 Interval,
3204 /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
3205 /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
3206 /// PG external form quotes each non-NULL element because
3207 /// interval text contains spaces / colons
3208 /// (`{"1 day","24:00:00",NULL}`).
3209 IntervalArray,
3210 /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3211 /// mirrors a scalar `ColumnTypeName` that already existed.
3212 BoolArray,
3213 SmallIntArray,
3214 FloatArray,
3215 NumericArray,
3216 DateArray,
3217 TimestampArray,
3218 TimestamptzArray,
3219 UuidArray,
3220 JsonArray,
3221 JsonbArray,
3222 BytesArray,
3223 VarcharArray,
3224 CharArray,
3225 /// v7.40.0 — five array spellings PG 18.6 accepts at
3226 /// `CREATE TABLE` and SPG refused at the type name. The
3227 /// element types were all present; only the `[]` step was.
3228 RealArray,
3229 TimeArray,
3230 TimeTzArray,
3231 InetArray,
3232 XmlArray,
3233 /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3234 /// as `Range(RangeKindAst)` — one column type variant covers
3235 /// all six builtin multiranges, kind pins the element type.
3236 /// Wire OIDs in pgwire.
3237 Multirange(RangeKindAst),
3238 /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3239 /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3240 /// Wire OIDs in pgwire.
3241 Point,
3242 Lseg,
3243 Path,
3244 PgBox,
3245 Polygon,
3246 Line,
3247 Circle,
3248 /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3249 Inet,
3250 Cidr,
3251 Macaddr,
3252 Macaddr8,
3253 /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3254 Bit(u32),
3255 /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3256 BitVarying(u32),
3257 Xml,
3258 Char1,
3259 MoneyArray,
3260}
3261
3262/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3263/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3264/// crate doesn't depend on storage. Bridged at engine boundary.
3265#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3266pub enum RangeKindAst {
3267 Int4,
3268 Int8,
3269 Num,
3270 Ts,
3271 TsTz,
3272 Date,
3273}
3274
3275impl fmt::Display for ColumnTypeName {
3276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3277 match self {
3278 Self::SmallInt => f.write_str("SMALLINT"),
3279 Self::Int => f.write_str("INT"),
3280 Self::BigInt => f.write_str("BIGINT"),
3281 Self::Float => f.write_str("FLOAT"),
3282 Self::Real => f.write_str("REAL"),
3283 Self::Text => f.write_str("TEXT"),
3284 Self::Name => f.write_str("name"),
3285 Self::Xid => f.write_str("xid"),
3286 Self::Xid8 => f.write_str("xid8"),
3287 Self::Oid => f.write_str("oid"),
3288 Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3289 Self::Char(n) => write!(f, "CHAR({n})"),
3290 Self::Bool => f.write_str("BOOL"),
3291 Self::Vector { dim, encoding } => match encoding {
3292 VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3293 VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3294 VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3295 },
3296 Self::Json => f.write_str("JSON"),
3297 Self::Jsonb => f.write_str("JSONB"),
3298 Self::Bytes => f.write_str("BYTEA"),
3299 Self::TextArray => f.write_str("TEXT[]"),
3300 Self::IntArray => f.write_str("INT[]"),
3301 Self::BigIntArray => f.write_str("BIGINT[]"),
3302 Self::OidArray => f.write_str("oid[]"),
3303 Self::TsVector => f.write_str("TSVECTOR"),
3304 Self::TsQuery => f.write_str("TSQUERY"),
3305 Self::Uuid => f.write_str("UUID"),
3306 Self::Numeric(p, s) => {
3307 if *s == 0 {
3308 write!(f, "NUMERIC({p})")
3309 } else {
3310 write!(f, "NUMERIC({p}, {s})")
3311 }
3312 }
3313 Self::Date => f.write_str("DATE"),
3314 Self::Timestamp => f.write_str("TIMESTAMP"),
3315 Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3316 Self::Time => f.write_str("TIME"),
3317 Self::Year => f.write_str("YEAR"),
3318 Self::TimeTz => f.write_str("TIMETZ"),
3319 Self::Money => f.write_str("MONEY"),
3320 Self::Range(k) => f.write_str(match k {
3321 RangeKindAst::Int4 => "INT4RANGE",
3322 RangeKindAst::Int8 => "INT8RANGE",
3323 RangeKindAst::Num => "NUMRANGE",
3324 RangeKindAst::Ts => "TSRANGE",
3325 RangeKindAst::TsTz => "TSTZRANGE",
3326 RangeKindAst::Date => "DATERANGE",
3327 }),
3328 Self::Hstore => f.write_str("HSTORE"),
3329 Self::Interval => f.write_str("INTERVAL"),
3330 Self::IntervalArray => f.write_str("INTERVAL[]"),
3331 Self::BoolArray => f.write_str("BOOL[]"),
3332 Self::SmallIntArray => f.write_str("SMALLINT[]"),
3333 Self::FloatArray => f.write_str("FLOAT[]"),
3334 Self::NumericArray => f.write_str("NUMERIC[]"),
3335 Self::DateArray => f.write_str("DATE[]"),
3336 Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3337 Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3338 Self::UuidArray => f.write_str("UUID[]"),
3339 Self::JsonArray => f.write_str("JSON[]"),
3340 Self::JsonbArray => f.write_str("JSONB[]"),
3341 Self::BytesArray => f.write_str("BYTEA[]"),
3342 Self::VarcharArray => f.write_str("VARCHAR[]"),
3343 Self::CharArray => f.write_str("CHAR[]"),
3344 Self::RealArray => f.write_str("REAL[]"),
3345 Self::TimeArray => f.write_str("TIME[]"),
3346 Self::TimeTzArray => f.write_str("TIMETZ[]"),
3347 Self::InetArray => f.write_str("INET[]"),
3348 Self::XmlArray => f.write_str("XML[]"),
3349 Self::Multirange(k) => f.write_str(match k {
3350 RangeKindAst::Int4 => "INT4MULTIRANGE",
3351 RangeKindAst::Int8 => "INT8MULTIRANGE",
3352 RangeKindAst::Num => "NUMMULTIRANGE",
3353 RangeKindAst::Ts => "TSMULTIRANGE",
3354 RangeKindAst::TsTz => "TSTZMULTIRANGE",
3355 RangeKindAst::Date => "DATEMULTIRANGE",
3356 }),
3357 Self::Point => f.write_str("POINT"),
3358 Self::Lseg => f.write_str("LSEG"),
3359 Self::Path => f.write_str("PATH"),
3360 Self::PgBox => f.write_str("BOX"),
3361 Self::Polygon => f.write_str("POLYGON"),
3362 Self::Line => f.write_str("LINE"),
3363 Self::Circle => f.write_str("CIRCLE"),
3364 Self::Inet => f.write_str("INET"),
3365 Self::Cidr => f.write_str("CIDR"),
3366 Self::Macaddr => f.write_str("MACADDR"),
3367 Self::Macaddr8 => f.write_str("MACADDR8"),
3368 Self::Bit(0) => f.write_str("BIT"),
3369 Self::Bit(n) => write!(f, "BIT({n})"),
3370 Self::BitVarying(0) => f.write_str("VARBIT"),
3371 Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3372 Self::Xml => f.write_str("XML"),
3373 Self::Char1 => f.write_str("\"char\""),
3374 Self::MoneyArray => f.write_str("MONEY[]"),
3375 Self::IntArray2D => f.write_str("INT[][]"),
3376 Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3377 Self::TextArray2D => f.write_str("TEXT[][]"),
3378 Self::BoolArray2D => f.write_str("BOOL[][]"),
3379 }
3380 }
3381}
3382
3383/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3384/// engine evaluates `expr` per matched row in the table's row order
3385/// and rewrites cells in place. Indexed columns are dropped + re-
3386/// inserted into the affected B-tree on each row change.
3387/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3388/// tail on a DML statement. Boxed off the statement struct so the PG-only
3389/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3390/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3391/// the identical meaning, so both share this one payload rather than each
3392/// growing its own.
3393#[derive(Debug, Clone, PartialEq)]
3394pub struct DmlOrderLimit {
3395 pub order_by: Vec<OrderBy>,
3396 pub limit: Option<u32>,
3397}
3398
3399/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3400/// FROM, kept so the engine can finish the job.
3401///
3402/// The parser rewrites the statement onto correlated subqueries, and it
3403/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3404/// name belongs to the target or to a source needs their column lists,
3405/// which parse time does not have. Carrying the clause lets the engine
3406/// — which has the catalog — resolve the rest.
3407#[derive(Debug, Clone, PartialEq)]
3408pub struct UpdateFromSources {
3409 pub from: FromClause,
3410 pub sub_where: Option<Expr>,
3411}
3412
3413#[derive(Debug, Clone, PartialEq)]
3414pub struct UpdateStatement {
3415 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3416 /// level UPDATE. Empty for a plain UPDATE.
3417 pub ctes: Vec<Cte>,
3418 pub table: String,
3419 /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3420 /// to `t`'s own rows and not to anything that descends from it.
3421 ///
3422 /// Round 644 taught the FROM clause the keyword and left DML behind
3423 /// because it needed a field here, and this struct carries a warning
3424 /// that round 413 measured widening it in place overflowing the
3425 /// parser's nesting stack. That warning was about `from_sources`, a
3426 /// struct wide enough to need boxing; a `bool` lands in the padding
3427 /// already present — same as `CreateTableStatement::temporary`.
3428 ///
3429 /// It also earns its keep beyond the spelling: the inheritance
3430 /// fan-out needs a way to say "the parent's own rows" as a
3431 /// statement, or running one on the parent recurses forever.
3432 pub only: bool,
3433 /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3434 /// statement's expressions refer to the target row by. PG allows the
3435 /// bare spelling here (unlike INSERT, which requires AS).
3436 pub alias: Option<String>,
3437 pub assignments: Vec<(String, Expr)>,
3438 /// v7.39 (round 533) — boxed: round 413 measured that widening this
3439 /// struct in place overflows the parser's nesting stack.
3440 pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3441 pub where_: Option<Expr>,
3442 /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3443 /// mutate the first `limit` rows in the given order. PG has no such
3444 /// clause; the parser accepts it only under the MySQL dialect. Boxed
3445 /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3446 /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3447 /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3448 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3449 /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3450 /// clause (legacy CommandComplete path). Some = engine
3451 /// evaluates the projection over each mutated row and
3452 /// streams the result as a Rows QueryResult.
3453 pub returning: Option<Vec<SelectItem>>,
3454}
3455
3456/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3457/// from the active catalog and prunes them from every index.
3458#[derive(Debug, Clone, PartialEq)]
3459pub struct DeleteStatement {
3460 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3461 /// level DELETE. Empty for a plain DELETE.
3462 pub ctes: Vec<Cte>,
3463 pub table: String,
3464 /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3465 /// to `t`'s own rows and not to anything that descends from it.
3466 ///
3467 /// Round 644 taught the FROM clause the keyword and left DML behind
3468 /// because it needed a field here, and this struct carries a warning
3469 /// that round 413 measured widening it in place overflowing the
3470 /// parser's nesting stack. That warning was about `from_sources`, a
3471 /// struct wide enough to need boxing; a `bool` lands in the padding
3472 /// already present — same as `CreateTableStatement::temporary`.
3473 ///
3474 /// It also earns its keep beyond the spelling: the inheritance
3475 /// fan-out needs a way to say "the parent's own rows" as a
3476 /// statement, or running one on the parent recurses forever.
3477 pub only: bool,
3478 /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3479 /// the WHERE / RETURNING expressions refer to the target row by.
3480 pub alias: Option<String>,
3481 pub where_: Option<Expr>,
3482 /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3483 /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3484 /// form (round 413), so it shares that payload — and it is boxed for
3485 /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3486 /// statement tipped the parser's 512 KiB nesting stack.
3487 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3488 /// v7.9.4 — `RETURNING <projection>`.
3489 pub returning: Option<Vec<SelectItem>>,
3490}
3491
3492/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3493/// One WHEN clause fires per source row depending on whether the
3494/// `on` condition matched any target row(s); the executor walks
3495/// `clauses` in declaration order and fires the first whose
3496/// `matched` kind and optional `condition` are both satisfied.
3497#[derive(Debug, Clone, PartialEq)]
3498pub struct MergeStatement {
3499 /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3500 /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3501 /// in PG). Each CTE materialises before the merge runs and its alias
3502 /// resolves as a source relation.
3503 pub ctes: Vec<Cte>,
3504 pub target: String,
3505 pub target_alias: Option<String>,
3506 pub source: String,
3507 pub source_alias: Option<String>,
3508 /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3509 /// the engine materialises this SELECT for the source rows and `source`
3510 /// is empty; the alias (required by PG for a subquery source) is in
3511 /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3512 pub source_select: Option<Box<SelectStatement>>,
3513 /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3514 /// positional column-alias list after the source alias. Empty when
3515 /// the statement carries none; the engine renames the materialised
3516 /// source columns positionally (PG's rule).
3517 pub source_column_aliases: Vec<String>,
3518 pub on: Expr,
3519 pub clauses: Vec<MergeWhenClause>,
3520 /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3521 /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3522 /// target/source aliases. `None` = no RETURNING (the common form).
3523 pub returning: Option<Vec<SelectItem>>,
3524}
3525
3526#[derive(Debug, Clone, PartialEq)]
3527pub struct MergeWhenClause {
3528 pub matched: MergeMatched,
3529 /// Optional `AND <expr>` filter — when present, the clause
3530 /// only fires for the source rows whose match-pair satisfies
3531 /// the predicate.
3532 pub condition: Option<Expr>,
3533 pub action: MergeAction,
3534}
3535
3536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3537pub enum MergeMatched {
3538 Matched,
3539 /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3540 /// target row (the classic insert branch).
3541 NotMatched,
3542 /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3543 /// row no source row matches. Actions are UPDATE / DELETE / DO
3544 /// NOTHING only (INSERT is a syntax error, as in PG).
3545 NotMatchedBySource,
3546}
3547
3548#[derive(Debug, Clone, PartialEq)]
3549pub enum MergeAction {
3550 /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3551 /// explicit column list (the bare `INSERT VALUES (vals)`
3552 /// shape lands later).
3553 Insert {
3554 columns: Vec<String>,
3555 values: Vec<Expr>,
3556 },
3557 /// `UPDATE SET col = expr [, …]` — applied to every matched
3558 /// target row for the firing source row.
3559 Update { assignments: Vec<(String, Expr)> },
3560 /// `DELETE` — drop every matched target row.
3561 Delete,
3562 /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3563 /// the clause and SPG mirrors so a customer-side MERGE that
3564 /// uses it for branch-control doesn't error).
3565 DoNothing,
3566}
3567
3568#[derive(Debug, Clone, PartialEq)]
3569pub struct InsertStatement {
3570 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3571 /// level INSERT (writable CTE outer body). Empty for a plain
3572 /// INSERT. PG semantics: each CTE materialises before the
3573 /// outer INSERT runs, sharing the same transaction.
3574 pub ctes: Vec<Cte>,
3575 pub table: String,
3576 /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3577 /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3578 /// row by. PG requires the AS keyword in this position.
3579 pub alias: Option<String>,
3580 /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3581 /// `None`, every tuple is positional and must match the table arity.
3582 /// When `Some`, the engine maps each tuple slot to the named column and
3583 /// fills the rest with NULL (must be nullable).
3584 pub columns: Option<Vec<String>>,
3585 /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3586 /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3587 /// `select_source` is `Some` (the engine builds rows from the
3588 /// inner SELECT result set instead).
3589 pub rows: Vec<Vec<Expr>>,
3590 /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3591 /// round-5 G4). When present, `rows` is empty and the engine
3592 /// materialises the SELECT result, coerces each output tuple to
3593 /// the target column types, and inserts as a single batch.
3594 pub select_source: Option<Box<SelectStatement>>,
3595 /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3596 /// upsert clause. None = legacy INSERT (conflict raises a
3597 /// DuplicateKey error). mailrs migration blocker #2.
3598 pub on_conflict: Option<OnConflictClause>,
3599 /// v7.9.4 — `RETURNING <projection>`.
3600 pub returning: Option<Vec<SelectItem>>,
3601 /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3602 /// between the column list and VALUES. Governs how explicitly-supplied
3603 /// values interact with `GENERATED … AS IDENTITY` columns:
3604 /// * `None` — default. A `GENERATED ALWAYS` identity column rejects
3605 /// an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3606 /// * `System` — override the ALWAYS restriction: the explicit value
3607 /// is used verbatim, as for a `BY DEFAULT` column.
3608 /// * `User` — ignore any explicit value on a `BY DEFAULT` identity
3609 /// column and generate from the sequence instead (no effect on
3610 /// non-identity columns).
3611 pub overriding: Overriding,
3612 /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3613 /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3614 /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3615 /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3616 /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3617 /// into a NOT NULL column becomes the type's default), and the engine
3618 /// cannot recover that intent from the conflict clause alone. A plain
3619 /// `bool` lands in this struct's existing padding, so the AST does not
3620 /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3621 pub mysql_ignore: bool,
3622}
3623
3624/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3625#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3626#[non_exhaustive]
3627pub enum Overriding {
3628 /// No `OVERRIDING` clause.
3629 #[default]
3630 None,
3631 /// `OVERRIDING SYSTEM VALUE`.
3632 System,
3633 /// `OVERRIDING USER VALUE`.
3634 User,
3635}
3636
3637/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3638#[derive(Debug, Clone, PartialEq)]
3639pub struct OnConflictClause {
3640 /// Local columns that identify the conflict (must match a
3641 /// UNIQUE / PRIMARY KEY index on the target table). Empty
3642 /// list means the user wrote `ON CONFLICT DO …` without a
3643 /// target — the engine arbitrates on every unique constraint
3644 /// (round 240).
3645 pub target_columns: Vec<String>,
3646 /// v7.39 (round 240) — the index predicate after the target list
3647 /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3648 /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3649 /// which satisfy any predicate, so it is parsed and carried but not
3650 /// consulted (recorded residual: partial-unique-index arbiters).
3651 pub index_where: Option<Expr>,
3652 /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3653 /// <name>`: the pg_dump conflict-target form. The engine
3654 /// resolves the name to the constraint's columns.
3655 pub constraint_name: Option<String>,
3656 /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3657 /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3658 /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3659 /// `ON CONFLICT DO UPDATE` is refused (42601).
3660 pub mysql_lowered: bool,
3661 /// The action on conflict.
3662 pub action: OnConflictAction,
3663}
3664
3665/// v7.9.7 — action on conflict.
3666#[derive(Debug, Clone, PartialEq)]
3667pub enum OnConflictAction {
3668 /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3669 /// silently skips conflicting ones.
3670 Nothing,
3671 /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3672 /// may reference `EXCLUDED.col` to read the incoming row's
3673 /// value (engine wires `EXCLUDED` as a virtual table).
3674 Update {
3675 assignments: Vec<(String, Expr)>,
3676 where_: Option<Expr>,
3677 },
3678}
3679
3680/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3681///
3682/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3683/// policies are spelled again here and mapped at the engine boundary.
3684/// v7.39 — the modes `BEGIN` / `START TRANSACTION` / `SET TRANSACTION`
3685/// / `SET SESSION CHARACTERISTICS` accept. `read_only` used to be parsed
3686/// and dropped on the floor, so `BEGIN READ ONLY` opened an ordinary
3687/// read-write transaction and every write in it was accepted.
3688///
3689/// `None` on either field means the statement did not name that mode, so
3690/// the session default applies — which is not the same as naming the
3691/// default explicitly.
3692#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3693pub struct TransactionModes {
3694 pub isolation: Option<IsolationLevel>,
3695 /// `Some(true)` = READ ONLY, `Some(false)` = READ WRITE.
3696 pub read_only: Option<bool>,
3697 /// v7.40.12 — `Some(true)` = DEFERRABLE, `Some(false)` = NOT
3698 /// DEFERRABLE. PG reports it through `transaction_deferrable`, and
3699 /// consults it only for a SERIALIZABLE READ ONLY transaction, where
3700 /// it may make the transaction WAIT for a snapshot it can run
3701 /// against without serialization failures. SPG reports the property
3702 /// and does not defer; the difference a client can observe is a
3703 /// wait, never a different answer.
3704 pub deferrable: Option<bool>,
3705}
3706
3707/// v7.39 — what a read-only transaction refuses, and what PG calls it.
3708///
3709/// SPG did not enforce read-only transactions at all: `BEGIN READ ONLY;
3710/// INSERT …` answered `INSERT 0 1` and committed, and
3711/// `default_transaction_read_only = on` changed nothing. Both GUCs were
3712/// in the inventory, so a session could set one, read it back, and be
3713/// told it held a guarantee nothing was enforcing. Applications open
3714/// read-only transactions as a SAFETY measure — a reporting connection,
3715/// a read-only leg in a pool, a "this path must not write" discipline —
3716/// so accepting the writes is the worst possible answer.
3717///
3718/// `Some(tag)` means refuse with PG's message, `cannot execute {tag} in
3719/// a read-only transaction` (SQLSTATE 25006). Every tag below was read
3720/// back from PostgreSQL 18.6 by running the statement inside
3721/// `BEGIN READ ONLY`, one statement per transaction so no error could be
3722/// attributed to the wrong line.
3723///
3724/// Several answers were not what one would guess, which is why they were
3725/// measured rather than reasoned:
3726///
3727/// * `CREATE TEMP TABLE` is REFUSED, tagged `CREATE TABLE`.
3728/// * `NOTIFY`, `LISTEN` and `REINDEX` are ALLOWED.
3729/// * `PREPARE` of an INSERT is ALLOWED — only the EXECUTE writes.
3730/// * `UPDATE … WHERE false`, which changes nothing, is still REFUSED:
3731/// the verb decides, not the row count.
3732/// * `GRANT`, `COMMENT ON` and `SELECT … FOR SHARE` are all REFUSED.
3733///
3734/// The match is exhaustive on purpose. A new statement cannot be added
3735/// without deciding here whether it writes, which is the failure this
3736/// repository keeps meeting: one member of a family gets handled and its
3737/// siblings quietly do not.
3738impl Statement {
3739 #[must_use]
3740 pub fn read_only_violation_tag(&self) -> Option<&'static str> {
3741 match self {
3742 // v7.39.9 — MySQL's RENAME TABLE is DDL, refused read-only
3743 // for the same reason ALTER TABLE … RENAME TO is.
3744 Self::RenameTables(_) => Some("RENAME TABLE"),
3745 // ---- writes rows -------------------------------------------
3746 Self::Insert { .. } => Some("INSERT"),
3747 Self::Update { .. } => Some("UPDATE"),
3748 Self::Delete { .. } => Some("DELETE"),
3749 Self::Merge { .. } => Some("MERGE"),
3750 Self::Truncate { .. } => Some("TRUNCATE TABLE"),
3751 Self::CopyFromFile { .. } => Some("COPY FROM"),
3752
3753 // A SELECT that takes row locks writes lock state, and PG
3754 // names the strength it was asked for.
3755 Self::Select(sel) => sel.locking.as_ref().map(|l| match l.strength {
3756 LockStrength::Update => "SELECT FOR UPDATE",
3757 LockStrength::NoKeyUpdate => "SELECT FOR NO KEY UPDATE",
3758 LockStrength::Share => "SELECT FOR SHARE",
3759 LockStrength::KeyShare => "SELECT FOR KEY SHARE",
3760 }),
3761
3762 // ---- changes the catalog -----------------------------------
3763 Self::CreateTable { .. } => Some("CREATE TABLE"),
3764 Self::DropTable { .. } => Some("DROP TABLE"),
3765 Self::AlterTable { .. } => Some("ALTER TABLE"),
3766 Self::CreateIndex { .. } => Some("CREATE INDEX"),
3767 Self::DropIndex { .. } => Some("DROP INDEX"),
3768 Self::AlterIndex { .. } => Some("ALTER INDEX"),
3769 Self::CreateView { .. } => Some("CREATE VIEW"),
3770 Self::DropView { .. } => Some("DROP VIEW"),
3771 Self::CreateMaterializedView { .. } => Some("CREATE MATERIALIZED VIEW"),
3772 Self::RefreshMaterializedView { .. } => Some("REFRESH MATERIALIZED VIEW"),
3773 Self::DropMaterializedView { .. } => Some("DROP MATERIALIZED VIEW"),
3774 Self::CreateSequence { .. } => Some("CREATE SEQUENCE"),
3775 Self::AlterSequence { .. } => Some("ALTER SEQUENCE"),
3776 Self::DropSequence { .. } => Some("DROP SEQUENCE"),
3777 Self::CreateType { .. } => Some("CREATE TYPE"),
3778 Self::DropType { .. } => Some("DROP TYPE"),
3779 Self::AlterTypeAddValue { .. } | Self::AlterTypeRenameValue { .. } => {
3780 Some("ALTER TYPE")
3781 }
3782 Self::CreateDomain { .. } => Some("CREATE DOMAIN"),
3783 Self::AlterDomain { .. } => Some("ALTER DOMAIN"),
3784 Self::DropDomain { .. } => Some("DROP DOMAIN"),
3785 Self::CreateSchema { .. } => Some("CREATE SCHEMA"),
3786 Self::DropSchema { .. } => Some("DROP SCHEMA"),
3787 Self::CreateFunction { .. } => Some("CREATE FUNCTION"),
3788 Self::DropFunction { .. } => Some("DROP FUNCTION"),
3789 Self::CreateTrigger { .. } => Some("CREATE TRIGGER"),
3790 Self::DropTrigger { .. } => Some("DROP TRIGGER"),
3791 Self::CreateRule { .. } => Some("CREATE RULE"),
3792 Self::DropRule { .. } => Some("DROP RULE"),
3793 Self::CreateExtension { .. } => Some("CREATE EXTENSION"),
3794 Self::CreateStatistics { .. } => Some("CREATE STATISTICS"),
3795 Self::DropStatistics { .. } => Some("DROP STATISTICS"),
3796 Self::DropAggregate { .. } => Some("DROP AGGREGATE"),
3797 Self::CommentOn { .. } => Some("COMMENT"),
3798 Self::DropDatabase { .. } => Some("DROP DATABASE"),
3799 Self::CreatePublication { .. } => Some("CREATE PUBLICATION"),
3800 Self::DropPublication { .. } => Some("DROP PUBLICATION"),
3801 Self::CreateSubscription { .. } => Some("CREATE SUBSCRIPTION"),
3802 Self::DropSubscription { .. } => Some("DROP SUBSCRIPTION"),
3803
3804 // ---- changes roles / permissions ---------------------------
3805 Self::CreateUser { .. } => Some("CREATE ROLE"),
3806 Self::DropUser { .. } => Some("DROP ROLE"),
3807 Self::AlterRolePassword { .. } | Self::SetDbRoleSetting { .. } => Some("ALTER ROLE"),
3808 Self::Grant { .. } => Some("GRANT"),
3809 Self::Revoke { .. } => Some("REVOKE"),
3810 Self::CreatePolicy { .. } => Some("CREATE POLICY"),
3811 Self::AlterPolicy { .. } => Some("ALTER POLICY"),
3812 Self::DropPolicy { .. } => Some("DROP POLICY"),
3813 Self::AlterSystem { .. } => Some("ALTER SYSTEM"),
3814
3815 // ---- SPG's own writers -------------------------------------
3816 // Rewrites cold-tier segments on disk. PG has no equivalent to
3817 // ask, so the test is what it does, not what it is called.
3818 Self::CompactColdSegments => Some("COMPACT COLD SEGMENTS"),
3819
3820 // ---- allowed -----------------------------------------------
3821 // Reads, transaction control, session state, cursors, and the
3822 // maintenance statements PG itself permits. `REINDEX` really is
3823 // allowed in a read-only transaction (measured), which is why
3824 // `Maintain` is here.
3825 //
3826 // `Prepare`, `Execute`, `Call` and `DoBlock` are allowed at
3827 // this level for the reason PG allows them: the write inside
3828 // is refused when it runs, by this same check. Measured:
3829 // `PREPARE p AS INSERT …` succeeds; `DO $$ … INSERT … $$`
3830 // fails with `cannot execute INSERT`.
3831 Self::Explain { .. }
3832 | Self::CopyTo { .. }
3833 | Self::CopyToFile { .. }
3834 | Self::Analyze { .. }
3835 | Self::Maintain { .. }
3836 | Self::Vacuum { .. }
3837 | Self::Begin { .. }
3838 | Self::Commit
3839 | Self::Rollback
3840 | Self::Savepoint { .. }
3841 | Self::RollbackToSavepoint { .. }
3842 | Self::ReleaseSavepoint { .. }
3843 | Self::PrepareTransaction { .. }
3844 | Self::SetTransaction { .. }
3845 | Self::SetConstraints { .. }
3846 | Self::SetParameter { .. }
3847 | Self::SetParameterList { .. }
3848 | Self::SetUserVars { .. }
3849 | Self::SetRole { .. }
3850 | Self::ResetParameter { .. }
3851 | Self::ShowParameter { .. }
3852 | Self::Discard { .. }
3853 | Self::Prepare { .. }
3854 | Self::Execute { .. }
3855 | Self::Deallocate { .. }
3856 | Self::Call { .. }
3857 | Self::DoBlock { .. }
3858 | Self::DeclareCursor { .. }
3859 | Self::FetchCursor { .. }
3860 | Self::MoveCursor { .. }
3861 | Self::CloseCursor { .. }
3862 | Self::Listen { .. }
3863 | Self::Notify { .. }
3864 | Self::Unlisten { .. }
3865 | Self::Kill { .. }
3866 | Self::WaitForWalPosition { .. }
3867 | Self::ValidateOnly { .. }
3868 | Self::NoOpPreventedInTransaction { .. }
3869 | Self::Empty
3870 | Self::ShowTables
3871 | Self::ShowDatabases
3872 | Self::UseDatabase(_)
3873 | Self::ShowCreateTable { .. }
3874 | Self::ShowIndexes { .. }
3875 | Self::ShowStatus
3876 | Self::ShowVariables
3877 | Self::ShowVariablesLike { .. }
3878 | Self::ShowProcesslist
3879 | Self::ShowColumns { .. }
3880 | Self::ShowUsers
3881 | Self::ShowPublications
3882 | Self::ShowSubscriptions => None,
3883 }
3884 }
3885}
3886
3887#[derive(Debug, Clone, PartialEq, Eq)]
3888pub struct LockingClause {
3889 pub strength: LockStrength,
3890 /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3891 pub of_tables: Vec<String>,
3892 pub policy: LockWait,
3893}
3894
3895/// PG's four tuple-lock strengths, weakest first.
3896#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3897pub enum LockStrength {
3898 KeyShare,
3899 Share,
3900 NoKeyUpdate,
3901 Update,
3902}
3903
3904/// What to do when the row is already locked.
3905#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3906pub enum LockWait {
3907 /// Block until it is free — PG's default.
3908 #[default]
3909 Wait,
3910 /// `NOWAIT` — fail the statement with 55P03.
3911 NoWait,
3912 /// `SKIP LOCKED` — leave the row out of the result.
3913 SkipLocked,
3914}
3915
3916#[derive(Debug, Clone, PartialEq, Default)]
3917pub struct SelectStatement {
3918 /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3919 /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3920 /// whole syntax and locked nothing: two workers running the classic
3921 /// `SKIP LOCKED` queue take both took the same row.
3922 /// v7.39 (round 305) — boxed. A locking clause appears on a
3923 /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3924 /// cost every `SelectStatement` 32 bytes, and this struct sits in
3925 /// recursive evaluation frames where the engine already runs close to
3926 /// its stack budget (a 512 KB depth guard is the canary).
3927 pub locking: Option<alloc::boxed::Box<LockingClause>>,
3928 /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3929 /// expressions, materialised once at query start before the
3930 /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3931 /// only — no `WITH RECURSIVE` for v4.x.
3932 pub ctes: Vec<Cte>,
3933 pub distinct: bool,
3934 /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3935 /// keep the first row (per ORDER BY) of each group the
3936 /// expressions define. Empty = no DISTINCT ON.
3937 pub distinct_on: Vec<Expr>,
3938 pub items: Vec<SelectItem>,
3939 pub from: Option<FromClause>,
3940 pub where_: Option<Expr>,
3941 pub group_by: Option<Vec<Expr>>,
3942 /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3943 /// expands `group_by` to every non-aggregate SELECT-list item
3944 /// before the executor runs. Mutually exclusive with an
3945 /// explicit `group_by` list (the parser sets exactly one).
3946 pub group_by_all: bool,
3947 /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3948 /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3949 /// aggregate executor resolves them through the same synthetic
3950 /// schema used for the SELECT items.
3951 pub having: Option<Expr>,
3952 /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3953 /// itself a `SelectStatement` with `order_by = None` and `limit =
3954 /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3955 /// top of the chain).
3956 pub unions: Vec<(UnionKind, SelectStatement)>,
3957 /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3958 /// Keys are matched left-to-right: first key decides, ties break
3959 /// to the second, etc.
3960 pub order_by: Vec<OrderBy>,
3961 /// `LIMIT <n>` — bound on row output. `n` is an integer
3962 /// literal **or** (v7.9.24) a placeholder `$N` resolved
3963 /// against the prepared-statement Bind values. mailrs
3964 /// migration follow-up H2.
3965 pub limit: Option<LimitExpr>,
3966 /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3967 /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3968 pub offset: Option<LimitExpr>,
3969 /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3970 /// (SQL:2008). When true and an ORDER BY is present, the
3971 /// executor extends past the LIMIT-truncated tail to include
3972 /// every row whose ORDER BY key equals the last-kept row's
3973 /// key. Requires an ORDER BY; the executor errors otherwise
3974 /// (matching PG's `WITH TIES` rule). The parser was already
3975 /// accepting `WITH TIES` since Phase 5.1; this field captures
3976 /// the choice so the executor can act on it.
3977 pub limit_with_ties: bool,
3978 /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3979 /// that NOTHING referenced. PG analyses every definition whether
3980 /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3981 /// and silently succeeded here — the referenced ones get their columns
3982 /// resolved through the WindowFunction nodes they were inlined into,
3983 /// and the unreferenced ones used to be dropped at parse, unexamined.
3984 /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3985 ///
3986 /// Not part of `Display`: an unreferenced definition has no effect on
3987 /// the result, so a deparsed body (a stored view) omits it.
3988 pub window_check_exprs: Vec<Expr>,
3989}
3990
3991impl Expr {
3992 /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3993 /// directly inside this expression to `f`. `f` receives each nested
3994 /// statement once; descending further (into that statement's own
3995 /// clauses) is the caller's job, which keeps this walk finite and
3996 /// lets the caller order the recursion.
3997 ///
3998 /// The match is deliberately **wildcard-free**: a new `Expr` variant
3999 /// does not compile until it says whether it can carry a subquery.
4000 /// The row-count resolution pass is built on this, and a shape it
4001 /// silently failed to visit would leave a `LimitExpr::Expr` behind —
4002 /// which every row-count reader would take as "no limit", i.e. the
4003 /// whole table. Compile-time exhaustiveness is what rules that out.
4004 /// Iterative on purpose. Expression trees here get deep (long
4005 /// boolean chains, big IN lists), and this walk is on the path of
4006 /// every statement; recursing would add a frame per node to a stack
4007 /// budget the engine already runs close to — a depth guard that runs
4008 /// on a deliberately small stack caught exactly that. Depth costs
4009 /// heap here instead.
4010 pub fn for_each_subquery_mut<E>(
4011 &mut self,
4012 f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
4013 ) -> Result<(), E> {
4014 let mut stack: Vec<&mut Self> = alloc::vec![self];
4015 while let Some(e) = stack.pop() {
4016 match e {
4017 Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
4018 Self::NamedArg { expr, .. }
4019 | Self::Collate { expr, .. }
4020 | Self::Variadic(expr)
4021 | Self::Unary { expr, .. }
4022 | Self::Cast { expr, .. }
4023 | Self::FieldAccess { base: expr, .. }
4024 | Self::IsNull { expr, .. }
4025 | Self::BoolTest { expr, .. }
4026 | Self::Extract { source: expr, .. } => stack.push(expr),
4027 Self::Binary { lhs, rhs, .. } => {
4028 stack.push(lhs);
4029 stack.push(rhs);
4030 }
4031 Self::Like { expr, pattern, .. } => {
4032 stack.push(expr);
4033 stack.push(pattern);
4034 }
4035 Self::ArraySubscript { target, index } => {
4036 stack.push(target);
4037 stack.push(index);
4038 }
4039 Self::ArraySlice { target, lo, hi } => {
4040 stack.push(target);
4041 stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
4042 }
4043 Self::AnyAll { expr, array, .. } => {
4044 stack.push(expr);
4045 stack.push(array);
4046 }
4047 Self::FunctionCall { args, .. } | Self::Array(args) => {
4048 stack.extend(args.iter_mut());
4049 }
4050 Self::AggregateOrdered {
4051 call,
4052 order_by,
4053 filter,
4054 ..
4055 } => {
4056 stack.push(call);
4057 stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
4058 stack.extend(filter.iter_mut().map(|b| &mut **b));
4059 }
4060 Self::WindowFunction {
4061 args,
4062 partition_by,
4063 order_by,
4064 filter,
4065 ..
4066 } => {
4067 // `frame` bounds hold folded numbers / interval
4068 // parts, never expressions — nothing to visit there.
4069 stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
4070 stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
4071 stack.extend(filter.iter_mut().map(|b| &mut **b));
4072 }
4073 Self::InList { expr, list, .. } => {
4074 stack.push(expr);
4075 stack.extend(list.iter_mut());
4076 }
4077 Self::Case {
4078 operand,
4079 branches,
4080 else_branch,
4081 } => {
4082 stack.extend(
4083 operand
4084 .iter_mut()
4085 .chain(else_branch.iter_mut())
4086 .map(|b| &mut **b),
4087 );
4088 for (when, then) in branches.iter_mut() {
4089 stack.push(when);
4090 stack.push(then);
4091 }
4092 }
4093 Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
4094 Self::InSubquery { expr, subquery, .. } => {
4095 stack.push(expr);
4096 f(subquery)?;
4097 }
4098 Self::RowInSubquery { row, subquery, .. }
4099 | Self::RowCmpSubquery { row, subquery, .. } => {
4100 stack.extend(row.iter_mut());
4101 f(subquery)?;
4102 }
4103 }
4104 }
4105 Ok(())
4106 }
4107
4108 /// The shared twin of [`Self::for_each_subquery_mut`], for analysis
4109 /// that reads a statement rather than rewriting it.
4110 ///
4111 /// Same wildcard-free match, same iterative walk. Rust cannot write
4112 /// one body generic over `&`/`&mut`, so the two must be edited
4113 /// together; exhaustiveness is what makes that a compile error
4114 /// rather than a silent gap in one of them.
4115 ///
4116 /// # Errors
4117 /// Whatever `f` returns.
4118 pub fn for_each_subquery<'a, E>(
4119 &'a self,
4120 f: &mut impl FnMut(&'a SelectStatement) -> Result<(), E>,
4121 ) -> Result<(), E> {
4122 let mut stack: Vec<&'a Self> = alloc::vec![self];
4123 while let Some(e) = stack.pop() {
4124 match e {
4125 Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
4126 Self::NamedArg { expr, .. }
4127 | Self::Collate { expr, .. }
4128 | Self::Variadic(expr)
4129 | Self::Unary { expr, .. }
4130 | Self::Cast { expr, .. }
4131 | Self::FieldAccess { base: expr, .. }
4132 | Self::IsNull { expr, .. }
4133 | Self::BoolTest { expr, .. }
4134 | Self::Extract { source: expr, .. } => stack.push(expr),
4135 Self::Binary { lhs, rhs, .. } => {
4136 stack.push(lhs);
4137 stack.push(rhs);
4138 }
4139 Self::Like { expr, pattern, .. } => {
4140 stack.push(expr);
4141 stack.push(pattern);
4142 }
4143 Self::ArraySubscript { target, index } => {
4144 stack.push(target);
4145 stack.push(index);
4146 }
4147 Self::ArraySlice { target, lo, hi } => {
4148 stack.push(target);
4149 stack.extend(lo.iter().chain(hi.iter()).map(|b| &**b));
4150 }
4151 Self::AnyAll { expr, array, .. } => {
4152 stack.push(expr);
4153 stack.push(array);
4154 }
4155 Self::FunctionCall { args, .. } | Self::Array(args) => {
4156 stack.extend(args.iter());
4157 }
4158 Self::AggregateOrdered {
4159 call,
4160 order_by,
4161 filter,
4162 ..
4163 } => {
4164 stack.push(call);
4165 stack.extend(order_by.iter().map(|o| &o.expr));
4166 stack.extend(filter.iter().map(|b| &**b));
4167 }
4168 Self::WindowFunction {
4169 args,
4170 partition_by,
4171 order_by,
4172 filter,
4173 ..
4174 } => {
4175 stack.extend(args.iter().chain(partition_by.iter()));
4176 stack.extend(order_by.iter().map(|(e, _, _)| e));
4177 stack.extend(filter.iter().map(|b| &**b));
4178 }
4179 Self::InList { expr, list, .. } => {
4180 stack.push(expr);
4181 stack.extend(list.iter());
4182 }
4183 Self::Case {
4184 operand,
4185 branches,
4186 else_branch,
4187 } => {
4188 stack.extend(operand.iter().chain(else_branch.iter()).map(|b| &**b));
4189 for (when, then) in branches {
4190 stack.push(when);
4191 stack.push(then);
4192 }
4193 }
4194 Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
4195 Self::InSubquery { expr, subquery, .. } => {
4196 stack.push(expr);
4197 f(subquery)?;
4198 }
4199 Self::RowInSubquery { row, subquery, .. }
4200 | Self::RowCmpSubquery { row, subquery, .. } => {
4201 stack.extend(row.iter());
4202 f(subquery)?;
4203 }
4204 }
4205 }
4206 Ok(())
4207 }
4208}
4209
4210/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
4211/// time or a placeholder `$N` resolved during extended-query
4212/// Bind. mailrs migration follow-up H2.
4213///
4214/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
4215/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
4216/// made the compiler point at every site that used to duplicate a
4217/// row-count out of the AST, which is exactly the set that must not
4218/// bypass the resolution pre-pass.
4219#[derive(Debug, Clone, PartialEq)]
4220pub enum LimitExpr {
4221 /// `LIMIT 10` — value known at parse time.
4222 Literal(u32),
4223 /// `LIMIT $N` — the 1-based parameter index, resolved against
4224 /// the bind values when the prepared statement executes.
4225 Placeholder(u16),
4226 /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
4227 /// greatest(2,3)`: a row-count expression that isn't constant, so
4228 /// it can't be folded at parse time. Evaluated once, before
4229 /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
4230 /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
4231 /// "no limit"). **No execution path may see this variant** —
4232 /// `as_literal` would report `None`, which every row-count reader
4233 /// takes to mean "unlimited", i.e. the whole table.
4234 Expr(alloc::boxed::Box<Expr>),
4235}
4236
4237impl fmt::Display for LimitExpr {
4238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4239 match self {
4240 Self::Literal(n) => write!(f, "{n}"),
4241 Self::Placeholder(n) => write!(f, "${n}"),
4242 // Parenthesised so the round-trip text re-parses as one
4243 // row-count expression (`LIMIT (SELECT 4)`), which is also
4244 // the only spelling `FETCH FIRST` accepts.
4245 Self::Expr(e) => write!(f, "({e})"),
4246 }
4247 }
4248}
4249
4250impl LimitExpr {
4251 /// Convenience for the simple-query path where no placeholders
4252 /// can possibly exist. Returns the literal value or `None` if
4253 /// this is a placeholder (caller must surface as Unsupported).
4254 ///
4255 /// v7.39 (round 305) — `None` is read by every row-count consumer as
4256 /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
4257 /// therefore silently return the whole table, so the engine's
4258 /// `resolve_limit_exprs` pre-pass rewrites the variant away before
4259 /// dispatch. The assertion makes a missed nesting site fail loudly
4260 /// in every test build rather than quietly widening a result set.
4261 #[must_use]
4262 pub fn as_literal(&self) -> Option<u32> {
4263 match self {
4264 Self::Literal(n) => Some(*n),
4265 Self::Placeholder(_) => None,
4266 Self::Expr(_) => {
4267 debug_assert!(
4268 false,
4269 "LimitExpr::Expr reached execution — resolve_limit_exprs \
4270 missed a nesting site; treating it as `no limit` would \
4271 return every row"
4272 );
4273 None
4274 }
4275 }
4276 }
4277}
4278
4279/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
4280/// the engine's `substitute_placeholders` pass these are
4281/// always Literal; in the simple-query path a Placeholder
4282/// shape returns None (executor surfaces as
4283/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
4284impl SelectStatement {
4285 #[must_use]
4286 pub fn limit_literal(&self) -> Option<u32> {
4287 self.limit.as_ref().and_then(LimitExpr::as_literal)
4288 }
4289 #[must_use]
4290 pub fn offset_literal(&self) -> Option<u32> {
4291 self.offset.as_ref().and_then(LimitExpr::as_literal)
4292 }
4293}
4294
4295#[derive(Debug, Clone, PartialEq)]
4296pub struct Cte {
4297 pub name: String,
4298 /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
4299 /// classical case) or a data-modifying statement
4300 /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
4301 /// CTE semantics. The modifying body's RETURNING projection
4302 /// becomes the materialised CTE table the outer query can
4303 /// reference; the modifying statement runs once before the
4304 /// outer query, within the same transaction.
4305 pub body: CteBody,
4306 /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
4307 /// RECURSIVE keyword. Applies to every CTE in the clause per
4308 /// PG semantics. A non-recursive body in a RECURSIVE WITH is
4309 /// allowed; the engine just runs it once.
4310 pub recursive: bool,
4311 /// v4.22: optional `WITH name(a, b, c)` column-name list. When
4312 /// non-empty, these override the body's output column names
4313 /// position-by-position; the engine errors out if the count
4314 /// doesn't match the body's projection width.
4315 pub column_overrides: Vec<String>,
4316 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
4317 /// SET seqcol` on a recursive CTE. Desugared at parse time into an
4318 /// extra ordering column on the body (see `rewrite_search_and_cycle`).
4319 pub search: Option<SearchClause>,
4320 /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
4321 /// USING pathcol` cycle detection, desugared at parse time.
4322 pub cycle: Option<CycleClause>,
4323}
4324
4325/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
4326#[derive(Debug, Clone, PartialEq)]
4327pub struct SearchClause {
4328 /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
4329 pub depth_first: bool,
4330 /// The CTE output columns the search orders by.
4331 pub by_columns: Vec<String>,
4332 /// The new column holding the ordering key (a row-array for depth,
4333 /// a `(depth, keys…)` row for breadth).
4334 pub set_column: String,
4335}
4336
4337/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
4338#[derive(Debug, Clone, PartialEq)]
4339pub struct CycleClause {
4340 /// Columns whose repetition along a path marks a cycle.
4341 pub columns: Vec<String>,
4342 /// The new boolean-ish column set to `mark_value` on a cycle.
4343 pub mark_column: String,
4344 /// Value written to `mark_column` when a cycle is detected (default
4345 /// `true`); `default_value` otherwise. PG allows any type; SPG carries
4346 /// them as literals.
4347 pub mark_value: Option<Literal>,
4348 pub default_value: Option<Literal>,
4349 /// The new column accumulating the visited-row path array.
4350 pub path_column: String,
4351}
4352
4353/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
4354/// (Insert / Update / Delete with optional RETURNING). The
4355/// data-modifying variants must carry a RETURNING projection for the
4356/// outer query to reference the CTE alias by; an empty RETURNING is
4357/// only valid if no outer reference materialises (rare — typically
4358/// caught at planning).
4359#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
4360#[derive(Debug, Clone, PartialEq)]
4361pub enum CteBody {
4362 Select(SelectStatement),
4363 Insert(Box<InsertStatement>),
4364 Update(Box<UpdateStatement>),
4365 Delete(Box<DeleteStatement>),
4366 /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
4367 /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
4368 Merge(Box<MergeStatement>),
4369}
4370
4371impl CteBody {
4372 /// Convenience accessor used by classical (read-only) CTE
4373 /// callsites that still expect a SELECT body. Returns None for
4374 /// data-modifying CTEs; callers must explicitly route those
4375 /// through `exec_with_ctes`'s modifying branch.
4376 #[must_use]
4377 pub fn as_select(&self) -> Option<&SelectStatement> {
4378 match self {
4379 Self::Select(s) => Some(s),
4380 _ => None,
4381 }
4382 }
4383
4384 #[must_use]
4385 pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
4386 match self {
4387 Self::Select(s) => Some(s),
4388 _ => None,
4389 }
4390 }
4391
4392 #[must_use]
4393 pub fn is_modifying(&self) -> bool {
4394 !matches!(self, Self::Select(_))
4395 }
4396}
4397
4398#[derive(Debug, Clone, PartialEq)]
4399pub struct OrderBy {
4400 pub expr: Expr,
4401 /// `false` = ASC (default), `true` = DESC.
4402 pub desc: bool,
4403 /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
4404 /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
4405 /// NULLS FIRST for DESC); the engine resolves the effective
4406 /// value via `nulls_first.unwrap_or(desc)`.
4407 pub nulls_first: Option<bool>,
4408 /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
4409 /// It lives here rather than in the expression for the same reason
4410 /// `desc` does: at an ORDER BY key a collation is ordering
4411 /// information, and nothing downstream of the sort needs it. A new
4412 /// `Expr` variant would instead put a new arm on `eval_expr`, which
4413 /// this repo has measured to overflow the debug stack.
4414 ///
4415 /// `None` means none was written, and the key falls back to whatever
4416 /// its COLUMN declares — which is every key that existed before this.
4417 pub collation: Option<String>,
4418}
4419
4420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4421pub enum UnionKind {
4422 /// `UNION` — dedupes the combined set.
4423 Distinct,
4424 /// `UNION ALL` — concatenates without dedup.
4425 All,
4426 /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
4427 /// present on both sides.
4428 Intersect,
4429 /// `INTERSECT ALL` — multiset intersection (min per-row count).
4430 IntersectAll,
4431 /// `EXCEPT` — distinct left rows absent from the right.
4432 Except,
4433 /// `EXCEPT ALL` — multiset subtraction.
4434 ExceptAll,
4435}
4436
4437#[derive(Debug, Clone, PartialEq)]
4438pub enum SelectItem {
4439 Wildcard,
4440 /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
4441 /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
4442 /// `NEW` pseudo-relation).
4443 QualifiedWildcard(String),
4444 Expr {
4445 expr: Expr,
4446 alias: Option<String>,
4447 },
4448}
4449
4450#[derive(Debug, Clone, PartialEq)]
4451pub struct TableRef {
4452 pub name: String,
4453 pub alias: Option<String>,
4454 /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
4455 /// children.
4456 ///
4457 /// The keyword used to be absorbed at parse time, on the reasoning
4458 /// that SPG's inheritance children are separate relations a plain
4459 /// scan does not descend into — so ONLY already described what the
4460 /// scan did. That stopped being true when a partition parent
4461 /// started unioning its children: measured, `SELECT count(*) FROM
4462 /// ONLY <partitioned parent>` answered 2 where PG answers 0.
4463 pub only: bool,
4464 /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
4465 /// When `Some(id)`, the scan restricts to rows that live in
4466 /// segment `<id>` only — useful for forensic inspection of a
4467 /// specific freezer-emitted segment without exposing the hot
4468 /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
4469 /// is STABILITY carve-out for v6.10 — needs the freezer to
4470 /// stamp each segment with a wall-clock at creation time.
4471 pub as_of_segment: Option<u32>,
4472 /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
4473 /// source. When `Some`, `name` is the alias (defaulting to
4474 /// `"unnest"` when no `AS` is given) and the engine builds a
4475 /// synthetic single-column table by evaluating the expression
4476 /// once at SELECT entry. Each TEXT[] element becomes one row;
4477 /// NULL elements become NULL cells. v7.11 supported
4478 /// uncorrelated UNNEST only as the FROM primary; v7.13.2
4479 /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
4480 /// position (cross-join with regular tables).
4481 pub unnest_expr: Option<Box<Expr>>,
4482 /// v7.13.2 — mailrs round-6 S5. PG-standard
4483 /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
4484 /// when non-empty, the first entry overrides the projected
4485 /// column name for the unnested column. Empty = fall back to
4486 /// the table alias (pre-v7.13.2 behaviour).
4487 pub unnest_column_aliases: Vec<String>,
4488 /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
4489 /// row-stream gains a trailing BIGINT column counting rows
4490 /// from 1 in element order. PG names it `ordinality`; a second
4491 /// entry in the column-alias list renames it.
4492 pub with_ordinality: bool,
4493 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4494 /// [, step])` set-returning source. When `Some`, the engine
4495 /// materialises a single-column virtual table by stepping
4496 /// `start` to `stop` inclusive. Args are the literal arg list
4497 /// (2 for default-step, 3 for explicit-step). Supports:
4498 /// * SmallInt / Int / BigInt with integer step (default = 1)
4499 /// * Timestamp with INTERVAL step (PG date-range pattern)
4500 /// Mutually exclusive with `unnest_expr` — both populate the
4501 /// same downstream dispatch slot. `name` defaults to
4502 /// `"generate_series"` when no alias is provided.
4503 pub generate_series_args: Option<Vec<Expr>>,
4504 /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
4505 /// table. When `Some`, the TableRef is a parenthesised SELECT
4506 /// that may reference columns from the preceding FROM items
4507 /// (correlated derived table). The executor materialises the
4508 /// subquery per left-row, substituting outer-column references
4509 /// against the current join row's values before running the
4510 /// inner SELECT, then cross-joins the result back.
4511 /// Mutually exclusive with `name` / `unnest_expr` /
4512 /// `generate_series_args`.
4513 pub lateral_subquery: Option<Box<SelectStatement>>,
4514 /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
4515 /// function as a FROM item. PG semantics: for each key/value
4516 /// pair in the JSONB object argument, emit one (key TEXT,
4517 /// value TEXT) row. When prefixed by `LATERAL` and joined via
4518 /// `CROSS JOIN LATERAL`, the argument may reference columns
4519 /// from a preceding FROM item, in which case the executor
4520 /// evaluates `<expr>` per outer row.
4521 /// Mutually exclusive with `unnest_expr` / `generate_series_args`
4522 /// / `lateral_subquery`. The optional `LATERAL` keyword does not
4523 /// require a separate flag — the executor evaluates per-row
4524 /// whenever the join sits in a JoinKind context.
4525 ///
4526 /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4527 /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4528 /// `json_each` / `json_each_text`) so the executor picks the
4529 /// value-column rendering (JSON text vs unwrapped text).
4530 pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4531 /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4532 /// function channel: `(lowercase fn name, args)`. Carries
4533 /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4534 /// dispatches by name.
4535 pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4536 /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4537 /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4538 /// reference to it yields the value, not a one-field composite
4539 /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4540 /// desugared shape is indistinguishable from a hand-written
4541 /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4542 /// only the parser knows which one it built, so it says so here.
4543 pub scalar_fn_item: bool,
4544 /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4545 /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4546 /// target-list SRFs follow — see round 67). The array-returning family keeps
4547 /// its own lowering; this channel carries the ones that have no array form
4548 /// (`generate_series`, a user `RETURNS SETOF` function).
4549 pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4550 /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4551 /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4552 /// tables (implicit LATERAL, like every SRF channel). Executed by
4553 /// walking the row path over the parsed doc, then each column's
4554 /// path per row-item; NESTED expands as a per-parent outer join.
4555 pub json_table: Option<Box<JsonTable>>,
4556}
4557
4558/// What a FROM item IS.
4559///
4560/// v7.40.10 — a boolean cannot make a consumer handle a new kind; an
4561/// enum can. Every consumer that must know the difference writes a
4562/// `match` with no wildcard arm, so a variant added here is a compile
4563/// error at each of them rather than a defect at whichever one the new
4564/// shape reaches first.
4565///
4566/// The engine asked "is this item synthesised?" in fifty-six places and
4567/// exactly one of them listed every field. The rest were missing
4568/// between one and five, and the gaps were reachable:
4569/// `SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)` answered
4570/// `relation "jsonb_each_text" does not exist` over the extended
4571/// protocol because one such list named four of seven.
4572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4573pub enum FromItemKind {
4574 /// A table, view or CTE named in the catalog.
4575 Relation,
4576 /// `unnest(...)` and the set-returning functions the parser lowers
4577 /// onto the same slot (`string_to_table`, `jsonb_object_keys`, …).
4578 Unnest,
4579 /// `generate_series(...)`.
4580 GenerateSeries,
4581 /// A derived table or LATERAL subquery.
4582 Subquery,
4583 /// `jsonb_each(...)` / `jsonb_each_text(...)`.
4584 JsonbEach,
4585 /// A table function call the parser kept as a name plus arguments.
4586 TableFn,
4587 /// `ROWS FROM (...)`.
4588 RowsFrom,
4589 /// `JSON_TABLE(...)`.
4590 JsonTable,
4591 /// A scalar function in FROM position, which yields one row.
4592 ScalarFn,
4593}
4594
4595/// One walkable slot of a FROM item: an expression, or a nested SELECT.
4596///
4597/// v7.40.10 — see [`TableRef::try_for_each_slot_mut`].
4598#[derive(Debug)]
4599pub enum FromSlot<'a> {
4600 Expr(&'a mut Expr),
4601 Select(&'a mut SelectStatement),
4602}
4603
4604/// The shared half of [`FromSlot`].
4605///
4606/// v7.40.11 — analysis reads a statement it must not modify, and Rust
4607/// has no way to write one walk that is generic over `&`/`&mut`. The two
4608/// bodies are the same destructure and must move together; the
4609/// destructures are total, so a new slot fails to compile in both.
4610#[derive(Debug)]
4611pub enum FromSlotRef<'a> {
4612 Expr(&'a Expr),
4613 Select(&'a SelectStatement),
4614}
4615
4616impl TableRef {
4617 /// Whether this FROM item NAMES A RELATION — a table, view or CTE —
4618 /// rather than producing its own rows.
4619 ///
4620 /// **A total destructure, no `..`.** A field added to `TableRef` is a
4621 /// compile error here.
4622 ///
4623 /// v7.40.10, on evidence. This question is asked in 56 places across
4624 /// the engine and every one of them wrote its own list of fields.
4625 /// Exactly one was complete. The others were missing between one and
4626 /// five slots each, and each gap is a defect waiting for the shape
4627 /// that reaches it:
4628 ///
4629 /// ```text
4630 /// try_stream_single_table's guard named four of seven, so
4631 /// SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)
4632 /// ERROR: relation "jsonb_each_text" does not exist
4633 /// over the extended protocol, while count(*) over the same item
4634 /// answered and the simple query protocol answered.
4635 /// ```
4636 ///
4637 /// `scalar_fn_item` counts as not-a-relation for the same reason the
4638 /// rest do: the row comes from the item, not from the catalog.
4639 #[must_use]
4640 pub fn names_a_relation(&self) -> bool {
4641 self.kind() == FromItemKind::Relation
4642 }
4643
4644 /// What this FROM item is.
4645 ///
4646 /// **A total destructure, no `..`.** A field added to `TableRef` is
4647 /// a compile error here — which is the point, because every
4648 /// consumer matches exhaustively on the result.
4649 ///
4650 /// The slots are mutually exclusive by construction: the parser
4651 /// fills exactly one of them, or none for a plain relation.
4652 #[must_use]
4653 pub fn kind(&self) -> FromItemKind {
4654 let Self {
4655 name: _,
4656 alias: _,
4657 only: _,
4658 as_of_segment: _,
4659 unnest_column_aliases: _,
4660 with_ordinality: _,
4661 scalar_fn_item,
4662 unnest_expr,
4663 generate_series_args,
4664 lateral_subquery,
4665 jsonb_each_text_arg,
4666 table_fn_call,
4667 rows_from,
4668 json_table,
4669 } = self;
4670 if unnest_expr.is_some() {
4671 FromItemKind::Unnest
4672 } else if generate_series_args.is_some() {
4673 FromItemKind::GenerateSeries
4674 } else if lateral_subquery.is_some() {
4675 FromItemKind::Subquery
4676 } else if jsonb_each_text_arg.is_some() {
4677 FromItemKind::JsonbEach
4678 } else if table_fn_call.is_some() {
4679 FromItemKind::TableFn
4680 } else if rows_from.is_some() {
4681 FromItemKind::RowsFrom
4682 } else if json_table.is_some() {
4683 FromItemKind::JsonTable
4684 } else if *scalar_fn_item {
4685 FromItemKind::ScalarFn
4686 } else {
4687 FromItemKind::Relation
4688 }
4689 }
4690
4691 /// Every expression this FROM item carries, and the SELECT nested in
4692 /// it — in one place, for every pass that needs them.
4693 ///
4694 /// **Written as a TOTAL destructure, with no `..`.** A field added
4695 /// to `TableRef` is a compile error here, rather than a defect in
4696 /// each pass that enumerated the slots for itself. That is the whole
4697 /// point of the function existing.
4698 ///
4699 /// v7.40.10, on evidence. `TableRef` carries seven expression slots
4700 /// and three separate passes each knew a different subset of them.
4701 /// In one day: the parameter-substitution walk knew only
4702 /// `lateral_subquery`, so `unnest($1)` reached execution still
4703 /// holding a placeholder (a customer's live 500); `describe` knew
4704 /// only `unnest_expr`, so `generate_series(…)` described no columns
4705 /// and a driver got a protocol error; and the LIMIT/OFFSET
4706 /// resolution knew CTEs and UNION peers but not a FROM subquery, so
4707 /// `LIMIT $n` inside a derived table returned every row.
4708 ///
4709 /// Fixing those three one at a time left four slots unvisited.
4710 /// Measured after the third fix shipped, all on the same message:
4711 ///
4712 /// ```text
4713 /// jsonb_each_text($1) parameter $1 referenced but only 0 bound
4714 /// ROWS FROM (…$1…) parameter $1 referenced but only 0 bound
4715 /// json_table($1, …) parameter $1 referenced but only 0 bound
4716 /// ```
4717 ///
4718 /// Those were the next three reports. This is what stops the fourth.
4719 ///
4720 /// # Errors
4721 /// Whatever the callbacks return; the walk stops at the first.
4722 pub fn try_for_each_slot_mut<E>(
4723 &mut self,
4724 visit: &mut dyn FnMut(FromSlot<'_>) -> Result<(), E>,
4725 ) -> Result<(), E> {
4726 // One callback rather than two, so a caller that needs the same
4727 // state for both — every caller so far — does not have to lend
4728 // it twice.
4729 let Self {
4730 // Not expressions — named so the destructure stays total.
4731 name: _,
4732 alias: _,
4733 only: _,
4734 as_of_segment: _,
4735 unnest_column_aliases: _,
4736 with_ordinality: _,
4737 scalar_fn_item: _,
4738 // The seven that carry something to walk.
4739 unnest_expr,
4740 generate_series_args,
4741 lateral_subquery,
4742 jsonb_each_text_arg,
4743 table_fn_call,
4744 rows_from,
4745 json_table,
4746 } = self;
4747 if let Some(e) = unnest_expr {
4748 visit(FromSlot::Expr(e))?;
4749 }
4750 if let Some(args) = generate_series_args {
4751 for a in args.iter_mut() {
4752 visit(FromSlot::Expr(a))?;
4753 }
4754 }
4755 if let Some(sub) = lateral_subquery {
4756 visit(FromSlot::Select(sub))?;
4757 }
4758 if let Some((_, e)) = jsonb_each_text_arg {
4759 visit(FromSlot::Expr(e))?;
4760 }
4761 if let Some(call) = table_fn_call {
4762 for a in call.1.iter_mut() {
4763 visit(FromSlot::Expr(a))?;
4764 }
4765 }
4766 if let Some(items) = rows_from {
4767 for (_, args) in items.iter_mut() {
4768 for a in args.iter_mut() {
4769 visit(FromSlot::Expr(a))?;
4770 }
4771 }
4772 }
4773 if let Some(jt) = json_table {
4774 let JsonTable {
4775 doc,
4776 row_path: _,
4777 columns: _,
4778 passing,
4779 } = jt.as_mut();
4780 visit(FromSlot::Expr(doc))?;
4781 for (_, e) in passing.iter_mut() {
4782 visit(FromSlot::Expr(e))?;
4783 }
4784 }
4785 Ok(())
4786 }
4787
4788 /// The shared twin of [`Self::try_for_each_slot_mut`], for analysis
4789 /// that reads a statement rather than rewriting it. Same total
4790 /// destructure — a new slot is a compile error in both.
4791 ///
4792 /// # Errors
4793 /// Whatever `visit` returns.
4794 pub fn try_for_each_slot<'a, E>(
4795 &'a self,
4796 visit: &mut dyn FnMut(FromSlotRef<'a>) -> Result<(), E>,
4797 ) -> Result<(), E> {
4798 let Self {
4799 name: _,
4800 alias: _,
4801 only: _,
4802 as_of_segment: _,
4803 unnest_column_aliases: _,
4804 with_ordinality: _,
4805 scalar_fn_item: _,
4806 unnest_expr,
4807 generate_series_args,
4808 lateral_subquery,
4809 jsonb_each_text_arg,
4810 table_fn_call,
4811 rows_from,
4812 json_table,
4813 } = self;
4814 if let Some(e) = unnest_expr {
4815 visit(FromSlotRef::Expr(e))?;
4816 }
4817 if let Some(args) = generate_series_args {
4818 for a in args {
4819 visit(FromSlotRef::Expr(a))?;
4820 }
4821 }
4822 if let Some(sub) = lateral_subquery {
4823 visit(FromSlotRef::Select(sub))?;
4824 }
4825 if let Some((_, e)) = jsonb_each_text_arg {
4826 visit(FromSlotRef::Expr(e))?;
4827 }
4828 if let Some(call) = table_fn_call {
4829 for a in &call.1 {
4830 visit(FromSlotRef::Expr(a))?;
4831 }
4832 }
4833 if let Some(items) = rows_from {
4834 for (_, args) in items {
4835 for a in args {
4836 visit(FromSlotRef::Expr(a))?;
4837 }
4838 }
4839 }
4840 if let Some(jt) = json_table {
4841 let JsonTable {
4842 doc,
4843 row_path: _,
4844 columns: _,
4845 passing,
4846 } = jt.as_ref();
4847 visit(FromSlotRef::Expr(doc))?;
4848 for (_, e) in passing {
4849 visit(FromSlotRef::Expr(e))?;
4850 }
4851 }
4852 Ok(())
4853 }
4854}
4855
4856/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4857#[derive(Debug, Clone, PartialEq)]
4858pub struct JsonTable {
4859 /// The document expression (jsonb/json/text). May reference outer
4860 /// columns → implicit LATERAL.
4861 pub doc: Box<Expr>,
4862 /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4863 /// match is one row's context item.
4864 pub row_path: String,
4865 /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4866 pub columns: Vec<JsonTableColumn>,
4867 /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4868 pub passing: Vec<(String, Expr)>,
4869}
4870
4871/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4872#[derive(Debug, Clone, PartialEq)]
4873pub enum JsonTableColumn {
4874 /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4875 Ordinality { name: String },
4876 /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4877 /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4878 /// `<name> <type> EXISTS [PATH '<p>']`.
4879 Regular {
4880 name: String,
4881 ty: ColumnTypeName,
4882 /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4883 path: String,
4884 /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4885 exists: bool,
4886 /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4887 format_json: bool,
4888 /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4889 wrapper: bool,
4890 /// Behaviour when the path matches nothing (default NULL).
4891 on_empty: JsonTableOnBehavior,
4892 /// Behaviour when coercion fails (default NULL).
4893 on_error: JsonTableOnBehavior,
4894 },
4895 /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4896 /// row like a LEFT JOIN (a parent with no nested match still emits one
4897 /// row, nested cols NULL).
4898 Nested {
4899 path: String,
4900 columns: Vec<JsonTableColumn>,
4901 },
4902}
4903
4904/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4905#[derive(Debug, Clone, PartialEq)]
4906pub enum JsonTableOnBehavior {
4907 /// Default: the column value is NULL.
4908 Null,
4909 /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4910 Error,
4911 /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4912 Default(Box<Expr>),
4913}
4914
4915/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4916/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4917/// joins evaluate left-associatively in nested-loop order.
4918#[derive(Debug, Clone, PartialEq)]
4919pub struct FromClause {
4920 pub primary: TableRef,
4921 pub joins: Vec<FromJoin>,
4922}
4923
4924#[derive(Debug, Clone, PartialEq)]
4925pub struct FromJoin {
4926 pub kind: JoinKind,
4927 pub table: TableRef,
4928 /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4929 pub on: Option<Expr>,
4930 /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4931 /// USING column list so the executor can perform PG's column-merge
4932 /// (the join columns collapse to a single unqualified output column,
4933 /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4934 /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4935 /// USING into an equivalent `on` predicate so the join filter/count
4936 /// path works unchanged; `using_cols` drives only the output-shape
4937 /// rewrite. Empty/`None` for `ON` and CROSS joins.
4938 pub using_cols: Option<Vec<String>>,
4939 /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4940 /// column names are not known until the table schemas are available
4941 /// (parse time is schema-less), so the parser only sets this flag and
4942 /// leaves `on`/`using_cols` empty; the engine resolves the common
4943 /// columns at execution time, synthesises the `on` predicate + the
4944 /// USING column-merge, and clears the flag. If there are no common
4945 /// columns PG treats it as a CROSS join.
4946 pub natural: bool,
4947}
4948
4949#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4950pub enum JoinKind {
4951 Inner,
4952 Left,
4953 Cross,
4954 /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4955 /// NULL-filling the left (drive) columns on unmatched right rows.
4956 /// The executor runs the LEFT algorithm's mirror: it tracks which
4957 /// peer rows matched and emits the unmatched ones with a NULL-left
4958 /// tuple after the probe loop. Output column order is unchanged
4959 /// (left-table cols then right-table cols).
4960 Right,
4961 /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4962 /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4963 FullOuter,
4964 /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4965 /// once, paired with the first peer row that satisfies the ON. Not
4966 /// reachable from SQL — the EXISTS pull-up emits it, which is what
4967 /// frees positive EXISTS from the round-721 uniqueness gate (an
4968 /// INNER join would multiply the outer rows; a semi join cannot).
4969 Semi,
4970}
4971
4972#[derive(Debug, Clone, PartialEq)]
4973pub enum Expr {
4974 Literal(Literal),
4975 /// v7.39.2 — `<expr> COLLATE <name>`: the collation this expression
4976 /// compares under, whatever the column or the database says.
4977 ///
4978 /// The parser used to refuse the locale names in this position and
4979 /// SILENTLY ABSORB the byte-order ones, so `'a' COLLATE "C" < 'B'`
4980 /// answered `t` where PostgreSQL 18.6 answers `f` — the one family
4981 /// it let through is the one where dropping it changes the answer.
4982 ///
4983 /// Whether dropping is safe depends on the DATABASE's own collation,
4984 /// which the parser cannot see: under `SPG_LC_COLLATE=C` absorbing
4985 /// `COLLATE "C"` is exactly right. So the name rides along and the
4986 /// engine, which knows, decides.
4987 Collate {
4988 expr: Box<Expr>,
4989 collation: String,
4990 },
4991 Column(ColumnName),
4992 /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4993 /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4994 /// callee's declared parameter names, and a user function's live in the
4995 /// catalog — which the parser cannot see. So the name rides along in the
4996 /// tree and the evaluator, which has the catalog, does the reordering.
4997 /// Appears only inside a `FunctionCall`'s argument list.
4998 NamedArg {
4999 name: String,
5000 expr: Box<Expr>,
5001 },
5002 /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
5003 /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
5004 /// expression evaluates to an array whose elements the evaluator splices
5005 /// into the call as individual trailing arguments. Appears only inside a
5006 /// `FunctionCall`'s argument list.
5007 Variadic(Box<Expr>),
5008 /// v6.1.1 — `$N` parameter placeholder for the extended query
5009 /// protocol. The number is 1-based per PostgreSQL convention.
5010 /// Evaluation looks up `params[N-1]` from the prepared-statement
5011 /// bind buffer; out-of-range indices raise a runtime error
5012 /// (same shape as a column-not-found miss).
5013 Placeholder(u16),
5014 Binary {
5015 lhs: Box<Expr>,
5016 op: BinOp,
5017 rhs: Box<Expr>,
5018 },
5019 Unary {
5020 op: UnOp,
5021 expr: Box<Expr>,
5022 },
5023 /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
5024 /// TEXT, BOOL targets; engine coerces at evaluation time.
5025 Cast {
5026 expr: Box<Expr>,
5027 target: CastTarget,
5028 },
5029 /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
5030 /// evaluates to a composite/record value (an explicit `ROW(...)`, a
5031 /// whole-row reference, or a composite-returning function); `field` names
5032 /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
5033 /// column names for a whole-row). Only the parenthesised form reaches
5034 /// here — a bare `a.b` is parsed as a qualified column reference.
5035 FieldAccess {
5036 base: Box<Expr>,
5037 field: String,
5038 },
5039 /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
5040 IsNull {
5041 expr: Box<Expr>,
5042 negated: bool,
5043 },
5044 /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
5045 /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
5046 /// `Some(false)` for FALSE and `None` for UNKNOWN.
5047 ///
5048 /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
5049 /// The semantics were right, but the AST then had no way to say what
5050 /// the user wrote, so every renderer printed the lowering:
5051 /// `CHECK ((a > 1) IS TRUE)` came back as
5052 /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
5053 /// dumped view lost the form too.
5054 BoolTest {
5055 expr: Box<Expr>,
5056 value: Option<bool>,
5057 negated: bool,
5058 },
5059 /// Function call `name(args...)`. v1.4 supports a small built-in set
5060 /// (length, upper, lower, abs, coalesce); unknown names error at eval
5061 /// time so the parser stays open for v1.5 aggregates.
5062 FunctionCall {
5063 name: String,
5064 args: Vec<Expr>,
5065 },
5066 /// v7.24 (mailrs round-16 A) — an aggregate call with an
5067 /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
5068 /// Wraps the plain [`Expr::FunctionCall`] so every existing
5069 /// FunctionCall consumer stays untouched; only the aggregate
5070 /// executor (and the expression walkers) know the wrapper.
5071 /// Non-aggregate evaluation contexts reject it at eval time.
5072 AggregateOrdered {
5073 call: Box<Expr>,
5074 order_by: Vec<OrderBy>,
5075 /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
5076 /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
5077 /// aggregate modifier so plain FunctionCall stays untouched.
5078 distinct: bool,
5079 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
5080 /// Only the rows where `cond` is true contribute to this
5081 /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
5082 /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
5083 /// END)`, which is faithful for NULL-ignoring aggregates but
5084 /// WRONG for `array_agg` (it would collect a NULL per excluded
5085 /// row). The executor instead skips excluded rows before
5086 /// accumulation, which is correct for every aggregate.
5087 filter: Option<Box<Expr>>,
5088 },
5089 /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
5090 /// wildcards are `%` (any run) and `_` (one char), backslash escapes
5091 /// the next char (so `\%` matches a literal `%`).
5092 Like {
5093 expr: Box<Expr>,
5094 pattern: Box<Expr>,
5095 negated: bool,
5096 /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
5097 /// match. PG folds both operands.
5098 case_insensitive: bool,
5099 },
5100 /// v4.12 window function call: `name(args) OVER (PARTITION BY
5101 /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
5102 /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
5103 /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
5104 /// unordered windows and "from start of partition through
5105 /// current row" for ordered windows — no explicit ROWS /
5106 /// RANGE clause in v4.12 MVP.
5107 WindowFunction {
5108 name: String,
5109 args: Vec<Expr>,
5110 partition_by: Vec<Expr>,
5111 /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
5112 /// (None = PG default, same contract as [`OrderBy`]).
5113 order_by: Vec<(
5114 Expr,
5115 bool, /* desc */
5116 Option<bool>, /* nulls_first */
5117 )>,
5118 /// v4.20 explicit frame. `None` means "use the default":
5119 /// whole-partition when unordered, running aggregate from
5120 /// partition start through current row when ordered.
5121 frame: Option<WindowFrame>,
5122 /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
5123 /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
5124 /// `Respect` (PG / ANSI default — NULLs participate). Other
5125 /// window functions ignore this flag.
5126 null_treatment: NullTreatment,
5127 /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
5128 /// = no FILTER. Only aggregate window functions honor it; the
5129 /// predicate restricts which peer rows contribute within the frame.
5130 filter: Option<Box<Expr>>,
5131 },
5132 /// v4.10 scalar subquery — `(SELECT ...)` used in expression
5133 /// position. Must return exactly one row × one column at eval
5134 /// time; the engine errors out otherwise. Uncorrelated only —
5135 /// the inner SELECT cannot reference outer columns.
5136 ScalarSubquery(Box<SelectStatement>),
5137 /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
5138 /// projection is ignored; only row-count matters.
5139 Exists {
5140 subquery: Box<SelectStatement>,
5141 negated: bool,
5142 },
5143 /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
5144 /// project exactly one column; membership is tested by Eq
5145 /// against each row's value (NULL handling follows ANSI:
5146 /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
5147 InSubquery {
5148 expr: Box<Expr>,
5149 subquery: Box<SelectStatement>,
5150 negated: bool,
5151 },
5152 /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
5153 /// against a multi-column subquery. Row comparisons against a *list*
5154 /// decompose to OR-of-AND at parse time, but the subquery form can't
5155 /// (its rows are only known at runtime), so this survives as its own
5156 /// node evaluated with PG's row-comparison three-valued logic.
5157 RowInSubquery {
5158 row: Vec<Expr>,
5159 subquery: Box<SelectStatement>,
5160 negated: bool,
5161 },
5162 /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
5163 /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
5164 /// RowInSubquery, the literal-RHS form decomposes at parse time but the
5165 /// subquery form can't, so it survives as its own node. The subquery
5166 /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
5167 RowCmpSubquery {
5168 row: Vec<Expr>,
5169 op: BinOp,
5170 subquery: Box<SelectStatement>,
5171 },
5172 /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
5173 /// list. Both the parser's literal-list path and the engine's
5174 /// IN-subquery materialisation used to desugar into a left-deep
5175 /// OR-Eq chain, so expression depth scaled with the element count
5176 /// — a 24k-row subquery result overflowed the 2 MiB worker stack
5177 /// (recursive eval AND recursive Box drop) and aborted embedding
5178 /// host processes. The flat node keeps depth constant: eval is an
5179 /// iterative scan with PG three-valued logic, drop is a Vec drop.
5180 InList {
5181 expr: Box<Expr>,
5182 list: Vec<Expr>,
5183 negated: bool,
5184 },
5185 /// `EXTRACT(<field> FROM <source>)` — pull an integer component
5186 /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
5187 /// because the `FROM` keyword is what separates the two halves,
5188 /// not a comma.
5189 Extract {
5190 field: ExtractField,
5191 source: Box<Expr>,
5192 },
5193 /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
5194 /// element is evaluated independently; NULLs are allowed.
5195 /// v7.10 supports only single-dimension TEXT[] semantically;
5196 /// non-text elements coerce at engine evaluation time when
5197 /// the surrounding context (column type / cast) makes the
5198 /// target clear.
5199 Array(Vec<Expr>),
5200 /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
5201 /// engine returns NULL for out-of-range indices.
5202 ArraySubscript {
5203 target: Box<Expr>,
5204 index: Box<Expr>,
5205 },
5206 /// Array slice `arr[lo:hi]` — PG 1-based, both ends
5207 /// inclusive; a missing bound extends to that end of the
5208 /// array and out-of-range bounds clamp. Returns an array of
5209 /// the same element type.
5210 ArraySlice {
5211 target: Box<Expr>,
5212 lo: Option<Box<Expr>>,
5213 hi: Option<Box<Expr>>,
5214 },
5215 /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
5216 /// operator is the comparison binary op (Eq / Ne / Lt / …);
5217 /// the engine desugars: `ANY` returns true if any element
5218 /// satisfies; `ALL` returns true only if every element does.
5219 /// NULL handling follows PG's three-valued logic.
5220 AnyAll {
5221 expr: Box<Expr>,
5222 op: BinOp,
5223 array: Box<Expr>,
5224 /// `true` = ANY, `false` = ALL.
5225 is_any: bool,
5226 },
5227 /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
5228 /// (searched form, `operand` is None) and
5229 /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
5230 /// `operand` is the lead expression compared against each
5231 /// branch's match). Each `(when_expr, then_expr)` branch
5232 /// stays as written; engine short-circuits on the first match.
5233 /// `else_branch` is `None` when no ELSE; evaluates to NULL.
5234 /// mailrs round-5 G9.
5235 Case {
5236 operand: Option<Box<Expr>>,
5237 branches: Vec<(Expr, Expr)>,
5238 else_branch: Option<Box<Expr>>,
5239 },
5240}
5241
5242/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
5243/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
5244/// in the offset walk. `Ignore` causes the function to skip NULL
5245/// values in the argument expression, returning the next non-NULL.
5246#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5247#[non_exhaustive]
5248pub enum NullTreatment {
5249 #[default]
5250 Respect,
5251 Ignore,
5252}
5253
5254/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
5255/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
5256/// where end implicitly = CURRENT ROW.
5257#[derive(Debug, Clone, PartialEq, Eq)]
5258pub struct WindowFrame {
5259 pub kind: FrameKind,
5260 pub start: FrameBound,
5261 pub end: Option<FrameBound>,
5262 /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
5263 /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
5264 /// no-op; CURRENT ROW drops the current row from the frame.
5265 pub exclude: FrameExclusion,
5266}
5267
5268#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5269pub enum FrameExclusion {
5270 /// Default — exclude nothing.
5271 #[default]
5272 NoOthers,
5273 /// Drop the current row from the frame.
5274 CurrentRow,
5275 /// Drop the current row's whole peer group.
5276 Group,
5277 /// Drop the current row's peers but keep the current row.
5278 Ties,
5279}
5280
5281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5282pub enum FrameKind {
5283 Rows,
5284 Range,
5285 /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
5286 /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
5287 /// bounds (no explicit integer offsets) GROUPS behaves identically
5288 /// to RANGE — both consult the peer-group of the current row.
5289 /// Integer offsets are not yet supported; the executor rejects
5290 /// them at run time.
5291 Groups,
5292}
5293
5294#[derive(Debug, Clone, PartialEq, Eq)]
5295pub enum FrameBound {
5296 UnboundedPreceding,
5297 OffsetPreceding(u64),
5298 CurrentRow,
5299 OffsetFollowing(u64),
5300 UnboundedFollowing,
5301 /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
5302 /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
5303 /// interval is folded to its (months, days, micros) components at
5304 /// parse time.
5305 IntervalPreceding {
5306 months: i32,
5307 days: i32,
5308 micros: i64,
5309 },
5310 IntervalFollowing {
5311 months: i32,
5312 days: i32,
5313 micros: i64,
5314 },
5315}
5316
5317impl fmt::Display for FrameBound {
5318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5319 match self {
5320 Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
5321 Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
5322 Self::CurrentRow => f.write_str("CURRENT ROW"),
5323 Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
5324 Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
5325 Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
5326 Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
5327 }
5328 }
5329}
5330
5331#[derive(Debug, Clone, PartialEq, Eq)]
5332pub enum ExtractField {
5333 Year,
5334 Month,
5335 Day,
5336 Hour,
5337 Minute,
5338 Second,
5339 Microsecond,
5340 /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
5341 /// SPG keeps the integer convention — truncated seconds).
5342 Epoch,
5343 /// Day of week, 0 = Sunday … 6 = Saturday.
5344 Dow,
5345 /// ISO day of week, 1 = Monday … 7 = Sunday.
5346 Isodow,
5347 /// Day of year, 1-366.
5348 Doy,
5349 /// ISO 8601 week number, 1-53.
5350 Week,
5351 /// ISO 8601 week-numbering year (pairs with `Week`).
5352 Isoyear,
5353 /// Quarter, 1-4.
5354 Quarter,
5355 /// Year divided by 10 (floor).
5356 Decade,
5357 /// Century — 2001-2100 is century 21.
5358 Century,
5359 /// Millennium — 2001-3000 is millennium 3.
5360 Millennium,
5361 /// Julian day number (truncated for timestamps).
5362 Julian,
5363 /// Seconds and fraction in milliseconds (ss·1000 + frac).
5364 Millisecond,
5365 /// UTC offset in seconds — SPG sessions run UTC, so 0.
5366 Timezone,
5367 /// Hour component of the UTC offset — 0.
5368 TimezoneHour,
5369 /// Minute component of the UTC offset — 0.
5370 TimezoneMinute,
5371 /// v7.39 (round 253) — a field name the parser does not know. PG
5372 /// resolves EXTRACT fields at RUNTIME and reports them with the
5373 /// source type (`unit "nosuch" not recognized for type timestamp
5374 /// without time zone`, 22023), so the parser carries the raw name
5375 /// instead of rejecting.
5376 Other(String),
5377}
5378
5379impl fmt::Display for ExtractField {
5380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5381 f.write_str(match self {
5382 Self::Year => "YEAR",
5383 Self::Month => "MONTH",
5384 Self::Day => "DAY",
5385 Self::Hour => "HOUR",
5386 Self::Minute => "MINUTE",
5387 Self::Second => "SECOND",
5388 Self::Microsecond => "MICROSECOND",
5389 Self::Epoch => "EPOCH",
5390 Self::Dow => "DOW",
5391 Self::Isodow => "ISODOW",
5392 Self::Doy => "DOY",
5393 Self::Week => "WEEK",
5394 Self::Isoyear => "ISOYEAR",
5395 Self::Quarter => "QUARTER",
5396 Self::Decade => "DECADE",
5397 Self::Century => "CENTURY",
5398 Self::Millennium => "MILLENNIUM",
5399 Self::Julian => "JULIAN",
5400 Self::Millisecond => "MILLISECOND",
5401 Self::Timezone => "TIMEZONE",
5402 Self::TimezoneHour => "TIMEZONE_HOUR",
5403 Self::TimezoneMinute => "TIMEZONE_MINUTE",
5404 Self::Other(name) => return f.write_str(name),
5405 })
5406 }
5407}
5408
5409#[derive(Debug, Clone, PartialEq, Eq)]
5410pub enum CastTarget {
5411 Int,
5412 BigInt,
5413 Float,
5414 Text,
5415 Bool,
5416 Vector,
5417 Date,
5418 Timestamp,
5419 /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
5420 /// H3a. Engine reuses the existing runtime-interval / timestamp
5421 /// paths (parse the text input, return the matching Value).
5422 Interval,
5423 Timestamptz,
5424 /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
5425 /// types (v7.9.0); the cast just routes Text→Json with the
5426 /// requested OID for the wire layer.
5427 Json,
5428 Jsonb,
5429 /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
5430 /// compatibility; engine surfaces as Unsupported with a
5431 /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
5432 RegType,
5433 RegClass,
5434 /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
5435 /// the PG external array form `{a,b,NULL}`.
5436 TextArray,
5437 /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
5438 /// `{1,2,3}` or widens a `TextArray` whose elements are
5439 /// integer-shaped.
5440 IntArray,
5441 BigIntArray,
5442 /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
5443 /// external form text representation. Used by pg_dump output
5444 /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
5445 TsVector,
5446 TsQuery,
5447 /// v7.17.0 — `::uuid`. Decodes the LHS Text via
5448 /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
5449 /// unhyphenated, uppercase, and brace-wrapped forms); malformed
5450 /// input is a SQL error.
5451 Uuid,
5452 /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
5453 /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
5454 /// inputs pass through unchanged. Closes the mailrs D-pre #3
5455 /// reverse-acceptance gap — anywhere a PG schema writes
5456 /// `expr::bytea`, SPG now matches.
5457 Bytea,
5458 /// v7.37.5 ship triage — generic cast target for the long tail
5459 /// of PG type names the parser meets in `expr::TYPE` shapes that
5460 /// don't deserve their own enum variant. The engine routes these
5461 /// through `column_type_to_data_type` + the existing typed
5462 /// `coerce_value` dispatch, so adding a new PG type to SPG
5463 /// implicitly adds its cast-target form too — no parser change
5464 /// per type. The string carries the lowercase PG type ident
5465 /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
5466 /// a clear message when the type isn't known.
5467 Named(String),
5468}
5469
5470impl fmt::Display for CastTarget {
5471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5472 f.write_str(match self {
5473 Self::Int => "int",
5474 Self::BigInt => "bigint",
5475 Self::Float => "float",
5476 Self::Text => "text",
5477 Self::Bool => "bool",
5478 Self::Vector => "vector",
5479 Self::Interval => "interval",
5480 Self::Timestamptz => "timestamptz",
5481 Self::Json => "json",
5482 Self::Jsonb => "jsonb",
5483 Self::RegType => "regtype",
5484 Self::RegClass => "regclass",
5485 Self::Date => "date",
5486 Self::Timestamp => "timestamp",
5487 Self::TextArray => "TEXT[]",
5488 Self::IntArray => "INT[]",
5489 Self::BigIntArray => "BIGINT[]",
5490 Self::TsVector => "tsvector",
5491 Self::TsQuery => "tsquery",
5492 Self::Uuid => "uuid",
5493 Self::Bytea => "bytea",
5494 // v7.37.5 — `Self::Named` carries its own canonical name.
5495 Self::Named(name) => return f.write_str(name),
5496 })
5497 }
5498}
5499
5500#[derive(Debug, Clone, PartialEq)]
5501pub enum Literal {
5502 Integer(i64),
5503 Float(f64),
5504 /// Exact decimal literal — a bare `12.34`-style token, kept as
5505 /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
5506 /// before it becomes a `Value::Numeric`. PG parses such literals as
5507 /// `numeric`, not `double precision`. (Scientific/huge literals stay
5508 /// `Float`.)
5509 Numeric {
5510 unscaled: i128,
5511 /// v7.39 (round 271) — widened to u16. At u8 a literal with more
5512 /// than 255 decimal places could not be represented, and the
5513 /// conversion's `.expect("lexer-validated decimal")` aborted the
5514 /// query with an internal error on SQL PG accepts.
5515 scale: u16,
5516 },
5517 /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
5518 /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
5519 /// `Value::NumericBig` at eval; previously such literals fell back to double.
5520 NumericBig(String),
5521 String(String),
5522 /// v7.38.8 — a temporal constant that has already been decoded.
5523 ///
5524 /// Without these the only way to carry one through the AST was as
5525 /// text, and a predicate comparing a `timestamp` column against a
5526 /// literal then coerced that text back into a timestamp ONCE PER
5527 /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
5528 /// profile. `constfold` produced text for the same reason: its exit
5529 /// had nothing else to hand back.
5530 ///
5531 /// `text` keeps the spelling so `Display` round-trips byte for byte,
5532 /// the way `Interval` already does and for the same reason: this
5533 /// node is printed in EXPLAIN, in dumps and in error messages, and
5534 /// none of those should change because the value stopped being
5535 /// carried as a string. The enum already holds a `String` and an
5536 /// `i128`, so neither variant widens it.
5537 Timestamp {
5538 micros: i64,
5539 text: String,
5540 },
5541 /// Days since the epoch `Value::Date` counts from. See
5542 /// [`Literal::Timestamp`].
5543 Date {
5544 days: i32,
5545 text: String,
5546 },
5547 Bool(bool),
5548 Null,
5549 /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
5550 Vector(Vec<f32>),
5551 /// TEXT[] value carried through the prepared-bind path
5552 /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
5553 /// text form, so the array rides the AST natively).
5554 TextArray(Vec<Option<String>>),
5555 /// INT[] value carried through the prepared-bind path.
5556 IntArray(Vec<Option<i32>>),
5557 /// BIGINT[] value carried through the prepared-bind path.
5558 BigIntArray(Vec<Option<i64>>),
5559 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
5560 /// Three independent dimensions: `months` (variable-length;
5561 /// year/month), `days` (fixed 86400 seconds at non-DST, but
5562 /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
5563 /// stays distinguishable), and `micros` (sub-day; can carry).
5564 /// `text` keeps the original spelling so Display round-trips
5565 /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
5566 Interval {
5567 months: i32,
5568 days: i32,
5569 micros: i64,
5570 text: String,
5571 },
5572}
5573
5574#[derive(Debug, Clone, PartialEq, Eq)]
5575pub struct ColumnName {
5576 pub qualifier: Option<String>,
5577 pub name: String,
5578}
5579
5580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5581pub enum BinOp {
5582 Or,
5583 And,
5584 Eq,
5585 NotEq,
5586 /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
5587 /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
5588 /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
5589 /// non-NULL behaviour matches `<>` / `=` exactly. Common in
5590 /// PG-style JOIN ON predicates and pg_dump output.
5591 IsDistinctFrom,
5592 IsNotDistinctFrom,
5593 /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
5594 /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
5595 /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
5596 /// is a real division (round 351).
5597 IntDiv,
5598 Lt,
5599 LtEq,
5600 Gt,
5601 GtEq,
5602 Add,
5603 Sub,
5604 Mul,
5605 Div,
5606 /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
5607 /// precedence as Mul/Div; result type follows left operand.
5608 Mod,
5609 /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
5610 /// operands of equal dimension; engine returns `Value::Float(d)`.
5611 L2Distance,
5612 /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
5613 GeomParallel,
5614 /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
5615 OverLeft,
5616 OverRight,
5617 /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
5618 GeomPerp,
5619 /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
5620 GeomSameAs,
5621 /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
5622 /// object to the left-hand one.
5623 ClosestPoint,
5624 /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
5625 GeomHoriz,
5626 /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
5627 /// more similar" remains true (matches pgvector's published convention).
5628 InnerProduct,
5629 /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
5630 CosineDistance,
5631 /// SQL string concatenation `||`. NULL propagates.
5632 Concat,
5633 /// Bitwise OR `|` on integers.
5634 BitOr,
5635 /// Bitwise AND `&` on integers.
5636 BitAnd,
5637 /// Bitwise XOR `#` on integers and equal-length bit strings.
5638 BitXor,
5639 /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
5640 /// sides as truth values and returns their exclusive-or (`1 XOR 0`
5641 /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
5642 /// MySQL dialect produces it; PG has no logical XOR. Its precedence
5643 /// sits between OR (loosest) and AND.
5644 LogicalXor,
5645 /// v4.14 `json -> key` — element access by string key (object)
5646 /// or integer index (array). Returns a JSON value.
5647 JsonGet,
5648 /// v4.14 `json ->> key` — same access, returns the result as
5649 /// TEXT (unwraps a top-level JSON string; renders other scalars
5650 /// as their canonical text).
5651 JsonGetText,
5652 /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
5653 /// text array literal like `'{a,0,b}'`. Returns JSON.
5654 JsonGetPath,
5655 /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
5656 JsonGetPathText,
5657 /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
5658 /// when every key/value in `sub_json` is structurally present in
5659 /// the left side. Matches PG semantics (top-level + recursive).
5660 JsonContains,
5661 /// `@?` — jsonb path existence (jsonb_path_exists).
5662 JsonPathExists,
5663 /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
5664 /// `a <@ b` is defined as `b @> a` (same semantics, swapped
5665 /// sides). Eval dispatch reuses `JsonContains` with swapped args.
5666 JsonContainedBy,
5667 /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
5668 /// returns BOOL. For an object, true if `key` is an existing
5669 /// member name; for an array, true if any element is the string
5670 /// `key` (PG semantics).
5671 JsonKeyExists,
5672 /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
5673 /// returns BOOL.
5674 JsonKeysAny,
5675 /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
5676 /// returns BOOL.
5677 JsonKeysAll,
5678 /// `jsonb #- path_text[]` — delete the value at a nested path.
5679 /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
5680 JsonDeletePath,
5681 /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
5682 /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
5683 /// tsvector` and engine eval normalises either ordering.
5684 TsMatch,
5685 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
5686 /// `<<`. LHS network is strictly inside RHS network (no equality).
5687 InetContainedBy,
5688 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
5689 /// `<<=`. LHS network ⊆ RHS network.
5690 InetContainedByEq,
5691 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
5692 /// LHS network strictly contains RHS network.
5693 InetContains,
5694 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
5695 /// LHS network ⊇ RHS network.
5696 InetContainsEq,
5697 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
5698 /// True iff either network contains any address of the other.
5699 InetOverlap,
5700 /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
5701 /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
5702 Intersects,
5703 /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
5704 /// (point, box).
5705 IsBelow,
5706 IsAbove,
5707 /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
5708 /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
5709 /// where `'A' < 'a'` is false under a non-C collation, which is the
5710 /// whole reason the operator family exists — it is what makes a LIKE
5711 /// prefix index-usable. pg_dump writes these into index definitions.
5712 PatternLt,
5713 PatternLtEq,
5714 PatternGt,
5715 PatternGtEq,
5716}
5717
5718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5719pub enum UnOp {
5720 Not,
5721 Neg,
5722 /// Bitwise NOT `~` on integers.
5723 BitNot,
5724 /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
5725 /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
5726 /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
5727 /// while PG18 and MariaDB accept every one of them.
5728 ///
5729 /// It is not a no-op to drop at parse time — PG refuses it on
5730 /// non-numeric operands ("operator does not exist: + boolean"), so the
5731 /// operand's type has to be seen at eval.
5732 Plus,
5733}
5734
5735// --- Display impls (round-trip-safe) --------------------------------------
5736
5737impl Statement {
5738 /// v7.18 — classify whether the statement is read-only at
5739 /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
5740 /// route SELECT-shaped traffic through the fan-out
5741 /// `AsyncReadHandle` (no writer-lock contention) while
5742 /// keeping DML / DDL / TX-control on the single-writer path.
5743 ///
5744 /// The classification matches what
5745 /// `Engine::execute_readonly_with_cancel` accepts: anything
5746 /// that does NOT mutate catalog, statistics, session state,
5747 /// or transaction state. WaitForWalPosition is included
5748 /// (engine returns `Unsupported`, but the classification is
5749 /// semantically read-only — no mutation). Empty is excluded
5750 /// out of an abundance of caution — the no-op routes
5751 /// through the writer so any future side effect lands
5752 /// uniformly.
5753 ///
5754 /// **Not connection-state aware**. `SET LOCAL` / `RESET`
5755 /// affect session parameters and must run on the writer
5756 /// engine that owns the session state; they classify as
5757 /// writer-path here. Same for `BEGIN` / `COMMIT` /
5758 /// `ROLLBACK` / `SAVEPOINT` — transaction control is
5759 /// always writer-path.
5760 /// v7.39 (round 435) — does this statement implicitly COMMIT an open
5761 /// transaction under MySQL?
5762 ///
5763 /// PG runs DDL inside the transaction; MySQL commits before (and after)
5764 /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
5765 /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
5766 /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
5767 /// nested START TRANSACTION; and measured NOT to fire for `CREATE
5768 /// TEMPORARY TABLE`, `SET`, or a SELECT.
5769 ///
5770 /// A positive list, not "everything that is not DML": a statement
5771 /// wrongly listed here commits a client's data early, which is as bad as
5772 /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
5773 /// COMPACT) are left out — a MySQL session never sends them.
5774 #[must_use]
5775 pub fn mysql_implicit_commit(&self) -> bool {
5776 match self {
5777 // MySQL's documented exception, measured on MariaDB 11: a
5778 // TEMPORARY table is not DDL for this purpose and does not
5779 // commit. (Round 435 got this for free because the parser then
5780 // lowered that spelling to `Statement::Empty`; round 436 made it
5781 // a real CREATE TABLE, and the round-435 pin caught it.)
5782 Self::CreateTable(c) => !c.temporary,
5783 // MySQL commits the open transaction and opens a fresh one.
5784 Self::Begin { .. }
5785 | Self::DropTable { .. }
5786 | Self::DropIndex { .. }
5787 | Self::CreateIndex(_)
5788 | Self::AlterIndex { .. }
5789 | Self::AlterTable(_)
5790 | Self::Truncate { .. }
5791 | Self::Analyze { .. }
5792 | Self::CreateStatistics { .. }
5793 | Self::DropStatistics { .. }
5794 | Self::CreateView { .. }
5795 | Self::DropView { .. }
5796 | Self::CreateMaterializedView { .. }
5797 | Self::RefreshMaterializedView { .. }
5798 | Self::DropMaterializedView { .. }
5799 | Self::CreateSequence(_)
5800 | Self::AlterSequence { .. }
5801 | Self::DropSequence { .. }
5802 | Self::CreateFunction(_)
5803 | Self::DropFunction { .. }
5804 | Self::CreateTrigger(_)
5805 | Self::DropTrigger { .. }
5806 | Self::CreateRule(_)
5807 | Self::DropRule { .. }
5808 | Self::CreateType(_)
5809 | Self::DropType { .. }
5810 | Self::AlterTypeAddValue { .. }
5811 | Self::AlterTypeRenameValue { .. }
5812 | Self::CreateDomain(_)
5813 | Self::AlterDomain { .. }
5814 | Self::DropDomain { .. }
5815 | Self::CreateSchema { .. }
5816 | Self::DropSchema { .. }
5817 | Self::CreateUser { .. }
5818 | Self::DropUser { .. }
5819 | Self::Grant { .. }
5820 | Self::Revoke { .. }
5821 | Self::CreatePolicy(_)
5822 | Self::AlterPolicy(_)
5823 | Self::DropPolicy { .. }
5824 | Self::CommentOn { .. }
5825 | Self::CreateExtension { .. } => true,
5826 _ => false,
5827 }
5828 }
5829
5830 #[must_use]
5831 pub fn is_readonly(&self) -> bool {
5832 match self {
5833 Statement::RenameTables(_) => false,
5834 // v7.39 (round 288) — SET CONSTRAINTS changes transaction
5835 // state, and IMMEDIATE can run the deferred checks there and
5836 // then; writer-path.
5837 Statement::SetConstraints { .. } => false,
5838 // v7.39 (round 695) — it writes nothing (SPG has no
5839 // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
5840 // writer and a read-only session refuses it there too.
5841 Statement::AlterSystem { .. } => false,
5842 // Same shape: a no-op here, a writer to PG, so a read-only
5843 // session refuses it as PG's would.
5844 Statement::NoOpPreventedInTransaction { .. } => false,
5845 Statement::DropDatabase { .. } => false,
5846 // v7.39 (round 696) — they perform nothing, so nothing is
5847 // written; PG classes LOCK and the OWNED BY pair as writers and
5848 // a read-only session refuses them there.
5849 Statement::ValidateOnly { .. } => false,
5850 // v7.39 (round 750) — a credential rotation persists.
5851 Statement::AlterRolePassword { .. } => true,
5852 Statement::DropAggregate { .. } => false,
5853 // v7.39 (round 547) — records a GUC default in the catalog.
5854 Statement::SetDbRoleSetting(_) => false,
5855 // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5856 // but they name a relation and PG refuses one that is not
5857 // there, so they are not read-only in the sense this asks.
5858 Statement::Maintain { .. } => false,
5859 // v7.39 (round 277) — the prepared-statement surface is
5860 // session state, like SET; writer-path so it lands on the
5861 // engine that owns the session. EXECUTE may also run a
5862 // write, and its body is only known at execution time.
5863 Statement::Prepare { .. }
5864 | Statement::Execute { .. }
5865 | Statement::Deallocate(_)
5866 | Statement::Call(_)
5867 | Statement::PrepareTransaction(_)
5868 | Statement::CreateStatistics { .. }
5869 | Statement::DropStatistics { .. }
5870 // v7.39 (round 318, V51) — KILL signals another connection;
5871 // it must run on the writer path that owns the registry hook.
5872 | Statement::Kill { .. }
5873 // v7.39 (round 320, V53) — DISCARD throws session state away;
5874 // writer path, like SET / RESET.
5875 | Statement::Discard(_)
5876 // v7.39.2 — `USE <db>` writes session state, the same way
5877 // SET does, and takes the same path.
5878 | Statement::UseDatabase(_) => false,
5879 // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5880 // locks MUTATES the lock table, so it is not a read. Left as
5881 // a read it went to the read-only executor and the locking
5882 // pre-pass never ran at all — the clause was honoured only
5883 // inside an explicit transaction, and silently ignored in
5884 // autocommit, which is where a queue worker runs it.
5885 Statement::Select(s) if s.locking.is_some() => false,
5886 Statement::Select(_)
5887 | Statement::CopyTo { .. }
5888 | Statement::CopyToFile { .. }
5889 | Statement::Explain(_)
5890 | Statement::ShowTables
5891 | Statement::ShowDatabases
5892 | Statement::ShowCreateTable(_)
5893 | Statement::ShowIndexes(_)
5894 | Statement::ShowStatus
5895 | Statement::ShowVariables
5896 | Statement::ShowVariablesLike(_)
5897 | Statement::ShowProcesslist
5898 | Statement::ShowColumns(_)
5899 | Statement::ShowUsers
5900 | Statement::ShowPublications
5901 | Statement::ShowSubscriptions
5902 | Statement::WaitForWalPosition { .. } => true,
5903 // Everything else mutates catalog, statistics,
5904 // session state, or transaction state — writer path.
5905 // Listed explicitly so a new Statement variant fails
5906 // the match exhaustiveness check and forces a
5907 // classification decision at add-site.
5908 Statement::Empty
5909 // v7.39 (round 169) — VACUUM mutates storage (reclaims
5910 // tombstoned versions): writer path.
5911 | Statement::Vacuum { .. }
5912 | Statement::DropTable { .. }
5913 | Statement::DropIndex { .. }
5914 | Statement::CreateTable(_)
5915 | Statement::CreateExtension(_)
5916 | Statement::DoBlock(_)
5917 | Statement::CreateIndex(_)
5918 | Statement::Insert(_)
5919 | Statement::Update(_)
5920 | Statement::Delete(_)
5921 | Statement::Merge(_)
5922 | Statement::Begin(_)
5923 | Statement::Commit
5924 | Statement::Rollback
5925 | Statement::Savepoint(_)
5926 | Statement::RollbackToSavepoint(_)
5927 | Statement::ReleaseSavepoint(_)
5928 | Statement::CreateUser(_)
5929 | Statement::DropUser { .. }
5930 | Statement::SetRole(_)
5931 | Statement::Grant(_)
5932 | Statement::Revoke(_)
5933 | Statement::CreatePolicy(_)
5934 | Statement::AlterPolicy(_)
5935 | Statement::DropPolicy(_)
5936 | Statement::AlterIndex(_)
5937 | Statement::AlterTable(_)
5938 | Statement::CreatePublication(_)
5939 | Statement::DropPublication { .. }
5940 | Statement::CreateSubscription(_)
5941 | Statement::DropSubscription { .. }
5942 | Statement::Analyze(_)
5943 | Statement::Truncate { .. }
5944 | Statement::CompactColdSegments
5945 | Statement::SetParameter { .. }
5946 | Statement::SetParameterList(_)
5947 | Statement::SetUserVars(..)
5948 | Statement::SetTransaction { .. }
5949 | Statement::ShowParameter(_)
5950 | Statement::ResetParameter(_)
5951 | Statement::CreateFunction(_)
5952 | Statement::CreateTrigger(_)
5953 | Statement::DropTrigger { .. }
5954 | Statement::CreateRule(_)
5955 | Statement::DropRule { .. }
5956 | Statement::DropFunction { .. }
5957 | Statement::CreateSequence(_)
5958 | Statement::AlterSequence(_)
5959 | Statement::DropSequence { .. }
5960 | Statement::CreateView(_)
5961 | Statement::DropView { .. }
5962 | Statement::CreateMaterializedView(_)
5963 | Statement::RefreshMaterializedView { .. }
5964 | Statement::DropMaterializedView { .. }
5965 | Statement::CreateType(_)
5966 | Statement::AlterTypeAddValue { .. }
5967 | Statement::AlterTypeRenameValue { .. }
5968 | Statement::CommentOn { .. }
5969 | Statement::DropType { .. }
5970 | Statement::CreateDomain(_)
5971 | Statement::DropDomain { .. }
5972 | Statement::CreateSchema { .. }
5973 | Statement::DropSchema { .. }
5974 // v7.39 (round 218) — cursors mutate per-session cursor state
5975 // (open/position/close) on the writer engine: writer path.
5976 | Statement::DeclareCursor { .. }
5977 | Statement::FetchCursor { .. }
5978 | Statement::MoveCursor { .. }
5979 | Statement::CloseCursor { .. }
5980 // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5981 // state / the notification queue: writer path.
5982 | Statement::Listen(_)
5983 | Statement::Notify { .. }
5984 | Statement::Unlisten(_)
5985 | Statement::CopyFromFile { .. }
5986 | Statement::AlterDomain { .. } => false,
5987 }
5988 }
5989}
5990
5991/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5992/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5993#[derive(Debug, Clone, PartialEq, Eq)]
5994pub struct GrantStatement {
5995 /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5996 /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5997 /// is why they keep the case the user typed.
5998 pub privileges: Vec<GrantPriv>,
5999 /// What the privileges are on.
6000 pub object: GrantObject,
6001 /// The roles granted to / revoked from. An empty string entry = PUBLIC.
6002 pub grantees: Vec<String>,
6003 /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
6004 /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
6005 /// privilege itself).
6006 pub grant_option: bool,
6007}
6008
6009/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
6010/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
6011/// An empty column list means the privilege is table-wide.
6012#[derive(Debug, Clone, PartialEq, Eq)]
6013pub struct GrantPriv {
6014 pub word: String,
6015 pub columns: Vec<String>,
6016}
6017
6018/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
6019/// privileges; every other object class parses and is accepted as a no-op, so
6020/// a pg_dump that grants on schemas / sequences / functions still restores.
6021#[derive(Debug, Clone, PartialEq, Eq)]
6022#[non_exhaustive]
6023pub enum GrantObject {
6024 /// `ON [TABLE] a, b` — the enforced case.
6025 Tables(Vec<String>),
6026 /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
6027 /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
6028 /// granted roles; the grantees are the members.
6029 Roles(Vec<String>),
6030 /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
6031 /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
6032 Sequences(Vec<String>),
6033 /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
6034 Schemas(Vec<String>),
6035 /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
6036 Databases(Vec<String>),
6037 /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
6038 /// (SPG keys functions by name); the argument list parses and is dropped.
6039 Functions(Vec<(String, Option<Vec<String>>)>),
6040 /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
6041 /// every table at GRANT time, exactly like PG.
6042 AllTablesInSchema,
6043 /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
6044 /// message.
6045 Other(String),
6046}
6047
6048impl GrantStatement {
6049 /// Round-trip text. `grant = false` renders the REVOKE form.
6050 fn render(&self, grant: bool) -> alloc::string::String {
6051 use core::fmt::Write as _;
6052 let mut s = alloc::string::String::new();
6053 let privs = if self.privileges.is_empty() {
6054 alloc::string::String::from("ALL")
6055 } else {
6056 let parts: Vec<_> = self
6057 .privileges
6058 .iter()
6059 .map(|p| {
6060 if p.columns.is_empty() {
6061 p.word.clone()
6062 } else {
6063 let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
6064 alloc::format!("{} ({})", p.word, cols.join(", "))
6065 }
6066 })
6067 .collect();
6068 parts.join(", ")
6069 };
6070 let obj = match &self.object {
6071 GrantObject::Tables(t) => {
6072 let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
6073 alloc::format!("TABLE {}", names.join(", "))
6074 }
6075 GrantObject::Roles(r) => {
6076 let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
6077 names.join(", ")
6078 }
6079 GrantObject::Sequences(n) => {
6080 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6081 alloc::format!("SEQUENCE {}", names.join(", "))
6082 }
6083 GrantObject::Schemas(n) => {
6084 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6085 alloc::format!("SCHEMA {}", names.join(", "))
6086 }
6087 GrantObject::Databases(n) => {
6088 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6089 alloc::format!("DATABASE {}", names.join(", "))
6090 }
6091 GrantObject::Functions(n) => {
6092 let names: Vec<_> = n
6093 .iter()
6094 .map(|(name, args)| match args {
6095 Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
6096 None => quote_ident(name),
6097 })
6098 .collect();
6099 alloc::format!("FUNCTION {}", names.join(", "))
6100 }
6101 GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
6102 GrantObject::Other(k) => k.clone(),
6103 };
6104 let who: Vec<_> = self
6105 .grantees
6106 .iter()
6107 .map(|g| {
6108 if g.is_empty() {
6109 "PUBLIC".into()
6110 } else {
6111 quote_ident(g)
6112 }
6113 })
6114 .collect();
6115 if let GrantObject::Roles(_) = &self.object {
6116 let _ = if grant {
6117 write!(s, "GRANT {obj} TO {}", who.join(", "))
6118 } else {
6119 write!(s, "REVOKE {obj} FROM {}", who.join(", "))
6120 };
6121 return s;
6122 }
6123 if grant {
6124 let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
6125 if self.grant_option {
6126 s.push_str(" WITH GRANT OPTION");
6127 }
6128 } else {
6129 s.push_str("REVOKE ");
6130 if self.grant_option {
6131 s.push_str("GRANT OPTION FOR ");
6132 }
6133 let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
6134 }
6135 s
6136 }
6137}
6138
6139impl fmt::Display for Statement {
6140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6141 match self {
6142 Self::Empty => Ok(()),
6143 // v7.39 (round 695) — deparsed the way PG writes it.
6144 // v7.39 (round 696) — never deparsed into a dump (nothing is
6145 // stored), so the shortest faithful spelling of what it was.
6146 Self::DropAggregate { if_exists, items } => {
6147 f.write_str("DROP AGGREGATE ")?;
6148 if *if_exists {
6149 f.write_str("IF EXISTS ")?;
6150 }
6151 for (i, (name, args)) in items.iter().enumerate() {
6152 if i > 0 {
6153 f.write_str(", ")?;
6154 }
6155 match args {
6156 Some(a) => write!(f, "{name}({})", a.join(", "))?,
6157 None => write!(f, "{name}(*)")?,
6158 }
6159 }
6160 Ok(())
6161 }
6162 Self::AlterRolePassword { name, password } => {
6163 write!(f, "ALTER ROLE {}", quote_ident(name))?;
6164 match password {
6165 Some(_) => f.write_str(" PASSWORD '<redacted>'"),
6166 None => f.write_str(" PASSWORD NULL"),
6167 }
6168 }
6169 Self::ValidateOnly { kind, names } => match kind {
6170 ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
6171 ValidateOnlyKind::RoleName => {
6172 write!(f, "DROP OWNED BY {}", names.join(", "))
6173 }
6174 ValidateOnlyKind::SessionAuthorization => {
6175 write!(f, "SET SESSION AUTHORIZATION {}", names.join(", "))
6176 }
6177 ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
6178 ValidateOnlyKind::ExtensionAvailable => {
6179 write!(f, "CREATE EXTENSION {}", names.join(", "))
6180 }
6181 ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
6182 ValidateOnlyKind::CollationName => {
6183 write!(f, "DROP COLLATION {}", names.join(", "))
6184 }
6185 ValidateOnlyKind::TsConfigName => {
6186 write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
6187 }
6188 ValidateOnlyKind::EventTriggerName => {
6189 write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
6190 }
6191 ValidateOnlyKind::TablespaceName => {
6192 write!(f, "DROP TABLESPACE {}", names.join(", "))
6193 }
6194 ValidateOnlyKind::LargeObjectOid => {
6195 write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
6196 }
6197 ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
6198 ValidateOnlyKind::AggregateName => {
6199 write!(f, "ALTER AGGREGATE {}", names.join(", "))
6200 }
6201 ValidateOnlyKind::ConversionName => {
6202 write!(f, "DROP CONVERSION {}", names.join(", "))
6203 }
6204 ValidateOnlyKind::LanguageName => {
6205 write!(f, "DROP LANGUAGE {}", names.join(", "))
6206 }
6207 ValidateOnlyKind::ExtensionInstalled => {
6208 write!(f, "DROP EXTENSION {}", names.join(", "))
6209 }
6210 },
6211 Self::AlterSystem { parameter } => match parameter {
6212 Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
6213 None => f.write_str("ALTER SYSTEM RESET ALL"),
6214 },
6215 // v7.39 (round 547) — round-trips as PG writes it.
6216 Self::SetDbRoleSetting(st) => {
6217 match (&st.database, &st.role) {
6218 (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
6219 (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
6220 (None, None) => f.write_str("ALTER ROLE ALL")?,
6221 }
6222 if let (Some(d), Some(_)) = (&st.database, &st.role) {
6223 write!(f, " IN DATABASE {d}")?;
6224 }
6225 match (&st.param, &st.value) {
6226 (None, _) => f.write_str(" RESET ALL"),
6227 (Some(p), None) => write!(f, " RESET {p}"),
6228 (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
6229 }
6230 }
6231 Self::Maintain {
6232 kind,
6233 concurrently,
6234 target,
6235 } => {
6236 f.write_str(match kind {
6237 crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
6238 _ => "REINDEX ",
6239 })?;
6240 if *concurrently {
6241 f.write_str("CONCURRENTLY ")?;
6242 }
6243 if let Some(t) = target {
6244 f.write_str(t)?;
6245 }
6246 Ok(())
6247 }
6248 Self::DropDatabase { name, if_exists } => {
6249 f.write_str("DROP DATABASE ")?;
6250 if *if_exists {
6251 f.write_str("IF EXISTS ")?;
6252 }
6253 f.write_str(name)
6254 }
6255 Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
6256 Self::SetConstraints { names, deferred } => {
6257 f.write_str("SET CONSTRAINTS ")?;
6258 if names.is_empty() {
6259 f.write_str("ALL")?;
6260 } else {
6261 for (i, n) in names.iter().enumerate() {
6262 if i > 0 {
6263 f.write_str(", ")?;
6264 }
6265 f.write_str(n)?;
6266 }
6267 }
6268 f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
6269 }
6270 // v7.39 (round 277) — the source text is kept verbatim so
6271 // `pg_prepared_statements.statement` can report it the way
6272 // PG does (the whole PREPARE statement, not just the body).
6273 Self::Prepare { source, .. } => f.write_str(source),
6274 Self::Execute { name, args } => {
6275 write!(f, "EXECUTE {}", quote_ident(name))?;
6276 if !args.is_empty() {
6277 f.write_str("(")?;
6278 for (i, a) in args.iter().enumerate() {
6279 if i > 0 {
6280 f.write_str(", ")?;
6281 }
6282 write!(f, "{a}")?;
6283 }
6284 f.write_str(")")?;
6285 }
6286 Ok(())
6287 }
6288 Self::CreateStatistics {
6289 name,
6290 if_not_exists,
6291 kinds,
6292 columns,
6293 table,
6294 } => {
6295 f.write_str("CREATE STATISTICS ")?;
6296 if *if_not_exists {
6297 f.write_str("IF NOT EXISTS ")?;
6298 }
6299 write!(f, "{}", quote_ident(name))?;
6300 if !kinds.is_empty() {
6301 write!(f, " ({})", kinds.join(", "))?;
6302 }
6303 write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
6304 }
6305 Self::DropStatistics { name, if_exists } => {
6306 f.write_str("DROP STATISTICS ")?;
6307 if *if_exists {
6308 f.write_str("IF EXISTS ")?;
6309 }
6310 write!(f, "{}", quote_ident(name))
6311 }
6312 Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
6313 Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
6314 Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
6315 Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
6316 Self::DeclareCursor {
6317 name,
6318 scroll,
6319 hold,
6320 query,
6321 } => {
6322 write!(f, "DECLARE {} ", quote_ident(name))?;
6323 match scroll {
6324 Some(true) => f.write_str("SCROLL ")?,
6325 Some(false) => f.write_str("NO SCROLL ")?,
6326 None => {}
6327 }
6328 f.write_str("CURSOR ")?;
6329 if *hold {
6330 f.write_str("WITH HOLD ")?;
6331 }
6332 write!(f, "FOR {query}")
6333 }
6334 Self::FetchCursor { name, direction } => {
6335 write!(f, "FETCH {direction} FROM {}", quote_ident(name))
6336 }
6337 Self::MoveCursor { name, direction } => {
6338 write!(f, "MOVE {direction} FROM {}", quote_ident(name))
6339 }
6340 Self::CloseCursor { name } => match name {
6341 Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
6342 None => f.write_str("CLOSE ALL"),
6343 },
6344 Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
6345 Self::Notify { channel, payload } => {
6346 write!(f, "NOTIFY {}", quote_ident(channel))?;
6347 if let Some(p) = payload {
6348 write!(f, ", '{}'", p.replace('\'', "''"))?;
6349 }
6350 Ok(())
6351 }
6352 Self::Unlisten(ch) => match ch {
6353 Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
6354 None => f.write_str("UNLISTEN *"),
6355 },
6356 Self::CopyTo {
6357 table,
6358 columns,
6359 query,
6360 options,
6361 } => {
6362 if let Some(q) = query {
6363 write!(f, "COPY ({q})")?;
6364 } else {
6365 write!(f, "COPY {table}")?;
6366 if let Some(cols) = columns {
6367 write!(f, " ({})", cols.join(", "))?;
6368 }
6369 }
6370 write!(f, " TO STDOUT")?;
6371 let mut parts: Vec<String> = Vec::new();
6372 if options.format == CopyFormat::Csv {
6373 parts.push("FORMAT csv".to_string());
6374 }
6375 if options.header {
6376 parts.push("HEADER true".to_string());
6377 }
6378 if let Some(d) = options.delimiter {
6379 parts.push(alloc::format!("DELIMITER '{d}'"));
6380 }
6381 if let Some(n) = &options.null_str {
6382 parts.push(alloc::format!("NULL '{n}'"));
6383 }
6384 if let Some(q) = options.quote {
6385 parts.push(alloc::format!("QUOTE '{q}'"));
6386 }
6387 if !parts.is_empty() {
6388 write!(f, " WITH ({})", parts.join(", "))?;
6389 }
6390 Ok(())
6391 }
6392 Self::CopyFromFile {
6393 table,
6394 columns,
6395 path,
6396 options,
6397 } => {
6398 write!(f, "COPY {table}")?;
6399 if let Some(cols) = columns {
6400 write!(f, " ({})", cols.join(", "))?;
6401 }
6402 write!(f, " FROM '{path}'")?;
6403 let mut parts: Vec<String> = Vec::new();
6404 if options.format == CopyFormat::Csv {
6405 parts.push("FORMAT csv".to_string());
6406 }
6407 if options.header {
6408 parts.push("HEADER true".to_string());
6409 }
6410 if let Some(d) = options.delimiter {
6411 parts.push(alloc::format!("DELIMITER '{d}'"));
6412 }
6413 if let Some(n) = &options.null_str {
6414 parts.push(alloc::format!("NULL '{n}'"));
6415 }
6416 if let Some(q) = options.quote {
6417 parts.push(alloc::format!("QUOTE '{q}'"));
6418 }
6419 if !parts.is_empty() {
6420 write!(f, " WITH ({})", parts.join(", "))?;
6421 }
6422 Ok(())
6423 }
6424 Self::CopyToFile {
6425 table,
6426 columns,
6427 query,
6428 path,
6429 options,
6430 } => {
6431 if let Some(q) = query {
6432 write!(f, "COPY ({q})")?;
6433 } else {
6434 write!(f, "COPY {table}")?;
6435 if let Some(cols) = columns {
6436 write!(f, " ({})", cols.join(", "))?;
6437 }
6438 }
6439 write!(f, " TO '{path}'")?;
6440 let mut parts: Vec<String> = Vec::new();
6441 if options.format == CopyFormat::Csv {
6442 parts.push("FORMAT csv".to_string());
6443 }
6444 if options.header {
6445 parts.push("HEADER true".to_string());
6446 }
6447 if let Some(d) = options.delimiter {
6448 parts.push(alloc::format!("DELIMITER '{d}'"));
6449 }
6450 if let Some(n) = &options.null_str {
6451 parts.push(alloc::format!("NULL '{n}'"));
6452 }
6453 if let Some(q) = options.quote {
6454 parts.push(alloc::format!("QUOTE '{q}'"));
6455 }
6456 if !parts.is_empty() {
6457 write!(f, " WITH ({})", parts.join(", "))?;
6458 }
6459 Ok(())
6460 }
6461 Self::AlterDomain { name, action } => {
6462 write!(f, "ALTER DOMAIN {name} ")?;
6463 match action {
6464 AlterDomainAction::AddConstraint { name: cn, check } => match cn {
6465 Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
6466 None => write!(f, "ADD CHECK ({check})"),
6467 },
6468 AlterDomainAction::DropConstraint {
6469 name: cn,
6470 if_exists,
6471 } => {
6472 if *if_exists {
6473 write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
6474 } else {
6475 write!(f, "DROP CONSTRAINT {cn}")
6476 }
6477 }
6478 AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
6479 AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
6480 AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
6481 AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
6482 AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
6483 }
6484 }
6485 Self::Truncate {
6486 tables,
6487 restart_identity,
6488 cascade,
6489 only,
6490 } => {
6491 f.write_str("TRUNCATE TABLE ")?;
6492 if *only {
6493 f.write_str("ONLY ")?;
6494 }
6495 for (i, t) in tables.iter().enumerate() {
6496 if i > 0 {
6497 f.write_str(", ")?;
6498 }
6499 f.write_str(t)?;
6500 }
6501 if *restart_identity {
6502 f.write_str(" RESTART IDENTITY")?;
6503 }
6504 if *cascade {
6505 f.write_str(" CASCADE")?;
6506 }
6507 Ok(())
6508 }
6509 Self::DropTable { names, if_exists } => {
6510 f.write_str("DROP TABLE ")?;
6511 if *if_exists {
6512 f.write_str("IF EXISTS ")?;
6513 }
6514 for (i, n) in names.iter().enumerate() {
6515 if i > 0 {
6516 f.write_str(", ")?;
6517 }
6518 write!(f, "{}", quote_ident(n))?;
6519 }
6520 Ok(())
6521 }
6522 Self::DropIndex {
6523 name,
6524 if_exists,
6525 table,
6526 } => {
6527 f.write_str("DROP INDEX ")?;
6528 if *if_exists {
6529 f.write_str("IF EXISTS ")?;
6530 }
6531 write!(f, "{}", quote_ident(name))?;
6532 if let Some(t) = table {
6533 write!(f, " ON {}", quote_ident(t))?;
6534 }
6535 Ok(())
6536 }
6537 Self::Select(s) => s.fmt(f),
6538 Self::CreateTable(s) => s.fmt(f),
6539 Self::CreateIndex(s) => s.fmt(f),
6540 Self::Insert(s) => s.fmt(f),
6541 Self::Update(s) => s.fmt(f),
6542 Self::Delete(s) => s.fmt(f),
6543 Self::Merge(s) => s.fmt(f),
6544 Self::Vacuum { table, analyze } => {
6545 f.write_str("VACUUM")?;
6546 if *analyze {
6547 f.write_str(" ANALYZE")?;
6548 }
6549 if let Some(t) = table {
6550 write!(f, " {}", quote_ident(t))?;
6551 }
6552 Ok(())
6553 }
6554 Self::Begin(modes) => {
6555 f.write_str("BEGIN")?;
6556 if let Some(level) = modes.isolation {
6557 write!(f, " ISOLATION LEVEL {level}")?;
6558 }
6559 match modes.read_only {
6560 Some(true) => f.write_str(" READ ONLY")?,
6561 Some(false) => f.write_str(" READ WRITE")?,
6562 None => {}
6563 }
6564 Ok(())
6565 }
6566 Self::Commit => f.write_str("COMMIT"),
6567 Self::Rollback => f.write_str("ROLLBACK"),
6568 Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
6569 Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
6570 Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
6571 Self::ShowTables => f.write_str("SHOW TABLES"),
6572 Self::ShowDatabases => f.write_str("SHOW DATABASES"),
6573 Self::UseDatabase(n) => write!(f, "USE {}", quote_ident(n)),
6574 Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
6575 Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
6576 Self::ShowStatus => f.write_str("SHOW STATUS"),
6577 Self::ShowVariables => f.write_str("SHOW VARIABLES"),
6578 Self::ShowVariablesLike(p) => {
6579 write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
6580 }
6581 Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
6582 Self::Discard(t) => write!(f, "DISCARD {t}"),
6583 Self::Kill { query_only, id } => {
6584 if *query_only {
6585 write!(f, "KILL QUERY {id}")
6586 } else {
6587 write!(f, "KILL CONNECTION {id}")
6588 }
6589 }
6590 Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
6591 Self::CreateUser(s) => write!(
6592 f,
6593 "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
6594 quote_ident(&s.name),
6595 s.role
6596 ),
6597 Self::DropUser { name, if_exists } => {
6598 let ie = if *if_exists { "IF EXISTS " } else { "" };
6599 write!(f, "DROP USER {ie}{}", quote_ident(name))
6600 }
6601 Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
6602 Self::SetRole(None) => f.write_str("RESET ROLE"),
6603 Self::Grant(g) => write!(f, "{}", g.render(true)),
6604 Self::Revoke(g) => write!(f, "{}", g.render(false)),
6605 Self::CreatePolicy(s) => {
6606 write!(
6607 f,
6608 "CREATE POLICY {} ON {}",
6609 quote_ident(&s.name),
6610 quote_ident(&s.table)
6611 )?;
6612 if !s.permissive {
6613 f.write_str(" AS RESTRICTIVE")?;
6614 }
6615 if !matches!(s.cmd, PolicyCmd::All) {
6616 let w = match s.cmd {
6617 PolicyCmd::Select => "SELECT",
6618 PolicyCmd::Insert => "INSERT",
6619 PolicyCmd::Update => "UPDATE",
6620 PolicyCmd::Delete => "DELETE",
6621 PolicyCmd::All => unreachable!(),
6622 };
6623 write!(f, " FOR {w}")?;
6624 }
6625 if !s.roles.is_empty() {
6626 write!(f, " TO {}", s.roles.join(", "))?;
6627 }
6628 if let Some(u) = &s.using {
6629 write!(f, " USING ({u})")?;
6630 }
6631 if let Some(c) = &s.with_check {
6632 write!(f, " WITH CHECK ({c})")?;
6633 }
6634 Ok(())
6635 }
6636 Self::AlterPolicy(s) => {
6637 write!(
6638 f,
6639 "ALTER POLICY {} ON {}",
6640 quote_ident(&s.name),
6641 quote_ident(&s.table)
6642 )?;
6643 if let Some(nn) = &s.rename_to {
6644 return write!(f, " RENAME TO {}", quote_ident(nn));
6645 }
6646 if let Some(roles) = &s.roles {
6647 write!(f, " TO {}", roles.join(", "))?;
6648 }
6649 if let Some(u) = &s.using {
6650 write!(f, " USING ({u})")?;
6651 }
6652 if let Some(c) = &s.with_check {
6653 write!(f, " WITH CHECK ({c})")?;
6654 }
6655 Ok(())
6656 }
6657 Self::DropPolicy(s) => {
6658 f.write_str("DROP POLICY ")?;
6659 if s.if_exists {
6660 f.write_str("IF EXISTS ")?;
6661 }
6662 write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
6663 }
6664 Self::ShowUsers => f.write_str("SHOW USERS"),
6665 Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
6666 Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
6667 Self::CreateSubscription(s) => {
6668 write!(
6669 f,
6670 "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
6671 quote_ident(&s.name),
6672 s.conn_str.replace('\'', "''")
6673 )?;
6674 for (i, p) in s.publications.iter().enumerate() {
6675 if i > 0 {
6676 f.write_str(", ")?;
6677 }
6678 write!(f, "{}", quote_ident(p))?;
6679 }
6680 Ok(())
6681 }
6682 Self::DropSubscription { name, if_exists } => {
6683 let opt = if *if_exists { "IF EXISTS " } else { "" };
6684 write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
6685 }
6686 Self::WaitForWalPosition { pos, timeout_ms } => {
6687 write!(f, "WAIT FOR WAL POSITION {pos}")?;
6688 if let Some(ms) = timeout_ms {
6689 write!(f, " WITH TIMEOUT {ms}")?;
6690 }
6691 Ok(())
6692 }
6693 Self::RenameTables(pairs) => {
6694 f.write_str("RENAME TABLE ")?;
6695 for (i, (from, to)) in pairs.iter().enumerate() {
6696 if i > 0 {
6697 f.write_str(", ")?;
6698 }
6699 write!(f, "{} TO {}", quote_ident(from), quote_ident(to))?;
6700 }
6701 Ok(())
6702 }
6703 Self::Analyze(None) => f.write_str("ANALYZE"),
6704 Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
6705 Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
6706 Self::Explain(e) => {
6707 if e.suggest {
6708 write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
6709 } else if e.analyze {
6710 write!(f, "EXPLAIN ANALYZE {}", e.inner)
6711 } else {
6712 write!(f, "EXPLAIN {}", e.inner)
6713 }
6714 }
6715 Self::AlterIndex(a) => {
6716 write!(f, "ALTER INDEX ")?;
6717 match &a.target {
6718 // Parameters are consumed, not stored; the shortest
6719 // faithful spelling.
6720 AlterIndexTarget::StorageParams => {
6721 write!(f, "{} SET ()", quote_ident(&a.name))
6722 }
6723 AlterIndexTarget::Rebuild { encoding } => {
6724 write!(f, "{} REBUILD", quote_ident(&a.name))?;
6725 if let Some(enc) = encoding {
6726 write!(f, " WITH (encoding = {enc})")?;
6727 }
6728 Ok(())
6729 }
6730 AlterIndexTarget::Rename { new, if_exists } => {
6731 if *if_exists {
6732 f.write_str("IF EXISTS ")?;
6733 }
6734 write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
6735 }
6736 }
6737 }
6738 Self::AlterTable(a) => {
6739 write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
6740 for (i, t) in a.targets.iter().enumerate() {
6741 if i > 0 {
6742 f.write_str(", ")?;
6743 }
6744 fmt_alter_target(f, t)?;
6745 }
6746 Ok(())
6747 }
6748 Self::CreatePublication(p) => {
6749 write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
6750 match &p.scope {
6751 PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
6752 PublicationScope::ForTables(ts) => {
6753 f.write_str(" FOR TABLE ")?;
6754 for (i, t) in ts.iter().enumerate() {
6755 if i > 0 {
6756 f.write_str(", ")?;
6757 }
6758 write!(f, "{}", quote_ident(t))?;
6759 }
6760 Ok(())
6761 }
6762 PublicationScope::TablesInSchema(schema) => {
6763 write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
6764 Ok(())
6765 }
6766 PublicationScope::AllTablesExcept(ts) => {
6767 f.write_str(" FOR ALL TABLES EXCEPT ")?;
6768 for (i, t) in ts.iter().enumerate() {
6769 if i > 0 {
6770 f.write_str(", ")?;
6771 }
6772 write!(f, "{}", quote_ident(t))?;
6773 }
6774 Ok(())
6775 }
6776 }
6777 }
6778 Self::CreateExtension(name) => {
6779 write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
6780 }
6781 Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
6782 Self::DropPublication { name, if_exists } => {
6783 let opt = if *if_exists { "IF EXISTS " } else { "" };
6784 write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
6785 }
6786 Self::SetParameter { name, value, local } => {
6787 write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
6788 match value {
6789 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
6790 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
6791 SetValue::Default => f.write_str("DEFAULT"),
6792 SetValue::Null => f.write_str("NULL"),
6793 }
6794 }
6795 Self::SetTransaction { modes } => {
6796 f.write_str("SET TRANSACTION")?;
6797 if let Some(isolation) = modes.isolation {
6798 let name = match isolation {
6799 IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
6800 IsolationLevel::ReadCommitted => "READ COMMITTED",
6801 IsolationLevel::RepeatableRead => "REPEATABLE READ",
6802 IsolationLevel::Serializable => "SERIALIZABLE",
6803 };
6804 write!(f, " ISOLATION LEVEL {name}")?;
6805 }
6806 match modes.read_only {
6807 Some(true) => f.write_str(" READ ONLY")?,
6808 Some(false) => f.write_str(" READ WRITE")?,
6809 None => {}
6810 }
6811 Ok(())
6812 }
6813 Self::ShowParameter(name) => write!(f, "SHOW {name}"),
6814 Self::SetUserVars(assigns, _) => {
6815 f.write_str("SET ")?;
6816 for (i, (name, value)) in assigns.iter().enumerate() {
6817 if i > 0 {
6818 f.write_str(", ")?;
6819 }
6820 write!(f, "@{name} = {value}")?;
6821 }
6822 Ok(())
6823 }
6824 Self::SetParameterList(pairs) => {
6825 f.write_str("SET ")?;
6826 for (i, (name, value)) in pairs.iter().enumerate() {
6827 if i > 0 {
6828 f.write_str(", ")?;
6829 }
6830 write!(f, "{name} = ")?;
6831 match value {
6832 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
6833 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
6834 SetValue::Default => f.write_str("DEFAULT")?,
6835 SetValue::Null => f.write_str("NULL")?,
6836 }
6837 }
6838 Ok(())
6839 }
6840 Self::ResetParameter(None) => f.write_str("RESET ALL"),
6841 Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
6842 Self::CreateFunction(s) => s.fmt(f),
6843 Self::CreateTrigger(s) => s.fmt(f),
6844 Self::DropTrigger {
6845 name,
6846 table,
6847 if_exists,
6848 } => {
6849 f.write_str("DROP TRIGGER ")?;
6850 if *if_exists {
6851 f.write_str("IF EXISTS ")?;
6852 }
6853 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6854 }
6855 Self::DropFunction {
6856 name,
6857 args,
6858 if_exists,
6859 } => {
6860 f.write_str("DROP FUNCTION ")?;
6861 if *if_exists {
6862 f.write_str("IF EXISTS ")?;
6863 }
6864 write!(f, "{}", quote_ident(name))?;
6865 if let Some(a) = args {
6866 write!(f, "({})", a.join(", "))?;
6867 }
6868 Ok(())
6869 }
6870 Self::CreateSequence(s) => s.fmt(f),
6871 Self::AlterSequence(s) => s.fmt(f),
6872 Self::DropSequence { names, if_exists } => {
6873 f.write_str("DROP SEQUENCE ")?;
6874 if *if_exists {
6875 f.write_str("IF EXISTS ")?;
6876 }
6877 for (i, n) in names.iter().enumerate() {
6878 if i > 0 {
6879 f.write_str(", ")?;
6880 }
6881 write!(f, "{}", quote_ident(n))?;
6882 }
6883 Ok(())
6884 }
6885 Self::CreateView(v) => v.fmt(f),
6886 Self::DropView { names, if_exists } => {
6887 f.write_str("DROP VIEW ")?;
6888 if *if_exists {
6889 f.write_str("IF EXISTS ")?;
6890 }
6891 for (i, n) in names.iter().enumerate() {
6892 if i > 0 {
6893 f.write_str(", ")?;
6894 }
6895 write!(f, "{}", quote_ident(n))?;
6896 }
6897 Ok(())
6898 }
6899 Self::CreateMaterializedView(v) => v.fmt(f),
6900 Self::RefreshMaterializedView { name, with_data } => {
6901 write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6902 if !*with_data {
6903 f.write_str(" WITH NO DATA")?;
6904 }
6905 Ok(())
6906 }
6907 Self::DropMaterializedView { names, if_exists } => {
6908 f.write_str("DROP MATERIALIZED VIEW ")?;
6909 if *if_exists {
6910 f.write_str("IF EXISTS ")?;
6911 }
6912 for (i, n) in names.iter().enumerate() {
6913 if i > 0 {
6914 f.write_str(", ")?;
6915 }
6916 write!(f, "{}", quote_ident(n))?;
6917 }
6918 Ok(())
6919 }
6920 Self::CreateType(t) => t.fmt(f),
6921 Self::CommentOn {
6922 kind,
6923 name,
6924 comment,
6925 } => {
6926 let body = match comment {
6927 Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6928 None => "NULL".into(),
6929 };
6930 write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6931 }
6932 Self::AlterTypeRenameValue {
6933 type_name,
6934 old,
6935 new,
6936 } => write!(
6937 f,
6938 "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6939 quote_ident(type_name),
6940 old.replace('\'', "''"),
6941 new.replace('\'', "''")
6942 ),
6943 Self::AlterTypeAddValue {
6944 type_name,
6945 label,
6946 if_not_exists,
6947 position,
6948 } => {
6949 write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6950 if *if_not_exists {
6951 write!(f, "IF NOT EXISTS ")?;
6952 }
6953 write!(f, "'{label}'")?;
6954 if let Some((is_before, anchor)) = position {
6955 write!(
6956 f,
6957 " {} '{anchor}'",
6958 if *is_before { "BEFORE" } else { "AFTER" }
6959 )?;
6960 }
6961 Ok(())
6962 }
6963 Self::DropType { names, if_exists } => {
6964 f.write_str("DROP TYPE ")?;
6965 if *if_exists {
6966 f.write_str("IF EXISTS ")?;
6967 }
6968 for (i, n) in names.iter().enumerate() {
6969 if i > 0 {
6970 f.write_str(", ")?;
6971 }
6972 write!(f, "{}", quote_ident(n))?;
6973 }
6974 Ok(())
6975 }
6976 Self::CreateDomain(d) => d.fmt(f),
6977 Self::DropDomain { names, if_exists } => {
6978 f.write_str("DROP DOMAIN ")?;
6979 if *if_exists {
6980 f.write_str("IF EXISTS ")?;
6981 }
6982 for (i, n) in names.iter().enumerate() {
6983 if i > 0 {
6984 f.write_str(", ")?;
6985 }
6986 write!(f, "{}", quote_ident(n))?;
6987 }
6988 Ok(())
6989 }
6990 Self::CreateSchema {
6991 name,
6992 if_not_exists,
6993 } => {
6994 f.write_str("CREATE SCHEMA ")?;
6995 if *if_not_exists {
6996 f.write_str("IF NOT EXISTS ")?;
6997 }
6998 write!(f, "{}", quote_ident(name))
6999 }
7000 Self::DropSchema { names, if_exists } => {
7001 f.write_str("DROP SCHEMA ")?;
7002 if *if_exists {
7003 f.write_str("IF EXISTS ")?;
7004 }
7005 for (i, n) in names.iter().enumerate() {
7006 if i > 0 {
7007 f.write_str(", ")?;
7008 }
7009 write!(f, "{}", quote_ident(n))?;
7010 }
7011 Ok(())
7012 }
7013 Self::CreateRule(r) => {
7014 f.write_str("CREATE ")?;
7015 if r.or_replace {
7016 f.write_str("OR REPLACE ")?;
7017 }
7018 write!(
7019 f,
7020 "RULE {} AS ON {} TO {}",
7021 quote_ident(&r.name),
7022 r.event,
7023 quote_ident(&r.table)
7024 )?;
7025 if let Some(w) = &r.when_condition {
7026 write!(f, " WHERE {w}")?;
7027 }
7028 f.write_str(if r.instead {
7029 " DO INSTEAD "
7030 } else {
7031 " DO ALSO "
7032 })?;
7033 if r.commands.is_empty() {
7034 f.write_str("NOTHING")?;
7035 } else if r.commands.len() == 1 {
7036 write!(f, "{}", r.commands[0])?;
7037 } else {
7038 f.write_str("(")?;
7039 for (i, c) in r.commands.iter().enumerate() {
7040 if i > 0 {
7041 f.write_str("; ")?;
7042 }
7043 write!(f, "{c}")?;
7044 }
7045 f.write_str(")")?;
7046 }
7047 Ok(())
7048 }
7049 Self::DropRule {
7050 name,
7051 table,
7052 if_exists,
7053 } => {
7054 f.write_str("DROP RULE ")?;
7055 if *if_exists {
7056 f.write_str("IF EXISTS ")?;
7057 }
7058 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
7059 }
7060 }
7061 }
7062}
7063
7064impl fmt::Display for CreateDomainStatement {
7065 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7066 write!(
7067 f,
7068 "CREATE DOMAIN {} AS {}",
7069 quote_ident(&self.name),
7070 self.base_type
7071 )?;
7072 if let Some(d) = &self.default {
7073 write!(f, " DEFAULT {d}")?;
7074 }
7075 if self.not_null {
7076 f.write_str(" NOT NULL")?;
7077 }
7078 for c in &self.checks {
7079 write!(f, " CHECK ({c})")?;
7080 }
7081 Ok(())
7082 }
7083}
7084
7085impl fmt::Display for CreateTypeStatement {
7086 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7087 write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
7088 match &self.kind {
7089 TypeKind::Enum { labels } => {
7090 f.write_str("ENUM (")?;
7091 for (i, l) in labels.iter().enumerate() {
7092 if i > 0 {
7093 f.write_str(", ")?;
7094 }
7095 write!(f, "'{}'", l.replace('\'', "''"))?;
7096 }
7097 f.write_str(")")
7098 }
7099 TypeKind::Composite { fields, .. } => {
7100 f.write_str("(")?;
7101 for (i, (n, t)) in fields.iter().enumerate() {
7102 if i > 0 {
7103 f.write_str(", ")?;
7104 }
7105 write!(f, "{} {}", quote_ident(n), t)?;
7106 }
7107 f.write_str(")")
7108 }
7109 }
7110 }
7111}
7112
7113impl fmt::Display for CreateMaterializedViewStatement {
7114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7115 f.write_str("CREATE MATERIALIZED VIEW ")?;
7116 if self.if_not_exists {
7117 f.write_str("IF NOT EXISTS ")?;
7118 }
7119 write!(f, "{}", quote_ident(&self.name))?;
7120 if !self.columns.is_empty() {
7121 f.write_str(" (")?;
7122 for (i, c) in self.columns.iter().enumerate() {
7123 if i > 0 {
7124 f.write_str(", ")?;
7125 }
7126 write!(f, "{}", quote_ident(c))?;
7127 }
7128 f.write_str(")")?;
7129 }
7130 write!(f, " AS {}", self.body)?;
7131 if !self.with_data {
7132 f.write_str(" WITH NO DATA")?;
7133 }
7134 Ok(())
7135 }
7136}
7137
7138impl fmt::Display for CreateViewStatement {
7139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7140 f.write_str("CREATE ")?;
7141 if self.or_replace {
7142 f.write_str("OR REPLACE ")?;
7143 }
7144 if self.temporary {
7145 f.write_str("TEMPORARY ")?;
7146 }
7147 f.write_str("VIEW ")?;
7148 if self.if_not_exists {
7149 f.write_str("IF NOT EXISTS ")?;
7150 }
7151 write!(f, "{}", quote_ident(&self.name))?;
7152 if !self.columns.is_empty() {
7153 f.write_str(" (")?;
7154 for (i, c) in self.columns.iter().enumerate() {
7155 if i > 0 {
7156 f.write_str(", ")?;
7157 }
7158 write!(f, "{}", quote_ident(c))?;
7159 }
7160 f.write_str(")")?;
7161 }
7162 write!(f, " AS {}", self.body)?;
7163 match self.check_option {
7164 Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
7165 Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
7166 None => Ok(()),
7167 }
7168 }
7169}
7170
7171impl fmt::Display for CreateSequenceStatement {
7172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7173 f.write_str("CREATE ")?;
7174 if self.temporary {
7175 f.write_str("TEMPORARY ")?;
7176 }
7177 f.write_str("SEQUENCE ")?;
7178 if self.if_not_exists {
7179 f.write_str("IF NOT EXISTS ")?;
7180 }
7181 write!(f, "{}", quote_ident(&self.name))?;
7182 if let Some(dt) = self.data_type {
7183 write!(f, " AS {dt}")?;
7184 }
7185 write_sequence_options(f, &self.options)
7186 }
7187}
7188
7189impl fmt::Display for AlterSequenceStatement {
7190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7191 f.write_str("ALTER SEQUENCE ")?;
7192 if self.if_exists {
7193 f.write_str("IF EXISTS ")?;
7194 }
7195 write!(f, "{}", quote_ident(&self.name))?;
7196 write_sequence_options(f, &self.options)
7197 }
7198}
7199
7200impl fmt::Display for SequenceDataType {
7201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7202 f.write_str(match self {
7203 Self::SmallInt => "smallint",
7204 Self::Int => "integer",
7205 Self::BigInt => "bigint",
7206 })
7207 }
7208}
7209
7210fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
7211 if let Some(n) = o.increment {
7212 write!(f, " INCREMENT BY {n}")?;
7213 }
7214 match o.min_value {
7215 Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
7216 Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
7217 None => {}
7218 }
7219 match o.max_value {
7220 Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
7221 Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
7222 None => {}
7223 }
7224 if let Some(n) = o.start {
7225 write!(f, " START WITH {n}")?;
7226 }
7227 match o.restart {
7228 Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
7229 Some(None) => f.write_str(" RESTART")?,
7230 None => {}
7231 }
7232 if let Some(n) = o.cache {
7233 write!(f, " CACHE {n}")?;
7234 }
7235 match o.cycle {
7236 Some(true) => f.write_str(" CYCLE")?,
7237 Some(false) => f.write_str(" NO CYCLE")?,
7238 None => {}
7239 }
7240 if let Some(ob) = &o.owned_by {
7241 match ob {
7242 SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
7243 SequenceOwnedBy::Column { table, column } => {
7244 write!(
7245 f,
7246 " OWNED BY {}.{}",
7247 quote_ident(table),
7248 quote_ident(column)
7249 )?;
7250 }
7251 }
7252 }
7253 Ok(())
7254}
7255
7256impl fmt::Display for CreateFunctionStatement {
7257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7258 f.write_str("CREATE ")?;
7259 if self.or_replace {
7260 f.write_str("OR REPLACE ")?;
7261 }
7262 write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
7263 for (i, arg) in self.args.iter().enumerate() {
7264 if i > 0 {
7265 f.write_str(", ")?;
7266 }
7267 match arg.mode {
7268 FunctionArgMode::In => {}
7269 FunctionArgMode::Out => f.write_str("OUT ")?,
7270 FunctionArgMode::InOut => f.write_str("INOUT ")?,
7271 }
7272 if let Some(name) = &arg.name {
7273 write!(f, "{} ", quote_ident(name))?;
7274 }
7275 match &arg.ty {
7276 FunctionArgType::Typed(t) => write!(f, "{t}")?,
7277 FunctionArgType::Raw(s) => f.write_str(s)?,
7278 }
7279 }
7280 f.write_str(") RETURNS ")?;
7281 match &self.returns {
7282 FunctionReturn::Trigger => f.write_str("TRIGGER")?,
7283 FunctionReturn::Void => f.write_str("VOID")?,
7284 FunctionReturn::Type(t) => write!(f, "{t}")?,
7285 FunctionReturn::Other(s) => f.write_str(s)?,
7286 }
7287 write!(f, " LANGUAGE {} AS $$", self.language)?;
7288 match &self.body {
7289 FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
7290 FunctionBody::Raw(s) => f.write_str(s)?,
7291 }
7292 f.write_str("$$")
7293 }
7294}
7295
7296impl fmt::Display for PlPgSqlBlock {
7297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7298 if !self.declarations.is_empty() {
7299 f.write_str("DECLARE\n")?;
7300 for d in &self.declarations {
7301 write!(f, " {} ", quote_ident(&d.name))?;
7302 match &d.ty {
7303 FunctionArgType::Typed(t) => write!(f, "{t}")?,
7304 FunctionArgType::Raw(s) => f.write_str(s)?,
7305 }
7306 if let Some(e) = &d.default {
7307 write!(f, " := {e}")?;
7308 }
7309 f.write_str(";\n")?;
7310 }
7311 }
7312 f.write_str("BEGIN\n")?;
7313 for stmt in &self.statements {
7314 writeln!(f, " {stmt};")?;
7315 }
7316 // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
7317 // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
7318 // parsed block through it — so every exception handler a function
7319 // declared was thrown away AT STORE TIME. The block executed fine while
7320 // it was still an AST (a DO block never round-trips through text), which
7321 // is why only functions and triggers lost theirs.
7322 if !self.exception_handlers.is_empty() {
7323 f.write_str("EXCEPTION\n")?;
7324 for h in &self.exception_handlers {
7325 writeln!(f, " WHEN {} THEN", h.conditions.join(" OR "))?;
7326 for stmt in &h.body {
7327 writeln!(f, " {stmt};")?;
7328 }
7329 }
7330 }
7331 f.write_str("END")
7332 }
7333}
7334
7335impl fmt::Display for PlPgSqlStmt {
7336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7337 match self {
7338 Self::Assign { target, value } => write!(f, "{target} := {value}"),
7339 Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
7340 Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
7341 Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
7342 Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
7343 Self::Return(t) => match t {
7344 ReturnTarget::New => f.write_str("RETURN NEW"),
7345 ReturnTarget::Old => f.write_str("RETURN OLD"),
7346 ReturnTarget::Null => f.write_str("RETURN NULL"),
7347 ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
7348 },
7349 Self::If {
7350 branches,
7351 else_branch,
7352 } => {
7353 for (i, (cond, body)) in branches.iter().enumerate() {
7354 if i == 0 {
7355 write!(f, "IF {cond} THEN ")?;
7356 } else {
7357 write!(f, " ELSIF {cond} THEN ")?;
7358 }
7359 for (j, s) in body.iter().enumerate() {
7360 if j > 0 {
7361 f.write_str("; ")?;
7362 }
7363 write!(f, "{s}")?;
7364 }
7365 }
7366 if !else_branch.is_empty() {
7367 f.write_str(" ELSE ")?;
7368 for (j, s) in else_branch.iter().enumerate() {
7369 if j > 0 {
7370 f.write_str("; ")?;
7371 }
7372 write!(f, "{s}")?;
7373 }
7374 }
7375 f.write_str(" END IF")
7376 }
7377 Self::Raise {
7378 level,
7379 message,
7380 args,
7381 } => {
7382 let lvl = match level {
7383 RaiseLevel::Notice => "NOTICE",
7384 RaiseLevel::Warning => "WARNING",
7385 RaiseLevel::Info => "INFO",
7386 RaiseLevel::Log => "LOG",
7387 RaiseLevel::Debug => "DEBUG",
7388 RaiseLevel::Exception => "EXCEPTION",
7389 };
7390 write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
7391 for a in args {
7392 write!(f, ", {a}")?;
7393 }
7394 Ok(())
7395 }
7396 Self::EmbeddedSql(s) => write!(f, "{s}"),
7397 Self::Assert { condition, message } => {
7398 write!(f, "ASSERT {condition}")?;
7399 if let Some(m) = message {
7400 write!(f, ", {m}")?;
7401 }
7402 Ok(())
7403 }
7404 Self::While { condition, body } => {
7405 writeln!(f, "WHILE {condition} LOOP")?;
7406 for s in body {
7407 writeln!(f, " {s};")?;
7408 }
7409 f.write_str("END LOOP")
7410 }
7411 Self::ForRange {
7412 var,
7413 start,
7414 end,
7415 reverse,
7416 body,
7417 } => {
7418 write!(f, "FOR {var} IN ")?;
7419 if *reverse {
7420 f.write_str("REVERSE ")?;
7421 }
7422 writeln!(f, "{start}..{end} LOOP")?;
7423 for s in body {
7424 writeln!(f, " {s};")?;
7425 }
7426 f.write_str("END LOOP")
7427 }
7428 Self::Loop { body } => {
7429 writeln!(f, "LOOP")?;
7430 for s in body {
7431 writeln!(f, " {s};")?;
7432 }
7433 f.write_str("END LOOP")
7434 }
7435 Self::Exit { when } => {
7436 f.write_str("EXIT")?;
7437 if let Some(c) = when {
7438 write!(f, " WHEN {c}")?;
7439 }
7440 Ok(())
7441 }
7442 Self::Continue { when } => {
7443 f.write_str("CONTINUE")?;
7444 if let Some(c) = when {
7445 write!(f, " WHEN {c}")?;
7446 }
7447 Ok(())
7448 }
7449 Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
7450 Self::ForQuery { var, query, body } => {
7451 writeln!(f, "FOR {var} IN ({query}) LOOP")?;
7452 for s in body {
7453 writeln!(f, " {s};")?;
7454 }
7455 f.write_str("END LOOP")
7456 }
7457 Self::ForExecute {
7458 var,
7459 sql_expr,
7460 body,
7461 } => {
7462 writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
7463 for s in body {
7464 writeln!(f, " {s};")?;
7465 }
7466 f.write_str("END LOOP")
7467 }
7468 }
7469 }
7470}
7471
7472impl fmt::Display for AssignTarget {
7473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7474 match self {
7475 Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
7476 Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
7477 Self::Local(n) => f.write_str(n),
7478 }
7479 }
7480}
7481
7482impl fmt::Display for CreateTriggerStatement {
7483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7484 f.write_str("CREATE ")?;
7485 if self.or_replace {
7486 f.write_str("OR REPLACE ")?;
7487 }
7488 write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
7489 match self.timing {
7490 TriggerTiming::Before => f.write_str("BEFORE")?,
7491 TriggerTiming::After => f.write_str("AFTER")?,
7492 TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
7493 }
7494 for (i, e) in self.events.iter().enumerate() {
7495 if i == 0 {
7496 f.write_str(" ")?;
7497 } else {
7498 f.write_str(" OR ")?;
7499 }
7500 match e {
7501 TriggerEvent::Insert => f.write_str("INSERT")?,
7502 TriggerEvent::Update => {
7503 f.write_str("UPDATE")?;
7504 if !self.update_columns.is_empty() {
7505 f.write_str(" OF ")?;
7506 for (j, col) in self.update_columns.iter().enumerate() {
7507 if j > 0 {
7508 f.write_str(", ")?;
7509 }
7510 f.write_str("e_ident(col))?;
7511 }
7512 }
7513 }
7514 TriggerEvent::Delete => f.write_str("DELETE")?,
7515 TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
7516 }
7517 }
7518 write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
7519 match self.for_each {
7520 TriggerForEach::Row => f.write_str("ROW")?,
7521 TriggerForEach::Statement => f.write_str("STATEMENT")?,
7522 }
7523 write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
7524 }
7525}
7526
7527impl fmt::Display for CreateIndexStatement {
7528 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7529 if self.is_unique {
7530 f.write_str("CREATE UNIQUE INDEX ")?;
7531 } else {
7532 f.write_str("CREATE INDEX ")?;
7533 }
7534 if self.if_not_exists {
7535 f.write_str("IF NOT EXISTS ")?;
7536 }
7537 write!(
7538 f,
7539 "{} ON {} ",
7540 quote_ident(&self.name),
7541 quote_ident(&self.table)
7542 )?;
7543 match self.method {
7544 IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
7545 IndexMethod::Brin => f.write_str("USING brin ")?,
7546 IndexMethod::Gin => f.write_str("USING gin ")?,
7547 IndexMethod::BTree => {}
7548 }
7549 if let Some(expr) = &self.expression {
7550 write!(f, "({})", expr)?;
7551 } else if self.extra_columns.is_empty() {
7552 // v7.15.0 — preserve operator class on round-trip
7553 // (`(col opclass)`) so WAL replay reconstructs the
7554 // engine-routing intent (e.g. `gin_trgm_ops` →
7555 // trigram-GIN build path).
7556 if let Some(op) = &self.opclass {
7557 write!(f, "({} {})", quote_ident(&self.column), op)?;
7558 } else {
7559 write!(f, "({})", quote_ident(&self.column))?;
7560 }
7561 } else {
7562 // v7.9.14 — multi-column key. Emit each column quoted
7563 // so the round-tripped form re-parses to identical AST.
7564 f.write_str("(")?;
7565 write!(f, "{}", quote_ident(&self.column))?;
7566 for c in &self.extra_columns {
7567 write!(f, ", {}", quote_ident(c))?;
7568 }
7569 f.write_str(")")?;
7570 }
7571 if !self.included_columns.is_empty() {
7572 f.write_str(" INCLUDE (")?;
7573 for (i, c) in self.included_columns.iter().enumerate() {
7574 if i > 0 {
7575 f.write_str(", ")?;
7576 }
7577 write!(f, "{}", quote_ident(c))?;
7578 }
7579 f.write_str(")")?;
7580 }
7581 if let Some(pred) = &self.partial_predicate {
7582 write!(f, " WHERE {}", pred)?;
7583 }
7584 Ok(())
7585 }
7586}
7587
7588impl fmt::Display for CreateTableStatement {
7589 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7590 f.write_str("CREATE TABLE ")?;
7591 if self.if_not_exists {
7592 f.write_str("IF NOT EXISTS ")?;
7593 }
7594 write!(f, "{}", quote_ident(&self.name))?;
7595 // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
7596 // no column list and no constraints; the table inherits its
7597 // columns from the parent at engine-DDL time.
7598 if let Some(spec) = &self.partition_of {
7599 write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
7600 return match &spec.bounds {
7601 PartitionOfBoundsAst::Range { lower, upper } => {
7602 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7603 }
7604 PartitionOfBoundsAst::List { values } => {
7605 f.write_str("FOR VALUES IN (")?;
7606 for (i, v) in values.iter().enumerate() {
7607 if i > 0 {
7608 f.write_str(", ")?;
7609 }
7610 write!(f, "{}", v)?;
7611 }
7612 f.write_str(")")
7613 }
7614 PartitionOfBoundsAst::Hash { modulus, remainder } => {
7615 write!(
7616 f,
7617 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7618 modulus, remainder
7619 )
7620 }
7621 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7622 };
7623 }
7624 f.write_str(" (")?;
7625 for (i, col) in self.columns.iter().enumerate() {
7626 if i > 0 {
7627 f.write_str(", ")?;
7628 }
7629 write!(f, "{col}")?;
7630 }
7631 // v7.6.0 — render FK constraints in table-level form, after
7632 // the column list. WAL replay round-trips through Display, so
7633 // every FK must serialise here for replay to reconstruct the
7634 // schema bit-for-bit.
7635 for fk in &self.foreign_keys {
7636 f.write_str(", ")?;
7637 write!(f, "{fk}")?;
7638 }
7639 // v7.13.0 — render table-level constraints (PRIMARY KEY /
7640 // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
7641 // column-level UNIQUE / CHECK get lifted to this list at
7642 // parse time, so emitting only here avoids double-counting.
7643 for tc in &self.table_constraints {
7644 f.write_str(", ")?;
7645 write!(f, "{tc}")?;
7646 }
7647 f.write_str(")")?;
7648 // v7.37.6-B — partition-parent suffix renders after the
7649 // closing column-list paren, before the optional MySQL
7650 // table-options tail (which Display doesn't currently emit).
7651 if let Some(spec) = &self.partition_by {
7652 f.write_str(" PARTITION BY ")?;
7653 match spec.kind {
7654 PartitionKindAst::Range => f.write_str("RANGE ")?,
7655 PartitionKindAst::List => f.write_str("LIST ")?,
7656 PartitionKindAst::Hash => f.write_str("HASH ")?,
7657 }
7658 f.write_str("(")?;
7659 for (i, col) in spec.key_columns.iter().enumerate() {
7660 if i > 0 {
7661 f.write_str(", ")?;
7662 }
7663 f.write_str("e_ident(col))?;
7664 }
7665 f.write_str(")")?;
7666 }
7667 Ok(())
7668 }
7669}
7670
7671fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
7672 match t {
7673 AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
7674 AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
7675 write!(f, "REPLICA IDENTITY USING INDEX {index}")
7676 }
7677 AlterTableTarget::Inherit { parent, detach } => {
7678 if *detach {
7679 write!(f, "NO INHERIT {parent}")
7680 } else {
7681 write!(f, "INHERIT {parent}")
7682 }
7683 }
7684 AlterTableTarget::SetHotTierBytes(n) => {
7685 write!(f, "SET hot_tier_bytes = {n}")
7686 }
7687 AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
7688 AlterTableTarget::DropForeignKey { name, if_exists } => {
7689 f.write_str("DROP CONSTRAINT ")?;
7690 if *if_exists {
7691 f.write_str("IF EXISTS ")?;
7692 }
7693 write!(f, "{}", quote_ident(name))
7694 }
7695 AlterTableTarget::DropIndex { name, if_exists } => {
7696 f.write_str("DROP INDEX ")?;
7697 if *if_exists {
7698 f.write_str("IF EXISTS ")?;
7699 }
7700 write!(f, "{}", quote_ident(name))
7701 }
7702 AlterTableTarget::ModifyColumn {
7703 column,
7704 rename_to,
7705 definition,
7706 position,
7707 } => {
7708 if let Some(new) = rename_to {
7709 write!(
7710 f,
7711 "CHANGE COLUMN {} {} {}",
7712 quote_ident(column),
7713 quote_ident(new),
7714 definition.ty
7715 )?;
7716 } else {
7717 write!(f, "MODIFY COLUMN {} {}", quote_ident(column), definition.ty)?;
7718 }
7719 if !definition.nullable {
7720 f.write_str(" NOT NULL")?;
7721 }
7722 write_column_position(f, position.as_ref())
7723 }
7724 AlterTableTarget::RenameIndex { old, new } => {
7725 write!(
7726 f,
7727 "RENAME INDEX {} TO {}",
7728 quote_ident(old),
7729 quote_ident(new)
7730 )
7731 }
7732 AlterTableTarget::SetTableAutoIncrement(n) => write!(f, "AUTO_INCREMENT = {n}"),
7733 AlterTableTarget::SetEngine(name) => write!(f, "ENGINE = {name}"),
7734 AlterTableTarget::ConvertToCharacterSet { charset, collate } => {
7735 write!(f, "CONVERT TO CHARACTER SET {charset}")?;
7736 if let Some(c) = collate {
7737 write!(f, " COLLATE {c}")?;
7738 }
7739 Ok(())
7740 }
7741 AlterTableTarget::AddColumn {
7742 column,
7743 if_not_exists,
7744 position,
7745 } => {
7746 f.write_str("ADD COLUMN ")?;
7747 if *if_not_exists {
7748 f.write_str("IF NOT EXISTS ")?;
7749 }
7750 write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
7751 if !column.nullable {
7752 f.write_str(" NOT NULL")?;
7753 }
7754 if let Some(d) = &column.default {
7755 write!(f, " DEFAULT {d}")?;
7756 }
7757 if column.auto_increment {
7758 f.write_str(" AUTO_INCREMENT")?;
7759 }
7760 if column.is_primary_key {
7761 f.write_str(" PRIMARY KEY")?;
7762 }
7763 Ok(())
7764 }
7765 AlterTableTarget::AlterColumnType {
7766 column,
7767 new_type,
7768 using,
7769 collation,
7770 } => {
7771 write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
7772 if let Some((_, name)) = collation {
7773 write!(f, " COLLATE {}", quote_ident(name))?;
7774 }
7775 if let Some(u) = using {
7776 write!(f, " USING {u}")?;
7777 }
7778 Ok(())
7779 }
7780 AlterTableTarget::DropColumn {
7781 column,
7782 if_exists,
7783 cascade,
7784 } => {
7785 f.write_str("DROP COLUMN ")?;
7786 if *if_exists {
7787 f.write_str("IF EXISTS ")?;
7788 }
7789 write!(f, "{}", quote_ident(column))?;
7790 if *cascade {
7791 f.write_str(" CASCADE")?;
7792 }
7793 Ok(())
7794 }
7795 AlterTableTarget::AddTableConstraint(tc) => {
7796 write!(f, "ADD {tc}")
7797 }
7798 AlterTableTarget::ValidateConstraint { name } => {
7799 write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
7800 }
7801 AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
7802 AlterTableTarget::ClusterOn { index } => match index {
7803 Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
7804 None => f.write_str("SET WITHOUT CLUSTER"),
7805 },
7806 AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
7807 // Round-trip-safe spelling: re-parsing this form lowers
7808 // back to SetColumnAutoIncrement (the nextval default is
7809 // how pg_dump says "serial").
7810 let seq = seq_name
7811 .clone()
7812 .unwrap_or_else(|| alloc::format!("{column}_seq"));
7813 write!(
7814 f,
7815 "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
7816 quote_ident(column)
7817 )
7818 }
7819 AlterTableTarget::RenameColumn { old, new } => {
7820 write!(
7821 f,
7822 "RENAME COLUMN {} TO {}",
7823 quote_ident(old),
7824 quote_ident(new)
7825 )
7826 }
7827 AlterTableTarget::RenameConstraint { old, new } => {
7828 write!(
7829 f,
7830 "RENAME CONSTRAINT {} TO {}",
7831 quote_ident(old),
7832 quote_ident(new)
7833 )
7834 }
7835 AlterTableTarget::RenameTable { new } => {
7836 write!(f, "RENAME TO {}", quote_ident(new))
7837 }
7838 AlterTableTarget::SetTriggerEnabled { which, enabled } => {
7839 f.write_str(if *enabled {
7840 "ENABLE TRIGGER "
7841 } else {
7842 "DISABLE TRIGGER "
7843 })?;
7844 match which {
7845 TriggerSelector::All => f.write_str("ALL"),
7846 TriggerSelector::Named(n) => f.write_str("e_ident(n)),
7847 }
7848 }
7849 AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
7850 (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
7851 (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
7852 (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
7853 (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
7854 (None, None) => Ok(()),
7855 },
7856 AlterTableTarget::AttachPartition { child, bounds } => {
7857 write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
7858 match bounds {
7859 PartitionOfBoundsAst::Range { lower, upper } => {
7860 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7861 }
7862 PartitionOfBoundsAst::List { values } => {
7863 f.write_str("FOR VALUES IN (")?;
7864 for (i, v) in values.iter().enumerate() {
7865 if i > 0 {
7866 f.write_str(", ")?;
7867 }
7868 write!(f, "{}", v)?;
7869 }
7870 f.write_str(")")
7871 }
7872 PartitionOfBoundsAst::Hash { modulus, remainder } => {
7873 write!(
7874 f,
7875 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7876 modulus, remainder
7877 )
7878 }
7879 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7880 }
7881 }
7882 AlterTableTarget::DetachPartition {
7883 child,
7884 concurrently,
7885 finalize,
7886 } => {
7887 write!(f, "DETACH PARTITION {}", quote_ident(child))?;
7888 if *concurrently {
7889 f.write_str(" CONCURRENTLY")?;
7890 }
7891 if *finalize {
7892 f.write_str(" FINALIZE")?;
7893 }
7894 Ok(())
7895 }
7896 AlterTableTarget::AlterColumnSetDefault {
7897 column,
7898 default_expr,
7899 } => write!(
7900 f,
7901 "ALTER COLUMN {} SET DEFAULT {}",
7902 quote_ident(column),
7903 default_expr
7904 ),
7905 AlterTableTarget::AlterColumnDropDefault { column } => {
7906 write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
7907 }
7908 AlterTableTarget::AlterColumnSetNotNull { column } => {
7909 write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
7910 }
7911 AlterTableTarget::AlterColumnDropNotNull { column } => {
7912 write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
7913 }
7914 AlterTableTarget::AlterColumnRestart { column, with } => {
7915 write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
7916 if let Some(n) = with {
7917 write!(f, " WITH {n}")?;
7918 }
7919 Ok(())
7920 }
7921 AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
7922 write!(
7923 f,
7924 "ALTER COLUMN {} DROP EXPRESSION{}",
7925 quote_ident(column),
7926 if *if_exists { " IF EXISTS" } else { "" }
7927 )
7928 }
7929 AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7930 write!(
7931 f,
7932 "ALTER COLUMN {} DROP IDENTITY{}",
7933 quote_ident(column),
7934 if *if_exists { " IF EXISTS" } else { "" }
7935 )
7936 }
7937 AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7938 write!(
7939 f,
7940 "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7941 quote_ident(column)
7942 )
7943 }
7944 }
7945}
7946
7947impl fmt::Display for TableConstraint {
7948 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7949 match self {
7950 Self::PrimaryKey { name, columns, .. } => {
7951 if let Some(n) = name {
7952 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7953 }
7954 f.write_str("PRIMARY KEY (")?;
7955 for (i, c) in columns.iter().enumerate() {
7956 if i > 0 {
7957 f.write_str(", ")?;
7958 }
7959 f.write_str("e_ident(c))?;
7960 }
7961 f.write_str(")")
7962 }
7963 Self::Unique {
7964 name,
7965 columns,
7966 nulls_not_distinct,
7967 ..
7968 } => {
7969 if let Some(n) = name {
7970 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7971 }
7972 f.write_str("UNIQUE ")?;
7973 if *nulls_not_distinct {
7974 f.write_str("NULLS NOT DISTINCT ")?;
7975 }
7976 f.write_str("(")?;
7977 for (i, c) in columns.iter().enumerate() {
7978 if i > 0 {
7979 f.write_str(", ")?;
7980 }
7981 f.write_str("e_ident(c))?;
7982 }
7983 f.write_str(")")
7984 }
7985 Self::Check {
7986 name,
7987 expr,
7988 not_valid,
7989 } => {
7990 if let Some(n) = name {
7991 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7992 }
7993 write!(f, "CHECK ({expr})")?;
7994 if *not_valid {
7995 write!(f, " NOT VALID")?;
7996 }
7997 Ok(())
7998 }
7999 Self::Index {
8000 name,
8001 columns,
8002 prefix_lengths,
8003 } => {
8004 f.write_str("KEY ")?;
8005 if let Some(n) = name {
8006 write!(f, "{} ", quote_ident(n))?;
8007 }
8008 f.write_str("(")?;
8009 for (i, c) in columns.iter().enumerate() {
8010 if i > 0 {
8011 f.write_str(", ")?;
8012 }
8013 f.write_str("e_ident(c))?;
8014 // v7.40.0 — the declared prefix rounds back with it.
8015 if let Some(Some(p)) = prefix_lengths.get(i) {
8016 write!(f, "({p})")?;
8017 }
8018 }
8019 f.write_str(")")
8020 }
8021 Self::FulltextIndex { name, columns } => {
8022 // Mysqldump emits `FULLTEXT KEY name (cols)` —
8023 // Display rounds back to that shape so dump
8024 // replay reproduces the input verbatim.
8025 f.write_str("FULLTEXT KEY ")?;
8026 if let Some(n) = name {
8027 write!(f, "{} ", quote_ident(n))?;
8028 }
8029 f.write_str("(")?;
8030 for (i, c) in columns.iter().enumerate() {
8031 if i > 0 {
8032 f.write_str(", ")?;
8033 }
8034 f.write_str("e_ident(c))?;
8035 }
8036 f.write_str(")")
8037 }
8038 Self::Exclude {
8039 name,
8040 method,
8041 elements,
8042 } => {
8043 if let Some(n) = name {
8044 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
8045 }
8046 f.write_str("EXCLUDE ")?;
8047 if let Some(m) = method {
8048 write!(f, "USING {m} ")?;
8049 }
8050 f.write_str("(")?;
8051 for (i, (col, op)) in elements.iter().enumerate() {
8052 if i > 0 {
8053 f.write_str(", ")?;
8054 }
8055 write!(f, "{} WITH {op}", quote_ident(col))?;
8056 }
8057 f.write_str(")")
8058 }
8059 }
8060 }
8061}
8062
8063impl fmt::Display for ForeignKeyConstraint {
8064 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8065 if let Some(name) = &self.name {
8066 write!(f, "CONSTRAINT {} ", quote_ident(name))?;
8067 }
8068 f.write_str("FOREIGN KEY (")?;
8069 for (i, c) in self.columns.iter().enumerate() {
8070 if i > 0 {
8071 f.write_str(", ")?;
8072 }
8073 f.write_str("e_ident(c))?;
8074 }
8075 write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
8076 if !self.parent_columns.is_empty() {
8077 f.write_str(" (")?;
8078 for (i, c) in self.parent_columns.iter().enumerate() {
8079 if i > 0 {
8080 f.write_str(", ")?;
8081 }
8082 f.write_str("e_ident(c))?;
8083 }
8084 f.write_str(")")?;
8085 }
8086 // Only render non-default actions to keep Display output
8087 // close to user input. SPG's default is RESTRICT (matches
8088 // SQL spec).
8089 if self.on_delete != FkAction::Restrict {
8090 write!(f, " ON DELETE {}", self.on_delete)?;
8091 }
8092 if self.on_update != FkAction::Restrict {
8093 write!(f, " ON UPDATE {}", self.on_update)?;
8094 }
8095 Ok(())
8096 }
8097}
8098
8099impl fmt::Display for FkAction {
8100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8101 match self {
8102 Self::Restrict => f.write_str("RESTRICT"),
8103 Self::Cascade => f.write_str("CASCADE"),
8104 Self::SetNull => f.write_str("SET NULL"),
8105 Self::SetDefault => f.write_str("SET DEFAULT"),
8106 Self::NoAction => f.write_str("NO ACTION"),
8107 }
8108 }
8109}
8110
8111impl fmt::Display for ColumnDef {
8112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8113 // v7.30.1 (mailrs round-24 class audit) — the type position
8114 // must re-parse to the same ColumnDef: a user-defined type
8115 // reference and the MySQL inline ENUM / SET value lists all
8116 // lower `ty` to Text, so rendering `ty` lost them.
8117 write!(f, "{}", quote_ident(&self.name))?;
8118 if let Some(ut) = &self.user_type_ref {
8119 write!(f, " {}", quote_ident(ut))?;
8120 } else if let Some(variants) = &self.inline_enum_variants {
8121 write_variant_list(f, "ENUM", variants)?;
8122 } else if let Some(variants) = &self.inline_set_variants {
8123 write_variant_list(f, "SET", variants)?;
8124 } else {
8125 write!(f, " {}", self.ty)?;
8126 }
8127 if self.is_unsigned {
8128 f.write_str(" UNSIGNED")?;
8129 }
8130 // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
8131 // DDL. Only emits when non-default so the typical output
8132 // stays unchanged.
8133 match self.collation {
8134 Collation::Binary => {}
8135 Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
8136 }
8137 if let Some(d) = &self.default {
8138 write!(f, " DEFAULT {d}")?;
8139 }
8140 if self.auto_increment {
8141 f.write_str(" AUTO_INCREMENT")?;
8142 }
8143 if !self.nullable {
8144 f.write_str(" NOT NULL")?;
8145 }
8146 // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
8147 // is NOT lifted to a table-level constraint at parse time
8148 // (unlike UNIQUE / CHECK), so the WAL round trip of a
8149 // prepared CREATE TABLE silently dropped the primary key.
8150 if self.is_primary_key {
8151 f.write_str(" PRIMARY KEY")?;
8152 }
8153 // The parser accepts only CURRENT_TIMESTAMP here (stored as
8154 // now()), so that spelling is the lossless round trip.
8155 if self.on_update_runtime.is_some() {
8156 f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
8157 }
8158 // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
8159 // replay reconstructs the computed-column declaration. The
8160 // expression sits inside a single set of parens; STORED is
8161 // the only variant the parser accepts.
8162 if let Some(gen_expr) = &self.generated_stored_expr {
8163 write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
8164 }
8165 Ok(())
8166 }
8167}
8168
8169/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
8170/// types (MySQL flavour; `ty` is Text underneath).
8171fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
8172 write!(f, " {kw}(")?;
8173 for (i, v) in variants.iter().enumerate() {
8174 if i > 0 {
8175 f.write_str(", ")?;
8176 }
8177 write!(f, "'{}'", v.replace('\'', "''"))?;
8178 }
8179 f.write_str(")")
8180}
8181
8182impl fmt::Display for InsertStatement {
8183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8184 write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
8185 if let Some(cols) = &self.columns {
8186 f.write_str(" (")?;
8187 for (i, c) in cols.iter().enumerate() {
8188 if i > 0 {
8189 f.write_str(", ")?;
8190 }
8191 f.write_str("e_ident(c))?;
8192 }
8193 f.write_str(")")?;
8194 }
8195 // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
8196 // skipping the VALUES list (mailrs round-5 G4).
8197 if let Some(sel) = &self.select_source {
8198 write!(f, " {sel}")?;
8199 } else {
8200 f.write_str(" VALUES ")?;
8201 for (ri, row) in self.rows.iter().enumerate() {
8202 if ri > 0 {
8203 f.write_str(", ")?;
8204 }
8205 f.write_str("(")?;
8206 for (i, v) in row.iter().enumerate() {
8207 if i > 0 {
8208 f.write_str(", ")?;
8209 }
8210 write!(f, "{v}")?;
8211 }
8212 f.write_str(")")?;
8213 }
8214 }
8215 // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
8216 // Display round trip: WAL persistence renders the bind-final
8217 // AST through this impl, and a replayed bare INSERT turns a
8218 // legal upsert no-op into a UNIQUE violation that refuses to
8219 // open the catalog.
8220 if let Some(oc) = &self.on_conflict {
8221 write!(f, " {oc}")?;
8222 }
8223 write_returning(self.returning.as_deref(), f)?;
8224 Ok(())
8225 }
8226}
8227
8228/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
8229/// parser produced, so the AST→SQL round trip preserves upsert
8230/// semantics (WAL replay depends on it).
8231impl fmt::Display for OnConflictClause {
8232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8233 f.write_str("ON CONFLICT")?;
8234 if let Some(name) = &self.constraint_name {
8235 write!(f, " ON CONSTRAINT {name}")?;
8236 }
8237 if !self.target_columns.is_empty() {
8238 f.write_str(" (")?;
8239 for (i, c) in self.target_columns.iter().enumerate() {
8240 if i > 0 {
8241 f.write_str(", ")?;
8242 }
8243 f.write_str("e_ident(c))?;
8244 }
8245 f.write_str(")")?;
8246 }
8247 if let Some(w) = &self.index_where {
8248 write!(f, " WHERE {w}")?;
8249 }
8250 match &self.action {
8251 OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
8252 OnConflictAction::Update {
8253 assignments,
8254 where_,
8255 } => {
8256 f.write_str(" DO UPDATE SET ")?;
8257 for (i, (col, expr)) in assignments.iter().enumerate() {
8258 if i > 0 {
8259 f.write_str(", ")?;
8260 }
8261 write!(f, "{} = {expr}", quote_ident(col))?;
8262 }
8263 if let Some(w) = where_ {
8264 write!(f, " WHERE {w}")?;
8265 }
8266 Ok(())
8267 }
8268 }
8269 }
8270}
8271
8272/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
8273/// tail for the three DML Display impls.
8274fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8275 let Some(items) = ret else {
8276 return Ok(());
8277 };
8278 f.write_str(" RETURNING ")?;
8279 for (i, item) in items.iter().enumerate() {
8280 if i > 0 {
8281 f.write_str(", ")?;
8282 }
8283 write!(f, "{item}")?;
8284 }
8285 Ok(())
8286}
8287
8288impl fmt::Display for UpdateStatement {
8289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8290 write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
8291 for (i, (col, expr)) in self.assignments.iter().enumerate() {
8292 if i > 0 {
8293 f.write_str(", ")?;
8294 }
8295 write!(f, "{} = {expr}", quote_ident(col))?;
8296 }
8297 if let Some(w) = &self.where_ {
8298 write!(f, " WHERE {w}")?;
8299 }
8300 // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
8301 if let Some(ol) = self.order_limit.as_deref() {
8302 if !ol.order_by.is_empty() {
8303 f.write_str(" ORDER BY ")?;
8304 for (i, o) in ol.order_by.iter().enumerate() {
8305 if i > 0 {
8306 f.write_str(", ")?;
8307 }
8308 write!(f, "{}", o.expr)?;
8309 if o.desc {
8310 f.write_str(" DESC")?;
8311 }
8312 match o.nulls_first {
8313 Some(true) => f.write_str(" NULLS FIRST")?,
8314 Some(false) => f.write_str(" NULLS LAST")?,
8315 None => {}
8316 }
8317 }
8318 }
8319 if let Some(n) = ol.limit {
8320 write!(f, " LIMIT {n}")?;
8321 }
8322 }
8323 write_returning(self.returning.as_deref(), f)?;
8324 Ok(())
8325 }
8326}
8327
8328impl fmt::Display for DeleteStatement {
8329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8330 write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
8331 if let Some(w) = &self.where_ {
8332 write!(f, " WHERE {w}")?;
8333 }
8334 write_returning(self.returning.as_deref(), f)?;
8335 Ok(())
8336 }
8337}
8338
8339impl fmt::Display for CteBody {
8340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8341 match self {
8342 Self::Select(s) => write!(f, "{s}"),
8343 Self::Insert(s) => write!(f, "{s}"),
8344 Self::Update(s) => write!(f, "{s}"),
8345 Self::Delete(s) => write!(f, "{s}"),
8346 Self::Merge(s) => write!(f, "{s}"),
8347 }
8348 }
8349}
8350
8351impl fmt::Display for MergeStatement {
8352 // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
8353 // (it round-trips for the cases tests cover, not for
8354 // round-tripping every edge of the surface).
8355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8356 fmt_with_clause(&self.ctes, f)?;
8357 f.write_str("MERGE INTO ")?;
8358 write!(f, "{}", quote_ident(&self.target))?;
8359 if let Some(a) = &self.target_alias {
8360 write!(f, " {}", quote_ident(a))?;
8361 }
8362 f.write_str(" USING ")?;
8363 if let Some(sub) = &self.source_select {
8364 write!(f, "({sub})")?;
8365 } else {
8366 write!(f, "{}", quote_ident(&self.source))?;
8367 }
8368 if let Some(a) = &self.source_alias {
8369 write!(f, " {}", quote_ident(a))?;
8370 }
8371 if !self.source_column_aliases.is_empty() {
8372 f.write_str("(")?;
8373 for (i, c) in self.source_column_aliases.iter().enumerate() {
8374 if i > 0 {
8375 f.write_str(", ")?;
8376 }
8377 write!(f, "{}", quote_ident(c))?;
8378 }
8379 f.write_str(")")?;
8380 }
8381 write!(f, " ON {}", self.on)?;
8382 for clause in &self.clauses {
8383 f.write_str(" WHEN ")?;
8384 f.write_str(match clause.matched {
8385 MergeMatched::Matched => "MATCHED",
8386 MergeMatched::NotMatched => "NOT MATCHED",
8387 MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
8388 })?;
8389 if let Some(c) = &clause.condition {
8390 write!(f, " AND {c}")?;
8391 }
8392 f.write_str(" THEN ")?;
8393 match &clause.action {
8394 MergeAction::Insert { columns, values } => {
8395 f.write_str("INSERT ")?;
8396 // A column list is optional (round 146): the bare
8397 // `INSERT VALUES (…)` form maps positionally.
8398 if !columns.is_empty() {
8399 f.write_str("(")?;
8400 for (i, c) in columns.iter().enumerate() {
8401 if i > 0 {
8402 f.write_str(", ")?;
8403 }
8404 write!(f, "{}", quote_ident(c))?;
8405 }
8406 f.write_str(") ")?;
8407 }
8408 f.write_str("VALUES (")?;
8409 for (i, v) in values.iter().enumerate() {
8410 if i > 0 {
8411 f.write_str(", ")?;
8412 }
8413 write!(f, "{v}")?;
8414 }
8415 f.write_str(")")?;
8416 }
8417 MergeAction::Update { assignments } => {
8418 f.write_str("UPDATE SET ")?;
8419 for (i, (c, e)) in assignments.iter().enumerate() {
8420 if i > 0 {
8421 f.write_str(", ")?;
8422 }
8423 write!(f, "{} = {e}", quote_ident(c))?;
8424 }
8425 }
8426 MergeAction::Delete => f.write_str("DELETE")?,
8427 MergeAction::DoNothing => f.write_str("DO NOTHING")?,
8428 }
8429 }
8430 if let Some(items) = &self.returning {
8431 f.write_str(" RETURNING ")?;
8432 for (i, it) in items.iter().enumerate() {
8433 if i > 0 {
8434 f.write_str(", ")?;
8435 }
8436 write!(f, "{it}")?;
8437 }
8438 }
8439 Ok(())
8440 }
8441}
8442
8443/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
8444/// carry a CTE list and must round-trip it identically.
8445fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
8446 if ctes.is_empty() {
8447 return Ok(());
8448 }
8449 f.write_str("WITH ")?;
8450 if ctes.iter().any(|c| c.recursive) {
8451 f.write_str("RECURSIVE ")?;
8452 }
8453 for (i, cte) in ctes.iter().enumerate() {
8454 if i > 0 {
8455 f.write_str(", ")?;
8456 }
8457 f.write_str("e_ident(&cte.name))?;
8458 if !cte.column_overrides.is_empty() {
8459 f.write_str(" (")?;
8460 for (ci, c) in cte.column_overrides.iter().enumerate() {
8461 if ci > 0 {
8462 f.write_str(", ")?;
8463 }
8464 f.write_str("e_ident(c))?;
8465 }
8466 f.write_str(")")?;
8467 }
8468 write!(f, " AS ({})", cte.body)?;
8469 }
8470 f.write_str(" ")
8471}
8472
8473impl fmt::Display for SelectStatement {
8474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8475 // v7.30.1 (mailrs round-24 class audit) — the WITH clause
8476 // must survive the round trip; a CTE-using statement
8477 // re-parsed without it references undefined tables.
8478 fmt_with_clause(&self.ctes, f)?;
8479 write_bare_select(self, f)?;
8480 for (kind, peer) in &self.unions {
8481 f.write_str(match kind {
8482 UnionKind::Distinct => " UNION ",
8483 UnionKind::All => " UNION ALL ",
8484 UnionKind::Intersect => " INTERSECT ",
8485 UnionKind::IntersectAll => " INTERSECT ALL ",
8486 UnionKind::Except => " EXCEPT ",
8487 UnionKind::ExceptAll => " EXCEPT ALL ",
8488 })?;
8489 write_bare_select(peer, f)?;
8490 }
8491 if !self.order_by.is_empty() {
8492 f.write_str(" ORDER BY ")?;
8493 for (i, o) in self.order_by.iter().enumerate() {
8494 if i > 0 {
8495 f.write_str(", ")?;
8496 }
8497 write!(f, "{}", o.expr)?;
8498 if o.desc {
8499 f.write_str(" DESC")?;
8500 }
8501 match o.nulls_first {
8502 Some(true) => f.write_str(" NULLS FIRST")?,
8503 Some(false) => f.write_str(" NULLS LAST")?,
8504 None => {}
8505 }
8506 }
8507 }
8508 // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
8509 // exists in the FETCH FIRST spelling; rendering it as LIMIT
8510 // dropped the tie-extension semantics on replay. The parser
8511 // accepts OFFSET before FETCH, so keep that order here.
8512 if self.limit_with_ties {
8513 if let Some(o) = &self.offset {
8514 write!(f, " OFFSET {o}")?;
8515 }
8516 if let Some(n) = &self.limit {
8517 write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
8518 }
8519 } else {
8520 if let Some(n) = &self.limit {
8521 write!(f, " LIMIT {n}")?;
8522 }
8523 if let Some(o) = &self.offset {
8524 write!(f, " OFFSET {o}")?;
8525 }
8526 }
8527 Ok(())
8528 }
8529}
8530
8531fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8532 f.write_str("SELECT ")?;
8533 if s.distinct {
8534 f.write_str("DISTINCT ")?;
8535 }
8536 write_bare_select_body(s, f)
8537}
8538
8539fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8540 for (i, item) in s.items.iter().enumerate() {
8541 if i > 0 {
8542 f.write_str(", ")?;
8543 }
8544 write!(f, "{item}")?;
8545 }
8546 if let Some(t) = &s.from {
8547 write!(f, " FROM {t}")?;
8548 }
8549 if let Some(e) = &s.where_ {
8550 write!(f, " WHERE {e}")?;
8551 }
8552 if let Some(gs) = &s.group_by {
8553 f.write_str(" GROUP BY ")?;
8554 for (i, g) in gs.iter().enumerate() {
8555 if i > 0 {
8556 f.write_str(", ")?;
8557 }
8558 write!(f, "{g}")?;
8559 }
8560 } else if s.group_by_all {
8561 // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
8562 // shortcut parses to group_by: None + this flag; dropping
8563 // it turned an aggregate query into a bare projection on
8564 // re-parse.
8565 f.write_str(" GROUP BY ALL")?;
8566 }
8567 if let Some(h) = &s.having {
8568 write!(f, " HAVING {h}")?;
8569 }
8570 Ok(())
8571}
8572
8573impl fmt::Display for SelectItem {
8574 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8575 match self {
8576 Self::Wildcard => f.write_str("*"),
8577 Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
8578 Self::Expr { expr, alias } => {
8579 write!(f, "{expr}")?;
8580 if let Some(a) = alias {
8581 write!(f, " AS {}", quote_ident(a))?;
8582 }
8583 Ok(())
8584 }
8585 }
8586 }
8587}
8588
8589impl fmt::Display for FromClause {
8590 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8591 write!(f, "{}", self.primary)?;
8592 for j in &self.joins {
8593 match j.kind {
8594 JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
8595 JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
8596 JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
8597 JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
8598 JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
8599 JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
8600 }
8601 if let Some(on) = &j.on {
8602 write!(f, " ON {on}")?;
8603 }
8604 }
8605 Ok(())
8606 }
8607}
8608
8609/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
8610/// for NESTED). Kept close to the parser's grammar so it re-parses.
8611fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
8612 for (i, c) in cols.iter().enumerate() {
8613 if i > 0 {
8614 f.write_str(", ")?;
8615 }
8616 match c {
8617 JsonTableColumn::Ordinality { name } => {
8618 write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
8619 }
8620 JsonTableColumn::Nested { path, columns } => {
8621 write!(f, "NESTED PATH '{path}' COLUMNS (")?;
8622 fmt_json_table_columns(f, columns)?;
8623 f.write_str(")")?;
8624 }
8625 JsonTableColumn::Regular {
8626 name,
8627 ty,
8628 path,
8629 exists,
8630 format_json,
8631 wrapper,
8632 on_empty,
8633 on_error,
8634 } => {
8635 write!(f, "{} {ty}", quote_ident(name))?;
8636 if *format_json {
8637 f.write_str(" FORMAT JSON")?;
8638 }
8639 if *exists {
8640 write!(f, " EXISTS PATH '{path}'")?;
8641 } else {
8642 write!(f, " PATH '{path}'")?;
8643 }
8644 if *wrapper {
8645 f.write_str(" WITH WRAPPER")?;
8646 }
8647 if let JsonTableOnBehavior::Error = on_empty {
8648 f.write_str(" ERROR ON EMPTY")?;
8649 } else if let JsonTableOnBehavior::Default(e) = on_empty {
8650 write!(f, " DEFAULT {e} ON EMPTY")?;
8651 }
8652 if let JsonTableOnBehavior::Error = on_error {
8653 f.write_str(" ERROR ON ERROR")?;
8654 } else if let JsonTableOnBehavior::Default(e) = on_error {
8655 write!(f, " DEFAULT {e} ON ERROR")?;
8656 }
8657 }
8658 }
8659 }
8660 Ok(())
8661}
8662
8663impl fmt::Display for TableRef {
8664 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8665 // v7.30.1 (mailrs round-24 class audit) — the dynamic
8666 // table-ref shapes must round-trip: rendering only the
8667 // (synthetic) name turned LATERAL / unnest() /
8668 // generate_series() into references to nonexistent tables
8669 // on re-parse.
8670 // v7.39 (round 205) — JSON_TABLE round-trips through Display
8671 // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
8672 if let Some(jt) = &self.json_table {
8673 write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
8674 if !jt.passing.is_empty() {
8675 f.write_str(" PASSING ")?;
8676 for (i, (n, e)) in jt.passing.iter().enumerate() {
8677 if i > 0 {
8678 f.write_str(", ")?;
8679 }
8680 write!(f, "{e} AS {}", quote_ident(n))?;
8681 }
8682 }
8683 f.write_str(" COLUMNS (")?;
8684 fmt_json_table_columns(f, &jt.columns)?;
8685 f.write_str(")")?;
8686 if let Some(a) = &self.alias {
8687 write!(f, " AS {}", quote_ident(a))?;
8688 }
8689 return Ok(());
8690 }
8691 if let Some(inner) = &self.lateral_subquery {
8692 write!(f, "LATERAL ({inner})")?;
8693 if let Some(a) = &self.alias {
8694 write!(f, " AS {}", quote_ident(a))?;
8695 // v7.37 D.28 — a derived table on the lateral_subquery channel
8696 // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
8697 // lowers here). Rendering the alias without the column list lost
8698 // the column names on re-parse (a view body round-trips through
8699 // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
8700 if !self.unnest_column_aliases.is_empty() {
8701 f.write_str(" (")?;
8702 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8703 if i > 0 {
8704 f.write_str(", ")?;
8705 }
8706 f.write_str("e_ident(c))?;
8707 }
8708 f.write_str(")")?;
8709 }
8710 }
8711 return Ok(());
8712 }
8713 if let Some(expr) = &self.unnest_expr {
8714 write!(f, "UNNEST({expr})")?;
8715 if let Some(a) = &self.alias {
8716 write!(f, " AS {}", quote_ident(a))?;
8717 if !self.unnest_column_aliases.is_empty() {
8718 f.write_str(" (")?;
8719 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8720 if i > 0 {
8721 f.write_str(", ")?;
8722 }
8723 f.write_str("e_ident(c))?;
8724 }
8725 f.write_str(")")?;
8726 }
8727 }
8728 return Ok(());
8729 }
8730 // 7.38.1 S5.1 — a FROM-position table function must re-render
8731 // as the CALL, not its bare name: ARRAY(subquery) desugars by
8732 // re-parsing the subquery's canonical text, and a dropped
8733 // argument list turned `pg_options_to_table(x)` into a
8734 // relation lookup that does not exist.
8735 if let Some(call) = &self.table_fn_call {
8736 let (fn_name, args) = call.as_ref();
8737 write!(f, "{fn_name}(")?;
8738 for (i, a) in args.iter().enumerate() {
8739 if i > 0 {
8740 f.write_str(", ")?;
8741 }
8742 write!(f, "{a}")?;
8743 }
8744 f.write_str(")")?;
8745 if let Some(a) = &self.alias {
8746 write!(f, " AS {}", quote_ident(a))?;
8747 if !self.unnest_column_aliases.is_empty() {
8748 f.write_str("(")?;
8749 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8750 if i > 0 {
8751 f.write_str(", ")?;
8752 }
8753 write!(f, "{}", quote_ident(c))?;
8754 }
8755 f.write_str(")")?;
8756 }
8757 }
8758 return Ok(());
8759 }
8760 if let Some(args) = &self.generate_series_args {
8761 f.write_str("generate_series(")?;
8762 for (i, a) in args.iter().enumerate() {
8763 if i > 0 {
8764 f.write_str(", ")?;
8765 }
8766 write!(f, "{a}")?;
8767 }
8768 f.write_str(")")?;
8769 if let Some(a) = &self.alias {
8770 write!(f, " AS {}", quote_ident(a))?;
8771 }
8772 return Ok(());
8773 }
8774 write!(f, "{}", quote_ident(&self.name))?;
8775 if let Some(seg) = self.as_of_segment {
8776 write!(f, " AS OF SEGMENT {seg}")?;
8777 }
8778 if let Some(a) = &self.alias {
8779 write!(f, " AS {}", quote_ident(a))?;
8780 }
8781 Ok(())
8782 }
8783}
8784
8785impl fmt::Display for ColumnName {
8786 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8787 if let Some(q) = &self.qualifier {
8788 write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
8789 } else {
8790 write!(f, "{}", quote_ident(&self.name))
8791 }
8792 }
8793}
8794
8795/// v7.39 (round 311) — render the left spine of an AND / OR chain
8796/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
8797/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
8798/// SAME operator flattens; anything else is an ordinary operand.
8799fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
8800 if let Expr::Binary {
8801 lhs,
8802 op: inner,
8803 rhs,
8804 } = e
8805 && *inner == op
8806 {
8807 write_bool_chain(f, lhs, op)?;
8808 return write!(f, " {op} {rhs}");
8809 }
8810 write!(f, "{e}")
8811}
8812
8813/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
8814/// form `pg_get_constraintdef(oid, true)` and friends return.
8815///
8816/// The default [`fmt::Display`] parenthesises every operator node, which
8817/// is what PG's non-pretty deparse does and what makes the text
8818/// round-trip. Pretty drops the pairs the grammar can put back, and the
8819/// rule is NOT plain precedence minimisation — measured against PG 18.4
8820/// across 37 shapes:
8821///
8822/// * the boolean layer follows precedence (NOT > AND > OR): an OR
8823/// under an AND keeps its parens, an AND under an OR does not, and a
8824/// comparison under any of them does not (`NOT a > 1`);
8825/// * an associative chain flattens completely, even where the source
8826/// nested it to the right (`a AND (b AND c)` prints as one chain);
8827/// * but an operand of a comparison or arithmetic operator keeps its
8828/// parens whenever it is itself an operator expression — so
8829/// `(a + b) > 10` and `(- a) + b`, even though precedence alone
8830/// would not require either. A cast, function call, column or
8831/// literal in that position does not (`a::text = t`,
8832/// `length(code) > 2`); a cast counts as compound exactly when the
8833/// thing it casts is (`((a + b)::text) = t`).
8834///
8835/// Anything outside that layer defers to `Display`, which is never
8836/// wrong — only more parenthesised than PG would print.
8837#[must_use]
8838pub fn pretty_expr(e: &Expr) -> String {
8839 let mut out = String::new();
8840 write_pretty(&mut out, e, PrettyParent::None, false, false);
8841 out
8842}
8843
8844/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
8845/// writes it.
8846///
8847/// MariaDB names the offending expression in its out-of-range message
8848/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
8849/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
8850/// MySQL client, for a cast the client had just written the other way.
8851#[must_use]
8852pub fn pretty_expr_mysql(e: &Expr) -> String {
8853 let mut out = String::new();
8854 write_pretty(&mut out, e, PrettyParent::None, false, true);
8855 out
8856}
8857
8858/// v7.39 (round 505) — how strongly an expression suggests its own column
8859/// name. A cast keeps its argument's name only when that name is STRONG;
8860/// otherwise the cast reports the type it casts to.
8861///
8862/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
8863/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
8864/// itself `text` — so `case` and a function name cannot be the same kind of
8865/// answer, even though a bare `CASE …` does report `case`.
8866#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8867enum NameStrength {
8868 /// Nothing to go on — PG reports `?column?`.
8869 None,
8870 /// A name, but one a cast overrides: `case`, or a type name.
8871 Weak,
8872 /// A name a cast keeps: a column, or the function that produced it.
8873 Strong,
8874}
8875
8876/// v7.39 (round 505) — the column name PG18 gives a projected expression
8877/// that carries no `AS` alias. `None` means `?column?`.
8878///
8879/// SPG used to print the parsed expression back out, which matched neither
8880/// oracle and made name-keyed row access miss on both wires:
8881///
8882/// | query | PG18 | SPG (before) |
8883/// |--------------|------------|--------------|
8884/// | `upper(s)` | `upper` | `upper(s)` |
8885/// | `a+b` | `?column?` | `(a + b)` |
8886/// | `'lit'` | `?column?` | `'lit'` |
8887/// | `CASE …` | `case` | `CASE WHEN (a = 1) THEN …` |
8888///
8889/// Every rule below is one of those measurements, taken with `\gdesc`
8890/// against PG18: a call is named for its function, a cast recurses into its
8891/// argument and falls back to the type, a scalar subquery takes the name of
8892/// the column it selects, and operators have no name at all.
8893#[must_use]
8894pub fn figure_column_name(expr: &Expr) -> Option<String> {
8895 let (name, _) = figure_name_inner(expr);
8896 name
8897}
8898
8899/// The name a function reports, which is not always the name SPG parsed it
8900/// under: `count(*)` is held as `count_star` so the star arity survives the
8901/// AST, and that internal spelling must not reach a client. PG18 reports
8902/// `count`.
8903/// v7.39.13 — public, because Describe was naming the same call from a
8904/// second map that did not have this entry.
8905///
8906/// `count(*)` is held as `count_star` so the star arity survives the
8907/// AST. The projection mapped it back and the extended protocol's
8908/// Describe did not, so `SELECT count(*) OVER ()` answered `count` in
8909/// the row stream and `count_star` to `\gdesc` — an ORM-visible column
8910/// name, and two answers to one question. Reported by sentori against
8911/// 7.39.12.
8912#[must_use]
8913pub fn canonical_function_name(name: &str) -> String {
8914 match name {
8915 "count_star" => "count".to_string(),
8916 other => other.to_ascii_lowercase(),
8917 }
8918}
8919
8920/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
8921/// reports when its operand has none of its own. Only the spellings that
8922/// differ from what the user writes need an entry; everything else is
8923/// already its own typname.
8924fn cast_target_typname(target: &CastTarget) -> String {
8925 let written = target.to_string().to_ascii_lowercase();
8926 let base = written.strip_suffix("[]").unwrap_or(&written);
8927 let mapped = match base {
8928 "bigint" => "int8",
8929 "integer" | "int" => "int4",
8930 "smallint" => "int2",
8931 "boolean" => "bool",
8932 "double precision" => "float8",
8933 "real" => "float4",
8934 "character varying" => "varchar",
8935 "character" => "bpchar",
8936 "timestamp with time zone" => "timestamptz",
8937 "timestamp without time zone" => "timestamp",
8938 "time without time zone" => "time",
8939 "decimal" => "numeric",
8940 other => other,
8941 };
8942 if written.ends_with("[]") {
8943 alloc::format!("_{mapped}")
8944 } else {
8945 String::from(mapped)
8946 }
8947}
8948
8949fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8950 let strong = |n: String| (Some(n), NameStrength::Strong);
8951 match expr {
8952 // A column keeps its own name, qualifier and all discarded:
8953 // `lbl.a` reports `a`.
8954 Expr::Column(c) => strong(c.name.clone()),
8955 // Calls are named for the function. This covers the shapes that
8956 // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8957 // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8958 // because PG resolves them to functions before naming them.
8959 Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8960 strong(canonical_function_name(name))
8961 }
8962 Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8963 Expr::Extract { .. } => strong("extract".to_string()),
8964 Expr::Exists { .. } => strong("exists".to_string()),
8965 Expr::Array(_) => strong("array".to_string()),
8966 // `(expr).field` is named for the field, as a column would be.
8967 Expr::FieldAccess { field, .. } => strong(field.clone()),
8968 // v7.39.12 — PostgreSQL names a subscript after its operand, so
8969 // `arr[1]` is `arr`. There was no arm, so it fell through to
8970 // `?column?`. Reported by sentori against 7.39.11 — the same
8971 // naming defect v7.38.20 closed, reached through a different
8972 // expression. Weak, like the field access above it: an outer
8973 // cast or function still names the column.
8974 Expr::ArraySubscript { target, .. } => (figure_name_inner(target).0, NameStrength::Weak),
8975 // A cast prefers its argument's name and settles for the type:
8976 // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8977 Expr::Cast {
8978 expr: inner,
8979 target,
8980 } => match figure_name_inner(inner) {
8981 (Some(n), NameStrength::Strong) => strong(n),
8982 // v7.38.7 — the fallback is the target type's INTERNAL name,
8983 // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8984 // the `bigint` the user typed. Measured on PG18 alongside
8985 // `CAST(7 AS bigint)`, which answers `int8` too.
8986 _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8987 },
8988 // A scalar subquery reports whatever its single output column
8989 // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8990 Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8991 // `CASE …` names itself, but weakly — a cast around it wins.
8992 Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8993 // A literal that carries its own type names itself for that type:
8994 // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8995 // reports nothing. Weak, like any other type name.
8996 Expr::Literal(Literal::Interval { .. }) => {
8997 (Some("interval".to_string()), NameStrength::Weak)
8998 }
8999 // A wrapper that adds no name of its own.
9000 Expr::Variadic(inner) => figure_name_inner(inner),
9001 Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
9002 // Everything else — operators, comparisons, IS NULL, LIKE, IN,
9003 // literals, placeholders — reports `?column?`.
9004 _ => (None, NameStrength::None),
9005 }
9006}
9007
9008/// The name a scalar subquery's single projected column reports.
9009fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
9010 match sel.items.as_slice() {
9011 [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
9012 [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
9013 _ => (None, NameStrength::None),
9014 }
9015}
9016
9017/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
9018fn pretty_prec(e: &Expr) -> u8 {
9019 match e {
9020 Expr::Binary { op, .. } => match op {
9021 // v7.39 (round 407) — this deparse ladder mirrors the parser's:
9022 // OR < XOR < AND < NOT < comparison < additive < multiplicative.
9023 // XOR (MySQL-only) sits between OR and AND, so AND and everything
9024 // above shifted +1 to open rung 2 for it.
9025 BinOp::Or => 1,
9026 BinOp::LogicalXor => 2,
9027 BinOp::And => 3,
9028 BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
9029 BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
9030 // Everything else in this enum is a comparison-shaped
9031 // operator; they share one level, as in the grammar.
9032 _ => 5,
9033 },
9034 Expr::Unary { op, .. } => match op {
9035 UnOp::Not => 4,
9036 UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
9037 },
9038 _ => u8::MAX,
9039 }
9040}
9041
9042/// Is this node an operator expression — the thing an arithmetic or
9043/// comparison parent keeps parentheses around? A cast inherits the
9044/// answer from what it casts.
9045fn pretty_is_compound(e: &Expr) -> bool {
9046 match e {
9047 Expr::Binary { .. } | Expr::Unary { .. } => true,
9048 Expr::Cast { expr, .. } => pretty_is_compound(expr),
9049 _ => false,
9050 }
9051}
9052
9053/// `parent` describes the enclosing operator: its binding power, and
9054/// whether it is a comparison (which keeps parens around any operator
9055/// operand) or a NOT (which keeps them at equal power too).
9056#[derive(Clone, Copy, PartialEq)]
9057enum PrettyParent {
9058 /// Nothing encloses this node.
9059 None,
9060 /// A comparison-shaped operator: an operator operand always keeps
9061 /// its parens, whatever precedence would allow.
9062 Comparison,
9063 /// Arithmetic / concatenation: precedence decides.
9064 Arith(u8),
9065 /// A boolean connective: precedence decides.
9066 Bool(u8),
9067 /// `NOT`: precedence decides, but equal power still needs parens so
9068 /// `NOT (NOT a > 1)` does not collapse.
9069 Not,
9070}
9071
9072fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
9073 let prec = pretty_prec(e);
9074 let is_unary_sign = matches!(
9075 e,
9076 Expr::Unary {
9077 op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
9078 ..
9079 }
9080 );
9081 let needs = match parent {
9082 PrettyParent::None => false,
9083 PrettyParent::Comparison => pretty_is_compound(e),
9084 // A sign always keeps its parens under an operator — PG writes
9085 // `(- a) + b` even though precedence would not require it.
9086 PrettyParent::Arith(p) => {
9087 is_unary_sign
9088 || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
9089 && (prec < p || (prec == p && is_rhs)))
9090 }
9091 PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
9092 PrettyParent::Not => {
9093 matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
9094 }
9095 };
9096 if needs {
9097 out.push('(');
9098 }
9099 match e {
9100 Expr::Binary { lhs, op, rhs } => {
9101 let child = match op {
9102 BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
9103 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
9104 PrettyParent::Arith(prec)
9105 }
9106 _ => PrettyParent::Comparison,
9107 };
9108 write_pretty(out, lhs, child, false, mysql);
9109 out.push(' ');
9110 out.push_str(&alloc::format!("{op}"));
9111 out.push(' ');
9112 // AND / OR are associative, so an explicitly right-nested
9113 // chain still prints as one chain.
9114 let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
9115 write_pretty(out, rhs, child, rhs_is_rhs, mysql);
9116 }
9117 Expr::Unary { op, expr } => match op {
9118 UnOp::Not => {
9119 out.push_str("NOT ");
9120 write_pretty(out, expr, PrettyParent::Not, false, mysql);
9121 }
9122 UnOp::Neg => {
9123 out.push_str("- ");
9124 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9125 }
9126 UnOp::Plus => {
9127 out.push_str("+ ");
9128 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9129 }
9130 UnOp::BitNot => {
9131 out.push('~');
9132 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9133 }
9134 },
9135 Expr::Cast { expr, target } => {
9136 if mysql {
9137 // MySQL's own spelling, which is what its error messages
9138 // quote back.
9139 out.push_str("cast(");
9140 write_pretty(out, expr, PrettyParent::None, false, mysql);
9141 out.push_str(&alloc::format!(
9142 " as {})",
9143 target.to_string().to_lowercase()
9144 ));
9145 } else {
9146 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9147 out.push_str(&alloc::format!("::{target}"));
9148 }
9149 }
9150 Expr::IsNull { expr, negated } => {
9151 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9152 out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
9153 }
9154 other => out.push_str(&alloc::format!("{other}")),
9155 }
9156 if needs {
9157 out.push(')');
9158 }
9159}
9160
9161const fn pretty_prec_not() -> u8 {
9162 // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
9163 // when the XOR insertion shifted the deparse ladder up by one).
9164 4
9165}
9166
9167impl fmt::Display for Expr {
9168 #[allow(clippy::too_many_lines)]
9169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9170 match self {
9171 Self::Literal(l) => write!(f, "{l}"),
9172 Self::Column(c) => write!(f, "{c}"),
9173 Self::Placeholder(n) => write!(f, "${n}"),
9174 // Round-trips as the spelling PG's docs lead with.
9175 Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
9176 Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
9177 // Round-trips with the name quoted, which is how PG spells a
9178 // collation everywhere: `"en_US.utf8"`, `"C"`.
9179 Self::Collate { expr, collation } => {
9180 write!(f, "{expr} COLLATE {}", quote_ident(collation))
9181 }
9182 // v7.39 (round 311) — an AND / OR chain that nests to the
9183 // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
9184 // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
9185 // its parentheses, because that is a different grouping as
9186 // written. Both halves measured against PG 18.4's deparse,
9187 // which flattens a same-operator left chain at parse time and
9188 // leaves `a AND (b AND c)` alone.
9189 Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
9190 f.write_str("(")?;
9191 write_bool_chain(f, lhs, *op)?;
9192 write!(f, " {op} {rhs}")?;
9193 f.write_str(")")
9194 }
9195 Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
9196 Self::Unary { op, expr } => match op {
9197 UnOp::Not => write!(f, "(NOT {expr})"),
9198 // A space after the sign, as PG's deparse writes it.
9199 UnOp::Neg => write!(f, "(- {expr})"),
9200 UnOp::Plus => write!(f, "(+ {expr})"),
9201 UnOp::BitNot => write!(f, "(~{expr})"),
9202 },
9203 // The OPERAND carries the parentheses, not the cast:
9204 // `(a)::text`, `((a + b))::text`. PG words it this way, and
9205 // it is what keeps `a::text = t` from reading as a cast of
9206 // the comparison.
9207 Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
9208 Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
9209 Self::AggregateOrdered {
9210 call,
9211 order_by,
9212 distinct,
9213 filter,
9214 } => {
9215 let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
9216 for (i, o) in order_by.iter().enumerate() {
9217 if i > 0 {
9218 f.write_str(", ")?;
9219 }
9220 write!(f, "{}", o.expr)?;
9221 if o.desc {
9222 f.write_str(" DESC")?;
9223 }
9224 match o.nulls_first {
9225 Some(true) => f.write_str(" NULLS FIRST")?,
9226 Some(false) => f.write_str(" NULLS LAST")?,
9227 None => {}
9228 }
9229 }
9230 Ok(())
9231 };
9232 // Ordered-set aggregates (`percentile_cont(f) WITHIN
9233 // GROUP (ORDER BY x)`) render the in-parens args as the
9234 // direct argument and the sort spec under WITHIN GROUP —
9235 // not as an in-argument ORDER BY.
9236 let ordered_set = matches!(
9237 call.as_ref(),
9238 Expr::FunctionCall { name, .. }
9239 if matches!(
9240 name.to_ascii_lowercase().as_str(),
9241 "percentile_cont" | "percentile_disc" | "mode"
9242 )
9243 );
9244 if ordered_set {
9245 write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
9246 fmt_order_by(f)?;
9247 f.write_str(")")?;
9248 } else {
9249 // `name([DISTINCT ]args [ORDER BY …])` — peel the
9250 // inner call's parens to splice modifiers.
9251 let inner = alloc::format!("{call}");
9252 let body = inner.strip_suffix(')').unwrap_or(&inner);
9253 let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
9254 write!(f, "{head}(")?;
9255 if *distinct {
9256 f.write_str("DISTINCT ")?;
9257 }
9258 write!(f, "{args_part}")?;
9259 if !order_by.is_empty() {
9260 f.write_str(" ORDER BY ")?;
9261 fmt_order_by(f)?;
9262 }
9263 f.write_str(")")?;
9264 }
9265 if let Some(cond) = filter {
9266 write!(f, " FILTER (WHERE {cond})")?;
9267 }
9268 Ok(())
9269 }
9270 Self::IsNull { expr, negated } => {
9271 if *negated {
9272 write!(f, "({expr} IS NOT NULL)")
9273 } else {
9274 write!(f, "({expr} IS NULL)")
9275 }
9276 }
9277 Self::BoolTest {
9278 expr,
9279 value,
9280 negated,
9281 } => {
9282 let word = match value {
9283 Some(true) => "TRUE",
9284 Some(false) => "FALSE",
9285 None => "UNKNOWN",
9286 };
9287 if *negated {
9288 write!(f, "({expr} IS NOT {word})")
9289 } else {
9290 write!(f, "({expr} IS {word})")
9291 }
9292 }
9293 Self::FunctionCall { name, args } => {
9294 write!(f, "{name}(")?;
9295 for (i, a) in args.iter().enumerate() {
9296 if i > 0 {
9297 f.write_str(", ")?;
9298 }
9299 write!(f, "{a}")?;
9300 }
9301 f.write_str(")")
9302 }
9303 Self::Like {
9304 expr,
9305 pattern,
9306 negated,
9307 case_insensitive,
9308 } => {
9309 let op = match (negated, case_insensitive) {
9310 (false, false) => "LIKE",
9311 (true, false) => "NOT LIKE",
9312 (false, true) => "ILIKE",
9313 (true, true) => "NOT ILIKE",
9314 };
9315 write!(f, "({expr} {op} {pattern})")
9316 }
9317 Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
9318 Self::WindowFunction {
9319 name,
9320 args,
9321 partition_by,
9322 order_by,
9323 frame,
9324 null_treatment,
9325 filter,
9326 } => {
9327 write!(f, "{name}(")?;
9328 for (i, a) in args.iter().enumerate() {
9329 if i > 0 {
9330 f.write_str(", ")?;
9331 }
9332 write!(f, "{a}")?;
9333 }
9334 f.write_str(")")?;
9335 // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
9336 // OVER; it round-trips so a window body's Display re-parses.
9337 if let Some(cond) = filter {
9338 write!(f, " FILTER (WHERE {cond})")?;
9339 }
9340 // v7.30.1 (mailrs round-24 class audit) — IGNORE
9341 // NULLS sits between the arg list and OVER; dropping
9342 // it reverted replayed queries to RESPECT NULLS.
9343 if matches!(null_treatment, NullTreatment::Ignore) {
9344 f.write_str(" IGNORE NULLS")?;
9345 }
9346 f.write_str(" OVER (")?;
9347 if !partition_by.is_empty() {
9348 f.write_str("PARTITION BY ")?;
9349 for (i, p) in partition_by.iter().enumerate() {
9350 if i > 0 {
9351 f.write_str(", ")?;
9352 }
9353 write!(f, "{p}")?;
9354 }
9355 }
9356 if !order_by.is_empty() {
9357 if !partition_by.is_empty() {
9358 f.write_str(" ")?;
9359 }
9360 f.write_str("ORDER BY ")?;
9361 for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
9362 if i > 0 {
9363 f.write_str(", ")?;
9364 }
9365 write!(f, "{e}")?;
9366 if *desc {
9367 f.write_str(" DESC")?;
9368 }
9369 match nulls_first {
9370 Some(true) => f.write_str(" NULLS FIRST")?,
9371 Some(false) => f.write_str(" NULLS LAST")?,
9372 None => {}
9373 }
9374 }
9375 }
9376 if let Some(fr) = frame {
9377 if !partition_by.is_empty() || !order_by.is_empty() {
9378 f.write_str(" ")?;
9379 }
9380 let k = match fr.kind {
9381 FrameKind::Rows => "ROWS",
9382 FrameKind::Range => "RANGE",
9383 FrameKind::Groups => "GROUPS",
9384 };
9385 if let Some(end) = &fr.end {
9386 write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
9387 } else {
9388 write!(f, "{k} {}", fr.start)?;
9389 }
9390 }
9391 f.write_str(")")
9392 }
9393 Self::ScalarSubquery(s) => write!(f, "({s})"),
9394 Self::Exists { subquery, negated } => {
9395 if *negated {
9396 write!(f, "NOT EXISTS ({subquery})")
9397 } else {
9398 write!(f, "EXISTS ({subquery})")
9399 }
9400 }
9401 Self::InSubquery {
9402 expr,
9403 subquery,
9404 negated,
9405 } => {
9406 if *negated {
9407 write!(f, "({expr} NOT IN ({subquery}))")
9408 } else {
9409 write!(f, "({expr} IN ({subquery}))")
9410 }
9411 }
9412 Self::RowInSubquery {
9413 row,
9414 subquery,
9415 negated,
9416 } => {
9417 write!(f, "(")?;
9418 for (i, e) in row.iter().enumerate() {
9419 if i > 0 {
9420 write!(f, ", ")?;
9421 }
9422 write!(f, "{e}")?;
9423 }
9424 let kw = if *negated { ") NOT IN (" } else { ") IN (" };
9425 write!(f, "{kw}{subquery})")
9426 }
9427 Self::RowCmpSubquery { row, op, subquery } => {
9428 write!(f, "(")?;
9429 for (i, e) in row.iter().enumerate() {
9430 if i > 0 {
9431 write!(f, ", ")?;
9432 }
9433 write!(f, "{e}")?;
9434 }
9435 write!(f, ") {op} ({subquery})")
9436 }
9437 Self::InList {
9438 expr,
9439 list,
9440 negated,
9441 } => {
9442 let kw = if *negated { " NOT IN (" } else { " IN (" };
9443 write!(f, "({expr}{kw}")?;
9444 for (i, e) in list.iter().enumerate() {
9445 if i > 0 {
9446 f.write_str(", ")?;
9447 }
9448 write!(f, "{e}")?;
9449 }
9450 f.write_str("))")
9451 }
9452 Self::Array(items) => {
9453 f.write_str("ARRAY[")?;
9454 for (i, e) in items.iter().enumerate() {
9455 if i > 0 {
9456 f.write_str(", ")?;
9457 }
9458 write!(f, "{e}")?;
9459 }
9460 f.write_str("]")
9461 }
9462 Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
9463 Self::ArraySlice { target, lo, hi } => {
9464 write!(f, "({target}[")?;
9465 if let Some(l) = lo {
9466 write!(f, "{l}")?;
9467 }
9468 write!(f, ":")?;
9469 if let Some(h) = hi {
9470 write!(f, "{h}")?;
9471 }
9472 write!(f, "])")
9473 }
9474 Self::AnyAll {
9475 expr,
9476 op,
9477 array,
9478 is_any,
9479 } => {
9480 let kw = if *is_any { "ANY" } else { "ALL" };
9481 write!(f, "({expr} {op} {kw}({array}))")
9482 }
9483 Self::Case {
9484 operand,
9485 branches,
9486 else_branch,
9487 } => {
9488 f.write_str("CASE")?;
9489 if let Some(op) = operand {
9490 write!(f, " {op}")?;
9491 }
9492 for (w, t) in branches {
9493 write!(f, " WHEN {w} THEN {t}")?;
9494 }
9495 if let Some(e) = else_branch {
9496 write!(f, " ELSE {e}")?;
9497 }
9498 f.write_str(" END")
9499 }
9500 }
9501 }
9502}
9503
9504/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
9505/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
9506pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
9507 use alloc::string::ToString;
9508 if scale == 0 {
9509 return alloc::format!("{unscaled}");
9510 }
9511 let neg = unscaled < 0;
9512 let digits = alloc::format!("{}", unscaled.unsigned_abs());
9513 let scale = scale as usize;
9514 let (int_part, frac_part) = if digits.len() > scale {
9515 (
9516 digits[..digits.len() - scale].to_string(),
9517 digits[digits.len() - scale..].to_string(),
9518 )
9519 } else {
9520 ("0".to_string(), alloc::format!("{digits:0>scale$}"))
9521 };
9522 alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
9523}
9524
9525/// A single-quoted SQL string, with an embedded quote doubled.
9526fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
9527 f.write_str("'")?;
9528 for c in s.chars() {
9529 if c == '\'' {
9530 f.write_str("''")?;
9531 } else {
9532 write!(f, "{c}")?;
9533 }
9534 }
9535 f.write_str("'")
9536}
9537
9538impl fmt::Display for Literal {
9539 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9540 match self {
9541 Self::Integer(n) => write!(f, "{n}"),
9542 Self::Float(x) => {
9543 let s = format!("{x}");
9544 // Default Display for an integral f64 (e.g. 1.0) emits "1",
9545 // which would round-trip back to Integer. Force a dot.
9546 if s.contains('.') || s.contains('e') || s.contains('E') {
9547 f.write_str(&s)
9548 } else {
9549 write!(f, "{s}.0")
9550 }
9551 }
9552 Self::Numeric { unscaled, scale } => {
9553 // Render the exact decimal `unscaled / 10^scale`, preserving
9554 // scale (trailing zeros) — round-trips to the same literal.
9555 f.write_str(&render_exact_decimal(*unscaled, *scale))
9556 }
9557 Self::NumericBig(s) => f.write_str(s),
9558 // Printed exactly as the text form was, so a reader cannot
9559 // tell whether the constant was decoded or not.
9560 Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
9561 Self::String(s) => write_quoted(f, s),
9562 Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
9563 Self::Null => f.write_str("NULL"),
9564 // PG external array form. Display round-trip re-enters
9565 // through the column-typed text coerce, same as pgwire.
9566 Self::TextArray(items) => {
9567 f.write_str("'{")?;
9568 for (i, it) in items.iter().enumerate() {
9569 if i > 0 {
9570 f.write_str(",")?;
9571 }
9572 match it {
9573 None => f.write_str("NULL")?,
9574 Some(s) => {
9575 f.write_str("\"")?;
9576 for c in s.chars() {
9577 match c {
9578 // array-element escapes
9579 '"' | '\\' => write!(f, "\\{c}")?,
9580 // the OUTER wrapper is a SQL string
9581 // literal — embedded quotes must
9582 // double, or the rendered form
9583 // (WAL replay parses it back) is
9584 // invalid SQL
9585 '\'' => f.write_str("''")?,
9586 _ => write!(f, "{c}")?,
9587 }
9588 }
9589 f.write_str("\"")?;
9590 }
9591 }
9592 }
9593 f.write_str("}'")
9594 }
9595 Self::IntArray(items) => {
9596 f.write_str("'{")?;
9597 for (i, it) in items.iter().enumerate() {
9598 if i > 0 {
9599 f.write_str(",")?;
9600 }
9601 match it {
9602 None => f.write_str("NULL")?,
9603 Some(n) => write!(f, "{n}")?,
9604 }
9605 }
9606 f.write_str("}'")
9607 }
9608 Self::BigIntArray(items) => {
9609 f.write_str("'{")?;
9610 for (i, it) in items.iter().enumerate() {
9611 if i > 0 {
9612 f.write_str(",")?;
9613 }
9614 match it {
9615 None => f.write_str("NULL")?,
9616 Some(n) => write!(f, "{n}")?,
9617 }
9618 }
9619 f.write_str("}'")
9620 }
9621 Self::Vector(v) => {
9622 f.write_str("[")?;
9623 for (i, x) in v.iter().enumerate() {
9624 if i > 0 {
9625 f.write_str(", ")?;
9626 }
9627 let s = format!("{x}");
9628 // Mirror Float Display: force a dot so re-parse stays
9629 // numerically literal.
9630 if s.contains('.') || s.contains('e') || s.contains('E') {
9631 f.write_str(&s)?;
9632 } else {
9633 write!(f, "{s}.0")?;
9634 }
9635 }
9636 f.write_str("]")
9637 }
9638 Self::Interval { text, .. } => {
9639 f.write_str("INTERVAL '")?;
9640 for c in text.chars() {
9641 if c == '\'' {
9642 f.write_str("''")?;
9643 } else {
9644 write!(f, "{c}")?;
9645 }
9646 }
9647 f.write_str("'")
9648 }
9649 }
9650 }
9651}
9652
9653impl fmt::Display for BinOp {
9654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9655 f.write_str(match self {
9656 Self::Or => "OR",
9657 Self::And => "AND",
9658 Self::Eq => "=",
9659 Self::NotEq => "<>",
9660 Self::IsDistinctFrom => "IS DISTINCT FROM",
9661 Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
9662 Self::IntDiv => "DIV",
9663 Self::Lt => "<",
9664 Self::LtEq => "<=",
9665 Self::Gt => ">",
9666 Self::GtEq => ">=",
9667 Self::Add => "+",
9668 Self::Sub => "-",
9669 Self::Mul => "*",
9670 Self::Div => "/",
9671 Self::Mod => "%",
9672 Self::L2Distance => "<->",
9673 Self::GeomParallel => "?||",
9674 Self::OverLeft => "&<",
9675 Self::OverRight => "&>",
9676 Self::GeomPerp => "?-|",
9677 Self::GeomSameAs => "~=",
9678 Self::ClosestPoint => "##",
9679 Self::GeomHoriz => "?-",
9680 Self::InnerProduct => "<#>",
9681 Self::CosineDistance => "<=>",
9682 Self::Concat => "||",
9683 Self::BitOr => "|",
9684 Self::BitAnd => "&",
9685 Self::BitXor => "#",
9686 Self::LogicalXor => "xor",
9687 Self::JsonGet => "->",
9688 Self::JsonGetText => "->>",
9689 Self::JsonGetPath => "#>",
9690 Self::JsonGetPathText => "#>>",
9691 Self::JsonContains => "@>",
9692 Self::JsonPathExists => "@?",
9693 Self::JsonContainedBy => "<@",
9694 Self::JsonKeyExists => "?",
9695 Self::JsonKeysAny => "?|",
9696 Self::JsonKeysAll => "?&",
9697 Self::JsonDeletePath => "#-",
9698 Self::TsMatch => "@@",
9699 Self::InetContainedBy => "<<",
9700 Self::InetContainedByEq => "<<=",
9701 Self::InetContains => ">>",
9702 Self::InetContainsEq => ">>=",
9703 Self::InetOverlap => "&&",
9704 Self::Intersects => "?#",
9705 Self::IsBelow => "<^",
9706 Self::IsAbove => ">^",
9707 Self::PatternLt => "~<~",
9708 Self::PatternLtEq => "~<=~",
9709 Self::PatternGt => "~>~",
9710 Self::PatternGtEq => "~>=~",
9711 })
9712 }
9713}
9714
9715/// Quote `s` as a PG double-quoted identifier when required (keyword,
9716/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
9717/// Otherwise return it as-is. Returns an owned `String` to keep the call site
9718/// uniform.
9719pub(crate) fn quote_ident(s: &str) -> String {
9720 let needs_quote = match s.chars().next() {
9721 None => true,
9722 Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
9723 _ => {
9724 s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
9725 || s.chars().any(|c| c.is_ascii_uppercase())
9726 || is_keyword(s)
9727 }
9728 };
9729 if !needs_quote {
9730 return s.to_string();
9731 }
9732 let mut out = String::with_capacity(s.len() + 2);
9733 out.push('"');
9734 for c in s.chars() {
9735 if c == '"' {
9736 out.push_str("\"\"");
9737 } else {
9738 out.push(c);
9739 }
9740 }
9741 out.push('"');
9742 out
9743}
9744
9745fn is_keyword(s: &str) -> bool {
9746 matches!(
9747 &*s.to_ascii_lowercase(),
9748 "select"
9749 | "from"
9750 | "where"
9751 | "as"
9752 | "null"
9753 | "true"
9754 | "false"
9755 | "and"
9756 | "or"
9757 | "not"
9758 | "create"
9759 | "table"
9760 | "insert"
9761 | "into"
9762 | "values"
9763 | "index"
9764 | "on"
9765 | "begin"
9766 | "commit"
9767 | "rollback"
9768 | "is"
9769 | "between"
9770 | "in"
9771 | "like"
9772 | "group"
9773 | "distinct"
9774 | "union"
9775 | "all"
9776 | "join"
9777 | "inner"
9778 | "left"
9779 | "cross"
9780 | "outer"
9781 | "default"
9782 | "savepoint"
9783 | "release"
9784 | "to"
9785 | "having"
9786 | "show"
9787 | "extract"
9788 | "offset"
9789 | "asc"
9790 | "desc"
9791 | "interval"
9792 )
9793}
9794
9795#[cfg(test)]
9796mod tests {
9797 use super::*;
9798 use alloc::vec;
9799
9800 #[test]
9801 fn integer_literal_renders_without_dot() {
9802 assert_eq!(Literal::Integer(42).to_string(), "42");
9803 }
9804
9805 #[test]
9806 fn integral_float_keeps_dot() {
9807 assert_eq!(Literal::Float(1.0).to_string(), "1.0");
9808 assert_eq!(Literal::Float(1.5).to_string(), "1.5");
9809 assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
9810 }
9811
9812 #[test]
9813 fn string_literal_doubles_quote() {
9814 assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
9815 }
9816
9817 #[test]
9818 fn bool_and_null_render_uppercase() {
9819 assert_eq!(Literal::Bool(true).to_string(), "TRUE");
9820 assert_eq!(Literal::Bool(false).to_string(), "FALSE");
9821 assert_eq!(Literal::Null.to_string(), "NULL");
9822 }
9823
9824 #[test]
9825 fn binary_op_always_parenthesised() {
9826 let e = Expr::Binary {
9827 lhs: Box::new(Expr::Literal(Literal::Integer(1))),
9828 op: BinOp::Add,
9829 rhs: Box::new(Expr::Literal(Literal::Integer(2))),
9830 };
9831 assert_eq!(e.to_string(), "(1 + 2)");
9832 }
9833
9834 #[test]
9835 fn select_star_from_table() {
9836 let s = SelectStatement {
9837 locking: None,
9838 items: vec![SelectItem::Wildcard],
9839 from: Some(FromClause {
9840 primary: TableRef {
9841 name: "users".into(),
9842 alias: None,
9843 only: false,
9844 as_of_segment: None,
9845 unnest_expr: None,
9846 unnest_column_aliases: Vec::new(),
9847 with_ordinality: false,
9848 generate_series_args: None,
9849 lateral_subquery: None,
9850 jsonb_each_text_arg: None,
9851 table_fn_call: None,
9852 rows_from: None,
9853 json_table: None,
9854 scalar_fn_item: false,
9855 },
9856 joins: vec![],
9857 }),
9858 where_: None,
9859 group_by: None,
9860 group_by_all: false,
9861 having: None,
9862 unions: vec![],
9863 order_by: Vec::new(),
9864 limit: None,
9865 offset: None,
9866 limit_with_ties: false,
9867 window_check_exprs: Vec::new(),
9868 distinct: false,
9869 distinct_on: Vec::new(),
9870 ctes: vec![],
9871 };
9872 assert_eq!(s.to_string(), "SELECT * FROM users");
9873 }
9874
9875 #[test]
9876 fn quote_ident_for_uppercase_and_keyword() {
9877 assert_eq!(quote_ident("foo"), "foo");
9878 assert_eq!(quote_ident("Foo"), "\"Foo\"");
9879 assert_eq!(quote_ident("select"), "\"select\"");
9880 assert_eq!(quote_ident(""), "\"\"");
9881 assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
9882 }
9883}