spg_sql/ast.rs
1//! AST for the PG-dialect subset SPG accepts in v0.2.
2//!
3//! `Display` is implemented so that for any AST `a` produced by [`crate::parser`],
4//! re-parsing `format!("{a}")` yields a structurally equal AST. Binary and
5//! unary operators always emit parentheses to remove any precedence
6//! ambiguity — round-trip safety wins over prettiness.
7
8use alloc::boxed::Box;
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use core::fmt;
13
14/// `COPY … TO STDOUT` output format. `text` is PG's default
15/// (tab-separated, `\N` nulls, backslash escapes); `csv` follows
16/// RFC-4180-style quoting.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum CopyFormat {
19 #[default]
20 Text,
21 Csv,
22}
23
24/// Options for `COPY … TO STDOUT [WITH] (…)`. Defaults reproduce the
25/// bare `COPY … TO STDOUT` text-format behaviour, so an empty option
26/// list is a no-op. `delimiter` / `null_str` / `quote` fall back to the
27/// per-format defaults (text: `\t` / `\N`; csv: `,` / `` / `"`) when
28/// unset.
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct CopyOptions {
31 pub format: CopyFormat,
32 pub header: bool,
33 pub delimiter: Option<char>,
34 pub null_str: Option<String>,
35 pub quote: Option<char>,
36 /// v7.39 (round 247) — CSV `ESCAPE`: the character that precedes a
37 /// quote (or itself) inside a quoted cell. Defaults to the quote
38 /// character (PG's doubling behavior).
39 pub escape: Option<char>,
40 /// v7.39 (round 247) — CSV `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`:
41 /// columns whose non-NULL cells always quote. `Some(vec![])` is the
42 /// `*` spelling (every column).
43 pub force_quote: Option<Vec<String>>,
44 /// v7.39 (round 265) — CSV `FORCE_NOT_NULL (col, …)`: for these
45 /// columns an UNQUOTED empty field reads as the empty string rather
46 /// than NULL (probed). COPY FROM only.
47 pub force_not_null: Option<Vec<String>>,
48 /// v7.39 (round 265) — CSV `FORCE_NULL (col, …)`: for these columns
49 /// a QUOTED empty field (`""`) also reads as NULL (probed). COPY
50 /// FROM only.
51 pub force_null: Option<Vec<String>>,
52}
53
54/// v7.39 (round 218) — FETCH / MOVE cursor direction. PG grammar: single-row
55/// forms (NEXT / PRIOR / FIRST / LAST / ABSOLUTE n / RELATIVE n) return at
56/// most one row; multi-row forms (bare n / ALL / FORWARD [n|ALL] /
57/// BACKWARD [n|ALL]) stream a run. A negative bare/FORWARD count means
58/// BACKWARD (normalized at execution).
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum CursorDirection {
61 Next,
62 Prior,
63 First,
64 Last,
65 Absolute(i64),
66 Relative(i64),
67 /// Bare `FETCH n` / `FORWARD n` (negative = backward n).
68 Count(i64),
69 /// `ALL` / `FORWARD ALL`.
70 All,
71 Backward(i64),
72 BackwardAll,
73}
74
75impl fmt::Display for CursorDirection {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self {
78 Self::Next => f.write_str("NEXT"),
79 Self::Prior => f.write_str("PRIOR"),
80 Self::First => f.write_str("FIRST"),
81 Self::Last => f.write_str("LAST"),
82 Self::Absolute(n) => write!(f, "ABSOLUTE {n}"),
83 Self::Relative(n) => write!(f, "RELATIVE {n}"),
84 Self::Count(n) => write!(f, "FORWARD {n}"),
85 Self::All => f.write_str("ALL"),
86 Self::Backward(n) => write!(f, "BACKWARD {n}"),
87 Self::BackwardAll => f.write_str("BACKWARD ALL"),
88 }
89 }
90}
91
92/// v7.39 (round 320, V53) — what a `DISCARD` throws away.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum DiscardTarget {
95 All,
96 Plans,
97 Sequences,
98 Temp,
99}
100
101impl fmt::Display for DiscardTarget {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.write_str(match self {
104 Self::All => "ALL",
105 Self::Plans => "PLANS",
106 Self::Sequences => "SEQUENCES",
107 Self::Temp => "TEMP",
108 })
109 }
110}
111
112/// v7.39 (round 535) — which maintenance statement, and therefore what
113/// its target names. Measured on PG18: INDEX / TABLE / CLUSTER name a
114/// relation, SCHEMA names a schema, and SYSTEM / DATABASE name neither
115/// in a way SPG can refuse.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum MaintainKind {
118 ReindexRelation,
119 ReindexSchema,
120 /// `REINDEX SYSTEM` / `REINDEX DATABASE`, and a bare `CLUSTER`.
121 Whole,
122 ClusterRelation,
123}
124
125/// v7.39 (round 547) — see [`Statement::SetDbRoleSetting`]. Boxed in the
126/// enum so the variant costs one pointer.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SetDbRoleSettingStatement {
129 pub database: Option<String>,
130 pub role: Option<String>,
131 pub param: Option<String>,
132 pub value: Option<String>,
133}
134
135/// v7.39 (round 696) — which operand a [`Statement::ValidateOnly`] names,
136/// and therefore which catalog answers whether it exists.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ValidateOnlyKind {
139 /// `LOCK TABLE <t> [, …]` — the relation must exist.
140 LockTable,
141 /// Every role named must exist: `DROP OWNED BY <r> [, …]`,
142 /// `REASSIGN OWNED BY <r> [, …] TO <r>`, and (round 697)
143 /// `SET SESSION AUTHORIZATION <r>`.
144 RoleName,
145 /// `SECURITY LABEL …` — PG refuses unconditionally, because no label
146 /// provider is loaded. SPG has none either.
147 SecurityLabel,
148 /// v7.39 (round 697) — `CREATE EXTENSION <e>`: the extension must be
149 /// AVAILABLE (PG: `extension "x" is not available`).
150 ExtensionAvailable,
151 /// v7.39 (round 708) — `ALTER TYPE <t> <any no-op form>`: the TYPE must
152 /// exist (PG: `type "x" does not exist`); the action itself stays a
153 /// no-op (PG genuinely renames; that residual is recorded).
154 TypeName,
155 /// v7.39 (round 708) — `ALTER AGGREGATE name(args) …`: names[0] is the
156 /// aggregate, the rest its argument type names (`*` = the `(*)` form).
157 /// Existence only; the action no-ops (PG really renames built-ins —
158 /// measured — and SPG does not model that).
159 AggregateName,
160 /// v7.39 (round 708) — `DROP CONVERSION <c>`: SPG ships no conversions,
161 /// so every name answers PG's `conversion "x" does not exist`.
162 ConversionName,
163 /// v7.39 (round 708) — `DROP LANGUAGE <l>`: an unknown language does
164 /// not exist; a shipped one is required (PG's two wordings, measured).
165 LanguageName,
166 /// v7.39 (round 709) — a collation name: performable or PG's
167 /// `collation "x" for encoding "UTF8" does not exist`.
168 CollationName,
169 /// v7.39 (round 709) — a text search configuration name.
170 TsConfigName,
171 /// v7.39 (round 709) — an event trigger name. SPG has none, so the
172 /// not-found answer is total.
173 EventTriggerName,
174 /// v7.39 (round 709) — a tablespace name. SPG has none beyond PG's two
175 /// built-ins, whose drop PG refuses with `permission denied` (measured).
176 TablespaceName,
177 /// v7.39 (round 709) — a large-object oid (names[0], decimal). The
178 /// registry is real (round 287), so the check is a lookup.
179 LargeObjectOid,
180 /// v7.39 (round 706) — `CREATE SERVER` / `CREATE FOREIGN TABLE` /
181 /// `CREATE FOREIGN DATA WRAPPER`. SPG has no foreign-data
182 /// infrastructure at all, so PG's refusals (`foreign-data wrapper "x"
183 /// does not exist`, `server "x" does not exist`) cannot be copied —
184 /// PG can refuse because the missing piece is installable there.
185 /// Accepted with a WARNING, the extension resolution (round 697):
186 /// refusing turns a dump that restores today into one that needs
187 /// editing, and silent acceptance was the actual defect.
188 ForeignInfra,
189 /// v7.39 (round 697) — `DROP EXTENSION <e>`: it must be installed
190 /// (PG: `extension "x" does not exist`).
191 ExtensionInstalled,
192}
193
194#[derive(Debug, Clone, PartialEq)]
195#[allow(clippy::large_enum_variant)] // Statement::Select dominates; Boxing would touch every match site
196pub enum Statement {
197 /// v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET <name>`.
198 ///
199 /// It used to be swallowed with the rest of the ALTER no-ops, which meant
200 /// `ALTER SYSTEM SET nosuch_guc = 1` was ACCEPTED where PG18 answers
201 /// `unrecognized configuration parameter`. SPG still applies nothing —
202 /// there is no postgresql.auto.conf to write — but a name it does not
203 /// know is now refused rather than swallowed.
204 ///
205 /// `None` is `RESET ALL`, which names no parameter.
206 AlterSystem {
207 parameter: Option<String>,
208 },
209 /// `DROP DATABASE [IF EXISTS] <name>`. SPG is single-database, so
210 /// this never succeeds; the name and the flag are carried so the
211 /// engine can answer with PG's wording for the two cases PG itself
212 /// has — an unknown name, or the database you are connected to.
213 DropDatabase {
214 name: String,
215 if_exists: bool,
216 },
217 /// A statement SPG accepts as a no-op but PG refuses inside a
218 /// transaction block — today `CREATE DATABASE` / `DROP DATABASE`,
219 /// which are no-ops here because SPG is single-database.
220 ///
221 /// The no-op path they used to share (`Statement::Empty`) also
222 /// carries CREATE ROLE, CREATE CAST and a dozen others that PG is
223 /// happy to run inside a transaction, so the object has to be named
224 /// to refuse the right ones.
225 NoOpPreventedInTransaction {
226 what: String,
227 /// v7.38.18 — `CREATE DATABASE … LC_COLLATE 'de_DE.utf8'` is in
228 /// every PostgreSQL bootstrap script there is, and SPG threw the
229 /// whole statement away. Being single-database makes the NAME a
230 /// no-op; it does not make the collation one, and a database
231 /// that quietly sorts by the container's `LANG` instead of the
232 /// one the script asked for is a silent difference in every
233 /// `ORDER BY` it will ever run.
234 ///
235 /// `LOCALE` and `LC_COLLATE` both land here; `LC_CTYPE` does
236 /// not, because SPG has no separate ctype.
237 collation: Option<String>,
238 /// v7.38.19 — the database's name, so `pg_database` can list one
239 /// that was created and can be connected to. It was thrown away
240 /// with the rest of the statement.
241 name: Option<String>,
242 },
243 /// v7.39 (round 696) — statements SPG performs nothing for, but whose
244 /// OPERAND PG validates before performing nothing either.
245 ///
246 /// All four used to be consumed whole by `is_dump_noise_statement`,
247 /// which meant `LOCK TABLE nosuch` and `DROP OWNED BY nosuchrole` were
248 /// ACCEPTED where PG18 errors. Accepting a statement that names
249 /// something that does not exist is the F29 shape: the caller is told
250 /// their intent was understood when the object it referred to is not
251 /// there.
252 ///
253 /// They share one variant because they share one rule — resolve the
254 /// name, refuse if absent, otherwise no-op — and four variants would be
255 /// four places for that rule to drift.
256 /// v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS] name(argtypes)[, …]`.
257 /// Consumed whole by the dump-noise list before, so `DROP AGGREGATE
258 /// nosuch(int)` reported success. PG validates every named aggregate's
259 /// EXISTENCE first (measured: a list with one unknown fails on the
260 /// unknown even when an earlier entry exists), renders the signature
261 /// with canonical type names (`int` → `integer`), and refuses to drop a
262 /// built-in (`cannot drop function sum(integer) because it is required
263 /// by the database system`). Every SPG aggregate is a built-in, so the
264 /// outcome is one of those two errors — or the IF EXISTS no-op.
265 ///
266 /// `args` holds the argument type names as written; `None` is the
267 /// `(*)` spelling.
268 DropAggregate {
269 if_exists: bool,
270 items: Vec<(String, Option<Vec<String>>)>,
271 },
272 /// v7.39 (round 750) — `ALTER ROLE|USER <name> … PASSWORD 'x' |
273 /// PASSWORD NULL`. The one attribute of the no-op family with a
274 /// SECURITY consequence: it was silently dropped (ledgered r710),
275 /// so a rotated credential never rotated. `None` = PASSWORD NULL
276 /// (the role keeps existing but can no longer password-auth).
277 AlterRolePassword {
278 name: String,
279 password: Option<String>,
280 },
281 ValidateOnly {
282 kind: ValidateOnlyKind,
283 /// The names the statement referred to. Empty means the form names
284 /// nothing (`SECURITY LABEL`, whose refusal is unconditional).
285 names: Vec<String>,
286 },
287
288 /// v7.39 (round 547) — `ALTER ROLE … SET/RESET` and
289 /// `ALTER DATABASE … SET/RESET`: the GUC defaults a session picks up
290 /// when it starts. Both used to land in the pg_dump no-op tail, so
291 /// the statement reported success and changed nothing.
292 ///
293 /// `database` / `role` are `None` for PG's oid 0 — `ALTER ROLE ALL`
294 /// sets both to None. `param` is `None` for RESET ALL. `value` is
295 /// `None` for RESET of one parameter.
296 SetDbRoleSetting(Box<SetDbRoleSettingStatement>),
297 /// v7.39 (round 288) — `SET CONSTRAINTS { ALL | <name>… }
298 /// { DEFERRED | IMMEDIATE }`. `deferred` carries the timing; the
299 /// name list is not yet honoured (ALL is what pg_dump emits and
300 /// what a circular-FK restore needs), so a named form applies to
301 /// all deferrable constraints too rather than silently doing
302 /// nothing.
303 /// v7.39 (round 308) — `SET CONSTRAINTS { ALL | name [, …] }
304 /// { DEFERRED | IMMEDIATE }`. An empty `names` is the ALL form;
305 /// otherwise the timing applies only to the constraints listed.
306 SetConstraints {
307 names: Vec<String>,
308 deferred: bool,
309 },
310
311 /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
312 /// [CASCADE | RESTRICT]`. Engine removes the matching tables
313 /// (each one) from the catalog; IF EXISTS makes the drop
314 /// idempotent. CASCADE / RESTRICT trailers parsed silently
315 /// (SPG always cascades index drops on table drop).
316 DropTable {
317 names: Vec<String>,
318 if_exists: bool,
319 },
320 /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
321 /// matching index across whichever table holds it.
322 DropIndex {
323 name: String,
324 if_exists: bool,
325 /// v7.39.7 — the table named by MySQL's `DROP INDEX i ON t`.
326 ///
327 /// MySQL keys an index name inside its table and its statement
328 /// says so; PostgreSQL keys it in the schema and has no `ON`
329 /// clause at all. `None` is the PostgreSQL form, which searches
330 /// every table for the name, and is what the MySQL dialect
331 /// refuses — as MySQL does.
332 table: Option<String>,
333 },
334 /// v7.14.0 — empty / comment-only statement. The lexer strips
335 /// `--` line comments and `/* … */` block comments (including
336 /// the MySQL conditional `/*!NNNNN … */` form) before the
337 /// parser ever sees them; a SQL chunk that contains nothing
338 /// else lands here. Engine returns CommandOk no-op so
339 /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
340 /// wrapped in conditional comments, etc.) load cleanly.
341 /// v7.39 (round 277) — SQL-level `PREPARE <name> [(type, …)] AS
342 /// <stmt>`. Session-scoped; the body keeps its `$N` placeholders
343 /// and is substituted at EXECUTE time.
344 Prepare {
345 name: String,
346 /// Declared parameter type names, in order. Empty when the
347 /// `(type, …)` list was omitted (PG infers them).
348 param_types: Vec<String>,
349 body: alloc::boxed::Box<Statement>,
350 /// The statement's own source text, which
351 /// `pg_prepared_statements.statement` reports verbatim.
352 source: String,
353 },
354 /// v7.39 (round 277) — `EXECUTE <name> [(arg, …)]`.
355 Execute {
356 name: String,
357 args: Vec<Expr>,
358 },
359 /// v7.39 (round 277) — `DEALLOCATE {<name> | ALL}`. `None` = ALL.
360 Deallocate(Option<String>),
361 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
362 /// [(kind, …)] ON <col>, … FROM <table>`. SPG records the object so
363 /// dumps restore and reflection is honest; the planner does not
364 /// consult it yet.
365 CreateStatistics {
366 name: String,
367 if_not_exists: bool,
368 /// Requested kinds as PG's single letters (`d` ndistinct,
369 /// `f` dependencies, `m` mcv). Empty = PG's default set.
370 kinds: Vec<String>,
371 columns: Vec<String>,
372 table: String,
373 },
374 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
375 DropStatistics {
376 name: String,
377 if_exists: bool,
378 },
379 /// v7.39 (round 278) — `CALL <proc>(…)`. Parses; the engine
380 /// reports that the procedure does not exist, because SPG has no
381 /// procedure catalog. Carried as a statement rather than raised at
382 /// parse time so the failure is a missing OBJECT (42883), not a
383 /// syntax error.
384 Call(String),
385 /// v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'`. Same shape:
386 /// 2PC is unavailable, which PG itself reports when
387 /// `max_prepared_transactions` is 0.
388 PrepareTransaction(String),
389 Empty,
390 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE]
391 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. The
392 /// canonical driver path for streaming large result sets (psycopg2
393 /// named cursors, JDBC setFetchSize).
394 DeclareCursor {
395 name: String,
396 /// `None` = neither keyword (PG default: backward allowed when the
397 /// plan supports it — always, for SPG's materialized cursors);
398 /// `Some(true)` = SCROLL; `Some(false)` = NO SCROLL (backward
399 /// fetch errors 55000).
400 scroll: Option<bool>,
401 /// `WITH HOLD` — survives the creating transaction's COMMIT.
402 hold: bool,
403 query: Box<Statement>,
404 },
405 /// v7.39 (round 218) — `FETCH [<direction>] [FROM|IN] <name>`.
406 FetchCursor {
407 name: String,
408 direction: CursorDirection,
409 },
410 /// v7.39 (round 218) — `MOVE [<direction>] [FROM|IN] <name>`: FETCH
411 /// without returning rows; the command tag carries the move count.
412 MoveCursor {
413 name: String,
414 direction: CursorDirection,
415 },
416 /// v7.39 (round 218) — `CLOSE <name>` / `CLOSE ALL` (`None` = ALL).
417 CloseCursor {
418 name: Option<String>,
419 },
420 /// v7.39 (round 222) — `LISTEN <channel>`: subscribe this session to
421 /// async notifications on the channel.
422 Listen(String),
423 /// v7.39 (round 222) — `NOTIFY <channel> [, '<payload>']`. Delivered at
424 /// COMMIT (PG semantics: transactional, deduplicated within the tx);
425 /// immediately under autocommit.
426 Notify {
427 channel: String,
428 payload: Option<String>,
429 },
430 /// v7.39 (round 222) — `UNLISTEN <channel>` / `UNLISTEN *` (`None` = *).
431 Unlisten(Option<String>),
432 /// `COPY table [(cols)] TO STDOUT` — the engine renders the
433 /// visible rows in COPY text format (tab-separated, `\N`
434 /// nulls, backslash escapes) as a single-text-column result
435 /// set; the wire layer streams CopyData from it.
436 CopyTo {
437 table: String,
438 columns: Option<Vec<String>>,
439 /// v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT`: an
440 /// arbitrary SELECT/VALUES/CTE (a whole [`Statement`], so set-ops and
441 /// VALUES ride through unchanged) whose result set is streamed in COPY
442 /// format. `Some` overrides `table`/`columns` (which are empty then);
443 /// `None` is the classic `COPY <table> …` shape.
444 query: Option<Box<Statement>>,
445 /// v7.37.x — `WITH (FORMAT csv, HEADER, DELIMITER, NULL, QUOTE)`
446 /// and the legacy `WITH CSV HEADER …` spelling. Default =
447 /// text format, no header (bare `COPY … TO STDOUT`).
448 options: CopyOptions,
449 },
450 /// v7.39 (round 249) — `COPY table [(cols)] FROM '<path>' [(opts)]`.
451 /// The engine is no_std and cannot read the file itself: the host
452 /// (embedded / server / tooling) reads the path and hands the bytes to
453 /// `Engine::copy_from_buffer`. Dispatching this statement straight to
454 /// the engine reports that contract.
455 CopyFromFile {
456 table: String,
457 columns: Option<Vec<String>>,
458 path: String,
459 options: CopyOptions,
460 },
461 /// v7.39 (round 249/252) — `COPY <table> [(cols)] TO '<file>'` (and
462 /// the `COPY (<query>) TO '<file>'` form). The engine is no_std and
463 /// cannot write the file itself: the host renders the payload via
464 /// `Engine::copy_to_buffer` and writes the path.
465 CopyToFile {
466 table: String,
467 columns: Option<Vec<String>>,
468 query: Option<Box<Statement>>,
469 path: String,
470 options: CopyOptions,
471 },
472 Select(SelectStatement),
473 CreateTable(CreateTableStatement),
474 /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
475 /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
476 /// no-op so PG dumps that include extension declarations
477 /// (notably `pgvector`) load against SPG without splitting
478 /// init scripts. mailrs migration follow-up F3.
479 CreateExtension(String),
480 /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
481 /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
482 /// the engine executes it at top level (mailrs round-10
483 /// A.2). Pre-v7.16.2 the parser discarded the body and the
484 /// engine returned CommandOk — a SEV-1 silent no-op that
485 /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
486 /// $$` idempotent migrations into invisible no-ops.
487 DoBlock(PlPgSqlBlock),
488 CreateIndex(CreateIndexStatement),
489 Insert(InsertStatement),
490 /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
491 Update(UpdateStatement),
492 /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
493 Delete(DeleteStatement),
494 /// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ `MERGE` statement.
495 /// `MERGE INTO target [alias] USING source [alias] ON cond
496 /// WHEN MATCHED [AND cond] THEN { UPDATE SET … | DELETE | DO NOTHING }
497 /// WHEN NOT MATCHED [AND cond] THEN { INSERT (cols) VALUES (vals) | DO NOTHING }
498 /// [WHEN …]`. SPG v7.17 supports table-based source (subquery
499 /// source is a follow-up); BY SOURCE / BY TARGET and RETURNING
500 /// are also follow-ups.
501 Merge(MergeStatement),
502 /// v7.39 (round 169) — `VACUUM [(opts)] [FULL|FREEZE|VERBOSE|ANALYZE]
503 /// [<table>]`. Was a parse-time no-op from the pre-MVCC era; with the
504 /// in-place MVCC gate ON, tombstoned versions are REAL bloat and a
505 /// customer's manual VACUUM must actually reclaim. `analyze` mirrors
506 /// the `VACUUM ANALYZE` spelling.
507 Vacuum {
508 table: Option<String>,
509 analyze: bool,
510 },
511 /// `BEGIN` / `START TRANSACTION` — with an optional explicit
512 /// `ISOLATION LEVEL …` mode (`None` = use the session default). PG
513 /// applies the level for the duration of this transaction only.
514 Begin(TransactionModes),
515 Commit,
516 Rollback,
517 /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
518 /// stack so a later `ROLLBACK TO <name>` can undo just the work
519 /// since this point.
520 Savepoint(String),
521 /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
522 /// named savepoint and discard later savepoints. Does not end the
523 /// transaction.
524 RollbackToSavepoint(String),
525 /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
526 /// rolling back. Keeps the work done since then.
527 ReleaseSavepoint(String),
528 /// `SHOW TABLES` — return the list of tables in the catalog.
529 ShowTables,
530 /// v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES` /
531 /// `SHOW SCHEMAS`. SPG is single-database; the executor
532 /// returns the canonical MySQL set so the mysql / MariaDB
533 /// client populates its database selector.
534 ShowDatabases,
535 /// v7.39.2 — MySQL `USE <db>`.
536 ///
537 /// It parsed as `Empty` and did nothing at all, so `USE myapp;
538 /// SELECT DATABASE()` answered the same constant it answered before
539 /// — measured against MySQL 9.7.2, which answers `myapp`. SPG serves
540 /// ONE database and answers to any name (see `CREATE DATABASE`), so
541 /// this does not switch catalogs; it records the NAME, which is the
542 /// half a client can observe and the half the PostgreSQL wire has
543 /// tracked since v7.39 (`current_database()` names what the startup
544 /// message asked for).
545 UseDatabase(String),
546 /// v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE TABLE <t>`
547 /// returns a 2-column row `(Table, "Create Table")` carrying
548 /// the synthesized DDL. mysqldump emits this for every
549 /// table at scrape time.
550 ShowCreateTable(String),
551 /// v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES FROM <t>`
552 /// (also `SHOW INDEX`, `SHOW KEYS`).
553 ShowIndexes(String),
554 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS`.
555 ShowStatus,
556 /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW VARIABLES`.
557 ShowVariables,
558 /// r1067 — MySQL `SHOW VARIABLES LIKE 'pattern'` (sysbench-tpcc
559 /// probes isolation with it at connect).
560 ShowVariablesLike(String),
561 /// v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
562 ShowProcesslist,
563 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
564 /// pgbouncer sends `DISCARD ALL` between pooled client sessions to make
565 /// the connection look brand new to the next client; it used to be
566 /// swallowed as dump noise, so nothing was discarded.
567 Discard(DiscardTarget),
568 /// v7.39 (round 318, V51) — MySQL `KILL [CONNECTION | QUERY] <expr>`.
569 /// The id is an expression because MariaDB accepts one
570 /// (`KILL connection_id()` is the documented way to drop your own
571 /// connection). `query_only` is the `QUERY` form: stop the target's
572 /// running statement but leave it connected.
573 Kill {
574 query_only: bool,
575 id: Box<Expr>,
576 },
577 /// `SHOW COLUMNS FROM <table>` — return one row per column with
578 /// its declared name / type / nullability.
579 ShowColumns(String),
580 /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
581 /// Role is optional; defaults to `readonly` when omitted.
582 CreateUser(CreateUserStatement),
583 /// `DROP USER 'name'` (v4.1). v7.39 (read01 round 58) — `IF EXISTS` is
584 /// carried through: PG skips with a NOTICE rather than erroring.
585 DropUser {
586 name: String,
587 if_exists: bool,
588 },
589 /// v7.39 (RLS) — `SET ROLE { name | NONE | DEFAULT }` / `RESET ROLE`.
590 /// `Some(name)` switches the session's effective role (drives
591 /// `current_user` and RLS enforcement); `None` resets to the login
592 /// identity (the Admin superuser).
593 SetRole(Option<String>),
594 /// v7.39 (read01 round 57) — `GRANT <privs> ON <object> TO <roles>`.
595 Grant(GrantStatement),
596 /// v7.39 (read01 round 57) — `REVOKE [GRANT OPTION FOR] <privs> ON
597 /// <object> FROM <roles>`.
598 Revoke(GrantStatement),
599 /// v7.39 (RLS) — `CREATE POLICY name ON table …`.
600 CreatePolicy(CreatePolicyStatement),
601 /// v7.39 (RLS) — `ALTER POLICY name ON table …`.
602 AlterPolicy(AlterPolicyStatement),
603 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
604 DropPolicy(DropPolicyStatement),
605 /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
606 ShowUsers,
607 /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
608 /// single-column text table describing the rewritten plan tree
609 /// for `inner`. `analyze` triggers an actual exec to attach
610 /// observed row counts and elapsed micros to each node.
611 Explain(ExplainStatement),
612 /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
613 /// Synchronous rebuild of an NSW index. With the optional
614 /// encoding clause, every stored cell at the indexed column is
615 /// also re-encoded through `coerce_value` before the new graph
616 /// builds.
617 AlterIndex(AlterIndexStatement),
618 /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
619 /// The only setting in v6.7.2 is `hot_tier_bytes`, which
620 /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
621 /// for the named table.
622 AlterTable(AlterTableStatement),
623 /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
624 /// The catalog row lives in `spg_publications`. Publisher-side
625 /// WAL filtering arrives in v6.1.5.
626 CreatePublication(CreatePublicationStatement),
627 /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
628 /// no-op when the publication does not exist.
629 DropPublication {
630 name: String,
631 /// v7.39 (round 754, F31-B4) — `IF EXISTS` quietly skips a
632 /// missing publication; the bare form refuses with PG's
633 /// sentence (PG18-measured — the old "silent no-op" note on
634 /// the executor was wrong).
635 if_exists: bool,
636 },
637 /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
638 /// publication ordered by name with `(name, scope_summary,
639 /// table_count)` columns. The scope summary is the human-
640 /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
641 /// TABLES EXCEPT …`; `table_count` is `NULL` for the
642 /// `AllTables` scope and the table-list length otherwise.
643 ShowPublications,
644 /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
645 /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
646 /// in `spg_subscriptions`; when the subscription is
647 /// `enabled = true` (default) the server spawns a
648 /// background worker that connects to `conn` and drains the
649 /// requested publication(s) into the local engine.
650 CreateSubscription(CreateSubscriptionStatement),
651 /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
652 /// PUBLICATION, silent no-op when absent. Stops the
653 /// associated worker thread before removing the row.
654 DropSubscription {
655 name: String,
656 /// v7.39 (round 754, F31-B4) — same contract as
657 /// [`Statement::DropPublication`].
658 if_exists: bool,
659 },
660 /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
661 /// subscription ordered by name with `(name, conn_str,
662 /// publications, enabled, last_received_pos)`.
663 ShowSubscriptions,
664 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
665 /// Blocks until the local server's apply position reaches
666 /// `<pos>` or `<ms>` elapses. Server-layer command: the
667 /// engine refuses it (`EngineError::Unsupported`) since
668 /// `lag_state` lives in `spg-server`'s `ServerState`.
669 WaitForWalPosition {
670 pos: u64,
671 /// `None` → wait forever; `Some(ms)` → return after `ms`
672 /// milliseconds even if the target isn't reached.
673 timeout_ms: Option<u64>,
674 },
675 /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
676 /// table; `ANALYZE <name>` re-stats just one. Populates
677 /// `spg_statistic` with per-column null_frac + n_distinct +
678 /// 100-bucket equi-depth histogram.
679 Analyze(Option<String>),
680 /// v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
681 /// PostgreSQL has only `ALTER TABLE … RENAME TO`, so this spelling
682 /// had nowhere to go; it is what a MySQL migration writes.
683 RenameTables(Vec<(String, String)>),
684 /// v7.39 (round 535) — `REINDEX { INDEX | TABLE | SCHEMA | DATABASE
685 /// | SYSTEM } [CONCURRENTLY] <name>` and `CLUSTER [VERBOSE]
686 /// [<table> [USING <index>]]`.
687 ///
688 /// SPG has neither index bloat nor a clustering order to rebuild, so
689 /// the work is a no-op — but PG VALIDATES the target, and both were
690 /// swallowed at parse time, so `REINDEX TABLE typo` reported success.
691 /// The name is carried now so the engine can say what PG says.
692 Maintain {
693 kind: MaintainKind,
694 /// `REINDEX … CONCURRENTLY`. Carried for the same reason as
695 /// [`CreateIndexStatement::concurrently`]: PG bars the
696 /// CONCURRENTLY form inside a transaction block and allows the
697 /// plain one.
698 concurrently: bool,
699 /// `None` for the whole-database forms, which name nothing.
700 target: Option<String>,
701 },
702 /// v7.37.17 (17.6 sibling) — `TRUNCATE [TABLE] [ONLY] <name>
703 /// [, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE |
704 /// RESTRICT]`. Clears every row from each named table. SPG's
705 /// SEQUENCE identity is per-table; RESTART IDENTITY reinitializes
706 /// the associated sequence to its starting value. CASCADE
707 /// currently walks direct FK-referring tables and truncates
708 /// them too (PG's semantics). The ONLY modifier (skip partitions)
709 /// and RESTRICT (default) are accepted with no effect since
710 /// SPG's declarative partitions are always truncated together.
711 Truncate {
712 tables: Vec<String>,
713 restart_identity: bool,
714 cascade: bool,
715 /// v7.39 (round 647) — `TRUNCATE ONLY t`. Absorbed as a no-op
716 /// since v7.14 on the reasoning that SPG's children are separate
717 /// relations a truncate does not descend into. Same reasoning
718 /// round 621 applied to `FROM ONLY`, and it stopped being true
719 /// for the same reason: measured, `TRUNCATE <inheritance parent>`
720 /// leaves the children's rows where PG empties them, and
721 /// `TRUNCATE ONLY <partitioned parent>` is silently accepted
722 /// where PG refuses it outright.
723 only: bool,
724 },
725 /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
726 /// BTree-cold indices and merges small cold-tier segments
727 /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
728 /// 4 MiB) into a single larger segment per (table, index).
729 /// `WHERE` predicate filtering on which tables to compact is
730 /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
731 /// v6.7.3 only supports the bare form.
732 CompactColdSegments,
733 /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
734 /// parameter on the engine; v7.12.1 honours
735 /// `default_text_search_config` (consumed by `to_tsvector` /
736 /// `plainto_tsquery` family when called without an explicit
737 /// config arg). All other names are accepted as a no-op so PG
738 /// dumps with `SET client_encoding`, `SET search_path` etc.
739 /// load cleanly.
740 SetParameter {
741 name: String,
742 value: SetValue,
743 /// v7.38 (read01 P3.19) — `SET LOCAL` scopes the change to the
744 /// current transaction; the engine saves the prior value and
745 /// restores it at COMMIT / ROLLBACK. Plain `SET` (and `SET
746 /// SESSION`) leave this false and persist for the session.
747 local: bool,
748 },
749 /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
750 /// multi-assignment (mysqldump preamble uses
751 /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
752 /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
753 /// source order. Pairs whose LHS is a MySQL session/user
754 /// variable (`@VAR` / `@@VAR`) are recorded with the raw
755 /// name so the engine can ignore them; pairs whose LHS is
756 /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
757 /// go through the regular `set_session_param` path.
758 SetParameterList(Vec<(String, SetValue)>),
759 /// v7.39 (round 430) — MySQL's USER-defined variables:
760 /// `SET @x = 5, @s := CONCAT('a','b')`. Distinct from
761 /// [`Self::SetParameter`] (a `@@`-style engine/session setting) in
762 /// every way that matters: the value is an arbitrary EXPRESSION, the
763 /// name lives in its own per-session namespace, and reading an unset
764 /// one answers NULL rather than raising. `:=` and `=` are the same
765 /// assignment here.
766 ///
767 /// Before this the parser stripped every `@`, so `@x` and `@@x` were
768 /// the same node: `SET @x = 5` silently landed in the session-parameter
769 /// store where nothing could read it back, and `SELECT @x` failed with
770 /// "Unknown system variable".
771 /// v7.39 (round 554) — `SET @a = …, SETTING = …`.
772 ///
773 /// `settings` is the trailing half a mysqldump preamble writes:
774 /// `SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'`
775 /// saves a value and changes it in one statement. The parser used
776 /// to refuse the mixture outright, so no mysqldump could be
777 /// restored past its preamble.
778 SetUserVars(Vec<(String, Expr)>, Vec<(String, Expr)>),
779 /// v7.38 轴 4 — `SET [SESSION] TRANSACTION ISOLATION LEVEL …`
780 /// (plus optional READ ONLY / READ WRITE / DEFERRABLE clauses
781 /// silently accepted). PG-standard surface for picking an
782 /// isolation level. Engine tracks the value on
783 /// `Engine::current_isolation_level()`; actual MVCC / SSI
784 /// semantics implementation lands separately. PG itself maps
785 /// READ UNCOMMITTED to READ COMMITTED; SPG mirrors that —
786 /// effectively every level reads as READ COMMITTED in v7.37.8.
787 SetTransaction {
788 modes: TransactionModes,
789 },
790 /// v7.38 轴 4 — `SHOW <param>` returns a 1-column 1-row result
791 /// with the parameter's current value as TEXT. Today the only
792 /// recognised param is `transaction_isolation`; further
793 /// surfaces (`search_path`, `application_name`, …) land as the
794 /// session-parameter inventory grows.
795 ShowParameter(String),
796 /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
797 /// to its default. No-op for parameters SPG does not track.
798 ResetParameter(Option<String>),
799 /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
800 /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
801 /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
802 /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
803 /// languages parse but error at exec time with a clear
804 /// unsupported message.
805 CreateFunction(CreateFunctionStatement),
806 /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
807 /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
808 /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
809 /// triggers and column-list / WHEN clauses are out of scope
810 /// for v7.12.4.
811 CreateTrigger(CreateTriggerStatement),
812 /// v7.39 (round 139) — `CREATE RULE name AS ON event TO table [WHERE cond]
813 /// DO [ALSO|INSTEAD] { NOTHING | command }` query-rewrite rule.
814 CreateRule(CreateRuleStatement),
815 /// v7.39 (round 139) — `DROP RULE [IF EXISTS] name ON table`.
816 DropRule {
817 name: String,
818 table: String,
819 if_exists: bool,
820 },
821 /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
822 /// no-op when missing if `IF EXISTS` is set.
823 DropTrigger {
824 name: String,
825 table: String,
826 if_exists: bool,
827 },
828 /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
829 /// DROP TRIGGER but global (no table scope).
830 DropFunction {
831 name: String,
832 /// v7.39 (read01 round 62) — the argument TYPES, when the statement gave
833 /// them: `DROP FUNCTION f(int)` drops that overload only. `None` = no
834 /// argument list, which PG accepts only when the name is unambiguous.
835 args: Option<Vec<String>>,
836 if_exists: bool,
837 },
838 /// v7.17.0 — `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] name
839 /// [AS data_type]
840 /// [INCREMENT [BY] n]
841 /// [MINVALUE n | NO MINVALUE]
842 /// [MAXVALUE n | NO MAXVALUE]
843 /// [START [WITH] n]
844 /// [CACHE n]
845 /// [[NO] CYCLE]
846 /// [OWNED BY {table.col | NONE}]`.
847 /// Closes the round-7+ silent-no-op SEQUENCE story so pg_dump
848 /// emits + nextval/currval/setval downstream all work.
849 CreateSequence(CreateSequenceStatement),
850 /// v7.17.0 — `ALTER SEQUENCE [IF EXISTS] name <options>` with
851 /// the same option grammar as CREATE SEQUENCE, plus
852 /// `RESTART [WITH n]` and `OWNED BY ...` re-attach.
853 AlterSequence(AlterSequenceStatement),
854 /// v7.17.0 — `DROP SEQUENCE [IF EXISTS] name [, name…]
855 /// [CASCADE | RESTRICT]`. CASCADE / RESTRICT trailers parsed
856 /// silently (no FK on sequences).
857 DropSequence {
858 names: Vec<String>,
859 if_exists: bool,
860 },
861 /// v7.17.0 Phase 1.2 — `CREATE [OR REPLACE] [TEMPORARY] VIEW
862 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …>`. Closes the
863 /// silent-no-op VIEW story from the v7.17 customer-readiness
864 /// audit: pre-v7.17 SPG parsed CREATE VIEW as Statement::Empty
865 /// so any downstream `SELECT FROM v` errored with table-not-
866 /// found. The view body is stored verbatim; SELECT FROM <v>
867 /// rewrites at exec-time by prepending the view body as a
868 /// synthetic CTE.
869 CreateView(CreateViewStatement),
870 /// v7.17.0 Phase 1.2 — `DROP VIEW [IF EXISTS] name [, name…]
871 /// [CASCADE | RESTRICT]`. Removes the matching view from the
872 /// catalog; CASCADE/RESTRICT parsed silently.
873 DropView {
874 names: Vec<String>,
875 if_exists: bool,
876 },
877 /// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW [IF NOT
878 /// EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
879 /// Closes the silent-no-op MATERIALIZED VIEW story. Storage
880 /// model: the materialised result lives as a regular table
881 /// with the matching name + a parallel
882 /// `materialized_views` registry mapping name → body source
883 /// (used by REFRESH).
884 CreateMaterializedView(CreateMaterializedViewStatement),
885 /// v7.17.0 Phase 1.3 — `REFRESH MATERIALIZED VIEW name [WITH
886 /// [NO] DATA]`. Re-runs the stored body and replaces the
887 /// cached rows. `WITH NO DATA` truncates without re-running.
888 RefreshMaterializedView {
889 name: String,
890 with_data: bool,
891 },
892 /// v7.17.0 Phase 1.3 — `DROP MATERIALIZED VIEW [IF EXISTS]
893 /// name [, name…] [CASCADE | RESTRICT]`. Drops both the
894 /// backing table and the source registry entry.
895 DropMaterializedView {
896 names: Vec<String>,
897 if_exists: bool,
898 },
899 /// v7.17.0 Phase 1.4 — `CREATE TYPE name AS ENUM ('a', 'b',
900 /// …)`. Closes the silent-no-op CREATE TYPE story so PG
901 /// dumps that declare enum types load with real constraints
902 /// instead of becoming free-form TEXT. Future kinds
903 /// (composite / range / domain) extend the inner `kind`
904 /// enum.
905 CreateType(CreateTypeStatement),
906 /// v7.37 D.55 — `ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
907 /// [{BEFORE | AFTER} 'existing']`. Extends an enum's label list so
908 /// enum evolution stops being a silent no-op. `position` is
909 /// `Some((is_before, anchor))`.
910 /// v7.39 (read01 round 49) — `ALTER TYPE t RENAME VALUE 'old' TO 'new'`.
911 /// Used to be swallowed by the ALTER TYPE no-op tail, so the rename was
912 /// accepted and silently ignored.
913 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
914 /// Used to be swallowed as dump noise, so a comment was accepted and lost
915 /// (and obj_description / col_description always returned NULL).
916 /// `kind` is lowercase ("table" / "column" / "index" / …); for a column
917 /// `name` is the dotted `table.column`. `comment: None` = `IS NULL` = remove.
918 CommentOn {
919 kind: String,
920 name: String,
921 comment: Option<String>,
922 },
923 AlterTypeRenameValue {
924 type_name: String,
925 old: String,
926 new: String,
927 },
928 AlterTypeAddValue {
929 type_name: String,
930 label: String,
931 if_not_exists: bool,
932 position: Option<(bool, String)>,
933 },
934 /// v7.17.0 Phase 1.4 — `DROP TYPE [IF EXISTS] name [, name…]
935 /// [CASCADE | RESTRICT]`. Removes the matching enum/domain
936 /// from the catalog.
937 DropType {
938 names: Vec<String>,
939 if_exists: bool,
940 },
941 /// v7.17.0 Phase 1.5 — `CREATE DOMAIN name AS base_type
942 /// [DEFAULT expr] [NOT NULL | NULL] [CHECK (expr)]*`.
943 /// A DOMAIN is a named CHECK-constrained alias over a built-
944 /// in type. The CHECK + NOT NULL + DEFAULT clauses apply to
945 /// every column declared with the domain. Closes the
946 /// silent-no-op CREATE DOMAIN story so PG dumps that ship
947 /// validated identifier types (email, positive_int, …) keep
948 /// their guarantees.
949 CreateDomain(CreateDomainStatement),
950 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`. Every form was
951 /// previously swallowed by the catch-all DDL arm: the statement
952 /// reported success and did nothing, so a migration that dropped a
953 /// constraint kept rejecting the data it had just been told to
954 /// accept.
955 AlterDomain {
956 name: String,
957 action: AlterDomainAction,
958 },
959 /// v7.17.0 Phase 1.5 — `DROP DOMAIN [IF EXISTS] name
960 /// [, name…] [CASCADE | RESTRICT]`. Removes the matching
961 /// domain from the catalog.
962 DropDomain {
963 names: Vec<String>,
964 if_exists: bool,
965 },
966 /// v7.17.0 Phase 1.6 — `CREATE SCHEMA [IF NOT EXISTS]
967 /// name [AUTHORIZATION user]`. SPG is single-database;
968 /// schemas are tracked as a namespace registry so pg_dump
969 /// multi-schema declarations land cleanly and `SELECT *
970 /// FROM information_schema.schemata` returns real entries.
971 /// Schema-qualified `schema.table` references still strip
972 /// the prefix at lookup time per PG (schemas are not
973 /// isolation boundaries in v7.17 — see project-next-docket
974 /// for the v7.18+ isolation tracking).
975 CreateSchema {
976 name: String,
977 if_not_exists: bool,
978 },
979 /// v7.17.0 Phase 1.6 — `DROP SCHEMA [IF EXISTS] name
980 /// [, name…] [CASCADE | RESTRICT]`. Removes the schema
981 /// from the registry; built-in `public` / `pg_catalog` /
982 /// `information_schema` cannot be dropped.
983 DropSchema {
984 names: Vec<String>,
985 if_exists: bool,
986 },
987}
988
989/// v7.39 (round 260) — the `ALTER DOMAIN` actions SPG implements.
990#[derive(Debug, Clone, PartialEq)]
991pub enum AlterDomainAction {
992 AddConstraint { name: Option<String>, check: Expr },
993 DropConstraint { name: String, if_exists: bool },
994 SetDefault(Expr),
995 DropDefault,
996 SetNotNull,
997 DropNotNull,
998 RenameTo(String),
999}
1000
1001/// v7.17.0 Phase 1.5 — `CREATE DOMAIN` AST.
1002#[derive(Debug, Clone, PartialEq)]
1003pub struct CreateDomainStatement {
1004 pub name: String,
1005 /// Base type for the domain (one of the built-in
1006 /// `ColumnTypeName` variants).
1007 pub base_type: ColumnTypeName,
1008 /// v7.39 (round 259) — `CREATE DOMAIN child AS parent …` where
1009 /// `parent` is itself a DOMAIN. The parser already captured the
1010 /// unknown type name; it just was not carried here, so the parent's
1011 /// CHECK constraints were invisible and a value violating them was
1012 /// silently accepted. `base_type` still holds the ultimate scalar
1013 /// type, which is what the storage tier stores.
1014 pub base_domain: Option<String>,
1015 /// Optional `DEFAULT <expr>`. Resolved at engine-side
1016 /// CREATE TABLE time when a column is bound to this domain.
1017 pub default: Option<Expr>,
1018 /// `NOT NULL` from the domain definition. Engine ORs this
1019 /// with the column-level nullability so the strictest of the
1020 /// two wins (i.e. the column is non-nullable if either side
1021 /// says so).
1022 pub not_null: bool,
1023 /// Zero-or-more `CHECK (expr)` predicates. Each one is
1024 /// enforced as part of the column's CHECK list at INSERT /
1025 /// UPDATE time, with `VALUE` substituted for the column's
1026 /// current cell value.
1027 pub checks: Vec<Expr>,
1028}
1029
1030/// v7.17.0 Phase 1.4 — `CREATE TYPE` AST.
1031#[derive(Debug, Clone, PartialEq, Eq)]
1032pub struct CreateTypeStatement {
1033 pub name: String,
1034 pub kind: TypeKind,
1035}
1036
1037/// v7.17.0 Phase 1.4 — flavour of the new type. Only ENUM is
1038/// implemented; the variant set is open so Phase 1.5 (DOMAIN)
1039/// and later (COMPOSITE, RANGE) can land without an AST shape
1040/// migration.
1041///
1042/// v7.37.x (ζ-B Phase 1 composite accept) — added Composite for
1043/// `CREATE TYPE name AS (field_name field_type, …)`. Phase 1
1044/// stores the field list in the catalog so PG dumps that emit
1045/// `CREATE TYPE … AS (…)` don't error out; using a composite type
1046/// as a column type lands in Phase 2 (Value::Composite encoding +
1047/// ROW() literal + field-access syntax).
1048#[derive(Debug, Clone, PartialEq, Eq)]
1049pub enum TypeKind {
1050 /// `AS ENUM ('a', 'b', …)`. Order is preserved (PG enum
1051 /// labels are ordered).
1052 Enum { labels: Vec<String> },
1053 /// `AS (field_name field_type, …)`. Order matters; PG
1054 /// composite literals are positional.
1055 Composite {
1056 fields: Vec<(String, ColumnTypeName)>,
1057 /// v7.39 (round 264) — parallel to `fields`: the raw type NAME
1058 /// when a field's type is not a builtin (i.e. another composite).
1059 /// The parser already captures it; without carrying it here a
1060 /// nested composite field resolved to the Text placeholder and
1061 /// the inner record never became a record.
1062 field_user_types: Vec<Option<String>>,
1063 },
1064}
1065
1066/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
1067/// a string literal, an identifier (often a config name), an
1068/// integer/float, or the bare `DEFAULT` keyword.
1069#[derive(Debug, Clone, PartialEq)]
1070pub enum SetValue {
1071 String(String),
1072 Ident(String),
1073 Number(String),
1074 Default,
1075}
1076
1077/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1078/// at parse time and tracks the selected value on the engine. The
1079/// actual semantic differentiation (REPEATABLE READ snapshot,
1080/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1081/// today every level reads as effective READ COMMITTED (which is
1082/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1083/// READ COMMITTED). Default = `ReadCommitted`.
1084#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1085pub enum IsolationLevel {
1086 ReadUncommitted,
1087 #[default]
1088 ReadCommitted,
1089 RepeatableRead,
1090 Serializable,
1091}
1092
1093impl IsolationLevel {
1094 /// Canonical PG-style display name, as `SHOW transaction_isolation`
1095 /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1096 /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1097 /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1098 /// `read uncommitted`) and only BEHAVES as read committed; the old
1099 /// fold renamed the label too.
1100 /// v7.39 — the MySQL display name, which is NOT the PG one: MySQL
1101 /// hyphenates and upper-cases. Measured on MySQL 9.7.2 by setting
1102 /// each level and reading `@@transaction_isolation` back:
1103 /// `READ-UNCOMMITTED` / `READ-COMMITTED` / `REPEATABLE-READ` /
1104 /// `SERIALIZABLE` (the last has no hyphen because it is one word).
1105 ///
1106 /// This exists so the two MySQL surfaces cannot drift: both
1107 /// `SHOW VARIABLES` and `@@transaction_isolation` used to carry
1108 /// their own hard-coded literal, and the literals disagreed —
1109 /// one said `REPEATABLE-READ` while the engine ran read committed.
1110 /// v7.39 — parse what `default_transaction_isolation` holds. PG
1111 /// accepts the SQL spellings and stores them lower-cased with a
1112 /// space; anything else is not a level this understands and the
1113 /// caller keeps its own default rather than guessing.
1114 #[must_use]
1115 pub fn from_pg_name(name: &str) -> Option<Self> {
1116 match name.trim().to_ascii_lowercase().as_str() {
1117 "read uncommitted" => Some(Self::ReadUncommitted),
1118 "read committed" => Some(Self::ReadCommitted),
1119 "repeatable read" => Some(Self::RepeatableRead),
1120 "serializable" => Some(Self::Serializable),
1121 _ => None,
1122 }
1123 }
1124
1125 #[must_use]
1126 pub fn as_mysql_str(self) -> &'static str {
1127 match self {
1128 Self::ReadUncommitted => "READ-UNCOMMITTED",
1129 Self::ReadCommitted => "READ-COMMITTED",
1130 Self::RepeatableRead => "REPEATABLE-READ",
1131 Self::Serializable => "SERIALIZABLE",
1132 }
1133 }
1134
1135 pub fn as_pg_str(self) -> &'static str {
1136 match self {
1137 Self::ReadUncommitted => "read uncommitted",
1138 Self::ReadCommitted => "read committed",
1139 Self::RepeatableRead => "repeatable read",
1140 Self::Serializable => "serializable",
1141 }
1142 }
1143}
1144
1145impl core::fmt::Display for IsolationLevel {
1146 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1147 f.write_str(self.as_pg_str())
1148 }
1149}
1150
1151/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1152/// single fixed-shape DDL; the WITH-clause options PG supports
1153/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1154/// scope for v6.1.4 — `enabled` defaults to true and there are
1155/// no other knobs to set in v6.1.x.
1156#[derive(Debug, Clone, PartialEq, Eq)]
1157pub struct CreateSubscriptionStatement {
1158 pub name: String,
1159 /// Connection string in PG keyword=value form (e.g.
1160 /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1161 /// `host` and `port` fields; the rest is reserved for
1162 /// future v6.1.x options.
1163 pub conn_str: String,
1164 /// One or more publications on the remote side. Order is
1165 /// preserved verbatim from the DDL; the worker requests them
1166 /// in this order. v6.1.4 records the list; v6.1.5
1167 /// publisher-side filtering enforces it.
1168 pub publications: Vec<String>,
1169}
1170
1171/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1172#[derive(Debug, Clone, PartialEq, Eq)]
1173pub struct CreateSequenceStatement {
1174 pub name: String,
1175 pub if_not_exists: bool,
1176 pub temporary: bool,
1177 /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1178 pub data_type: Option<SequenceDataType>,
1179 pub options: SequenceOptions,
1180}
1181
1182/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1184pub enum SequenceDataType {
1185 SmallInt,
1186 Int,
1187 BigInt,
1188}
1189
1190/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1191/// All fields are optional. `min_value`/`max_value` carry
1192/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1193#[derive(Debug, Clone, Default, PartialEq, Eq)]
1194pub struct SequenceOptions {
1195 pub increment: Option<i64>,
1196 pub min_value: Option<SeqBound>,
1197 pub max_value: Option<SeqBound>,
1198 pub start: Option<i64>,
1199 /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1200 /// RESTART, `Some(Some(n))` = RESTART WITH n.
1201 pub restart: Option<Option<i64>>,
1202 pub cache: Option<i64>,
1203 pub cycle: Option<bool>,
1204 pub owned_by: Option<SequenceOwnedBy>,
1205}
1206
1207/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1209pub enum SeqBound {
1210 Value(i64),
1211 NoBound,
1212}
1213
1214/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1215#[derive(Debug, Clone, PartialEq, Eq)]
1216pub enum SequenceOwnedBy {
1217 None,
1218 Column { table: String, column: String },
1219}
1220
1221/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1222#[derive(Debug, Clone, PartialEq)]
1223pub struct CreateMaterializedViewStatement {
1224 pub name: String,
1225 pub if_not_exists: bool,
1226 /// Optional `(col, col, …)` rename list. Applies to the
1227 /// backing table at CREATE / REFRESH time.
1228 pub columns: Vec<String>,
1229 /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1230 /// the cached rows.
1231 pub body: SelectStatement,
1232 /// `WITH DATA` (default) = materialise the rows at CREATE
1233 /// time. `WITH NO DATA` = create an empty backing table;
1234 /// callers must REFRESH before SELECT returns rows.
1235 pub with_data: bool,
1236 /// v7.38 (read01 P6.49) — when true this node came from
1237 /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1238 /// executor creates a plain table and does NOT register it in the
1239 /// materialized-view registry (no REFRESH semantics).
1240 pub as_plain_table: bool,
1241 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1242 /// meaningful together with `as_plain_table`; the executor puts the
1243 /// resulting table in the creating session's namespace.
1244 pub temporary: bool,
1245}
1246
1247/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1248/// auto-updatable view. `Cascaded` is PG's default when the bare
1249/// `WITH CHECK OPTION` is written.
1250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1251pub enum ViewCheckOption {
1252 Local,
1253 Cascaded,
1254}
1255
1256/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1257#[derive(Debug, Clone, PartialEq)]
1258pub struct CreateViewStatement {
1259 pub name: String,
1260 pub or_replace: bool,
1261 pub if_not_exists: bool,
1262 pub temporary: bool,
1263 /// Optional `(col, col, …)` rename list. When non-empty,
1264 /// these override the body's projected column names per-
1265 /// position at SELECT-from-view time.
1266 pub columns: Vec<String>,
1267 /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1268 /// time to materialise the view as a synthetic CTE.
1269 pub body: SelectStatement,
1270 /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1271 /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1272 /// 44000). `None` = no check option.
1273 pub check_option: Option<ViewCheckOption>,
1274}
1275
1276/// v7.17.0 — `ALTER SEQUENCE` AST node.
1277#[derive(Debug, Clone, PartialEq, Eq)]
1278pub struct AlterSequenceStatement {
1279 pub name: String,
1280 pub if_exists: bool,
1281 pub options: SequenceOptions,
1282 /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1283 /// instead of `options`; the two forms are mutually exclusive in PG.
1284 pub rename_to: Option<String>,
1285}
1286
1287/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1288/// the [`PublicationScope`] shape. v6.1.2 only accepted
1289/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1290/// variants by flipping the parser gate (no AST migration).
1291#[derive(Debug, Clone, PartialEq, Eq)]
1292pub struct CreatePublicationStatement {
1293 pub name: String,
1294 pub scope: PublicationScope,
1295}
1296
1297/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1298/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1299/// variants — the on-disk shape, snapshot serialisation, and the
1300/// AST round-trip Display path were already in place in v6.1.2
1301/// so this is a parser-only widening.
1302#[derive(Debug, Clone, PartialEq, Eq)]
1303pub enum PublicationScope {
1304 AllTables,
1305 ForTables(Vec<String>),
1306 AllTablesExcept(Vec<String>),
1307 /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1308 /// (PG 15+). AST-only: the executor folds `public` to
1309 /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1310 /// and refuses any other schema with PG's sentence, so the
1311 /// catalog / serializer / replication filter never see it.
1312 TablesInSchema(String),
1313}
1314
1315#[derive(Debug, Clone, PartialEq, Eq)]
1316pub struct AlterIndexStatement {
1317 pub name: String,
1318 pub target: AlterIndexTarget,
1319}
1320
1321#[derive(Debug, Clone, PartialEq, Eq)]
1322pub enum AlterIndexTarget {
1323 /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1324 /// rebuilds the existing graph in place without touching the
1325 /// column encoding; `Some(enc)` re-encodes every cell first.
1326 Rebuild { encoding: Option<VecEncoding> },
1327 /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1328 /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1329 /// uses it to make the migration idempotent (re-running on a
1330 /// DB where the rename already happened is a no-op rather
1331 /// than an error).
1332 Rename { new: String, if_exists: bool },
1333 /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1334 /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1335 /// does not exist`), so the index is validated and the storage
1336 /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1337 /// SET/RESET arms already record).
1338 StorageParams,
1339}
1340
1341/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1342/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1343/// can add more SET subjects without changing the dispatch shape.
1344#[derive(Debug, Clone, PartialEq)]
1345pub struct AlterTableStatement {
1346 pub name: String,
1347 /// v7.13.2 — mailrs round-6 S1. One or more subactions
1348 /// separated by commas in the source SQL. PG-semantic apply
1349 /// is sequential; engine bails on first error (no
1350 /// transactional rollback of completed subactions in v7.13).
1351 /// Single-subaction shape stays a 1-element vec.
1352 pub targets: Vec<AlterTableTarget>,
1353}
1354/// v7.39.9 — the `FIRST` / `AFTER c` trailer, written back the way it
1355/// was read.
1356fn write_column_position(
1357 f: &mut core::fmt::Formatter<'_>,
1358 pos: Option<&ColumnPosition>,
1359) -> core::fmt::Result {
1360 match pos {
1361 Some(ColumnPosition::First) => f.write_str(" FIRST"),
1362 Some(ColumnPosition::After(c)) => write!(f, " AFTER {}", quote_ident(c)),
1363 None => Ok(()),
1364 }
1365}
1366
1367/// v7.39.9 — where MySQL's `ADD` / `MODIFY` / `CHANGE` puts a column.
1368///
1369/// The row encoding is positional and `SELECT *` reads it in order, so
1370/// this is an answer, not a formatting preference.
1371#[derive(Debug, Clone, PartialEq, Eq)]
1372pub enum ColumnPosition {
1373 First,
1374 After(String),
1375}
1376
1377#[derive(Debug, Clone, PartialEq)]
1378#[allow(clippy::large_enum_variant)]
1379pub enum AlterTableTarget {
1380 /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1381 ///
1382 /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1383 /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1384 /// the reasoning went stale: `NO INHERIT` reported success while the
1385 /// child stayed attached, which is the worst kind of answer — the
1386 /// statement says it worked and the catalog disagrees.
1387 Inherit { parent: String, detach: bool },
1388 /// Per-table hot-tier byte budget override. The freezer
1389 /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1390 SetHotTierBytes(u64),
1391 /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1392 /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1393 /// Engine validates existing rows against the new constraint
1394 /// before installing it.
1395 AddForeignKey(ForeignKeyConstraint),
1396 /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1397 /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1398 /// no-op when no FK with that name exists; otherwise raises.
1399 DropForeignKey { name: String, if_exists: bool },
1400 /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1401 /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1402 /// as the standalone `DROP INDEX` statement.
1403 DropIndex { name: String, if_exists: bool },
1404 /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1405 /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1406 /// (20 migrate-*.sql hits). Engine appends the column to the
1407 /// schema and back-fills every existing row with the DEFAULT
1408 /// (or NULL when no DEFAULT and the column is nullable).
1409 AddColumn {
1410 column: ColumnDef,
1411 if_not_exists: bool,
1412 /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>`, which say where
1413 /// the column goes. `None` is the PostgreSQL form and appends.
1414 position: Option<ColumnPosition>,
1415 },
1416 /// v7.39.9 — MySQL's `MODIFY COLUMN c <definition>` and
1417 /// `CHANGE COLUMN old new <definition>`.
1418 ///
1419 /// Both REPLACE the column's definition rather than amending it,
1420 /// which is the part that cannot be expressed by the PostgreSQL
1421 /// spellings SPG already had. Measured on MySQL 9.7.2: a column
1422 /// declared `INT NOT NULL DEFAULT 5`, after `MODIFY COLUMN b
1423 /// BIGINT`, is `bigint` NULLABLE with NO default — restating them
1424 /// keeps them, omitting them drops them. `CHANGE` is the same and
1425 /// also renames.
1426 ModifyColumn {
1427 /// The column as it is named now.
1428 column: String,
1429 /// `CHANGE`'s new name; `None` for `MODIFY`, which keeps it.
1430 rename_to: Option<String>,
1431 /// The whole new definition, exactly as written.
1432 definition: ColumnDef,
1433 position: Option<ColumnPosition>,
1434 },
1435 /// v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
1436 RenameIndex { old: String, new: String },
1437 /// v7.39.9 — MySQL's `ALTER TABLE t AUTO_INCREMENT = n`, which sets
1438 /// the value the NEXT insert takes. Measured on 9.7.2: after
1439 /// `= 100`, the next row's id is 100.
1440 SetTableAutoIncrement(i64),
1441 /// v7.39.9 — MySQL's `ENGINE = <name>`. SPG has one storage engine
1442 /// and substitutes for every name MySQL knows, exactly as
1443 /// `CREATE TABLE` already does; a name MySQL does not know is
1444 /// refused with its 1286, because a typo in a migration must not
1445 /// quietly become SPG's storage.
1446 SetEngine(String),
1447 /// v7.39.9 — MySQL's `CONVERT TO CHARACTER SET <cs> [COLLATE <c>]`.
1448 /// SPG stores UTF-8 throughout, so a charset it can represent is
1449 /// accepted and one it cannot is refused with MySQL's 1115.
1450 ConvertToCharacterSet {
1451 charset: String,
1452 collate: Option<String>,
1453 },
1454 /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1455 /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1456 /// existing row's column value by evaluating the optional
1457 /// USING expression (default `col::<ty>`) and re-coercing
1458 /// against the new column type.
1459 AlterColumnType {
1460 column: String,
1461 new_type: ColumnTypeName,
1462 using: Option<Expr>,
1463 /// v7.39 (round 713) — `COLLATE <name>` between the type and
1464 /// USING. PG re-collates the column, and an ABSENT clause RESETS
1465 /// the collation to the type default (measured round 713) — so
1466 /// `None` is not "leave it alone". The type parser consumed the
1467 /// clause all along and this surface dropped it on the floor:
1468 /// the statement succeeded and the ordering did not change, the
1469 /// silent-divergence shape. Folded variant + the name as written.
1470 collation: Option<(Collation, String)>,
1471 },
1472 /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1473 /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1474 /// every row's value at that position is removed; any index
1475 /// on the column is dropped. `if_exists` makes the drop a
1476 /// no-op when the column is missing. `cascade` removes
1477 /// dependents (FKs referencing the column, partial indexes
1478 /// whose predicate names the column); without it, the engine
1479 /// rejects when dependents exist.
1480 DropColumn {
1481 column: String,
1482 if_exists: bool,
1483 cascade: bool,
1484 },
1485 /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1486 /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1487 /// CONSTRAINT name CHECK (expr)` — table-level constraints
1488 /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1489 /// separate ALTER TABLE statement, so this surface lets the
1490 /// dump load straight through.
1491 AddTableConstraint(TableConstraint),
1492 /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1493 /// there is nothing to record; what PG does that SPG did not is
1494 /// REFUSE a role that does not exist. The name has to reach the
1495 /// engine for that, because only the engine knows the roles.
1496 OwnerTo { role: String },
1497 /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1498 /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1499 /// the hint is still a no-op; naming an index that does not exist is
1500 /// not.
1501 ClusterOn { index: Option<String> },
1502 /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1503 /// already in the table against a constraint added `NOT VALID` and,
1504 /// if they all pass, mark it validated. It used to be swallowed as a
1505 /// no-op on the theory that SPG validated at ADD time; SPG did not.
1506 ValidateConstraint { name: String },
1507 /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1508 /// Renames the column in the schema and propagates the rename
1509 /// to every stored source string that references it as a
1510 /// (potentially-qualified) column identifier: CHECK predicates,
1511 /// partial-index predicates, runtime DEFAULT expressions, and
1512 /// triggers' `UPDATE OF` column lists. Function bodies and
1513 /// trigger bodies are NOT auto-rewritten — they're loose
1514 /// source text and may contain references SPG can't statically
1515 /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1516 /// the column even if dependents exist; users renaming a
1517 /// column referenced by a function body update the function
1518 /// body separately.
1519 RenameColumn { old: String, new: String },
1520 /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1521 /// Reachable now that the schema stores user-supplied constraint names.
1522 RenameConstraint { old: String, new: String },
1523 /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1524 /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1525 /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1526 /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1527 /// (identity); both lower to this. SPG's auto-increment is
1528 /// max+1-scan based, so the dump's `setval(…)` calls stay
1529 /// no-ops without losing the sequence position.
1530 SetColumnAutoIncrement {
1531 column: String,
1532 /// The implicit sequence pg_dump names for an identity
1533 /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1534 /// nextval target for a serial default. The engine creates
1535 /// it if absent so the dump's later `setval(s, …)` lands.
1536 seq_name: Option<String>,
1537 },
1538 /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1539 /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1540 /// migrate-042 uses it). The engine moves the table entry
1541 /// in the catalog under the new name; child catalog state
1542 /// (FKs pointing at this table, triggers watching this
1543 /// table) tracks the rename through the storage layer.
1544 RenameTable { new: String },
1545 /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1546 /// { ALL | <name> }`. Toggles whether row-level triggers
1547 /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1548 /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1549 /// ENABLE epilogue around every table's data block so the
1550 /// rows already-computed in prod don't get re-rewritten
1551 /// (and so trigger-driven side effects like
1552 /// audit/queueing don't re-fire during a bulk reload).
1553 /// `which == TriggerSelector::All` toggles every trigger
1554 /// on the table; `Named(name)` toggles one trigger. The
1555 /// engine persists the disabled state on `TriggerDef.enabled`
1556 /// (catalog FILE_VERSION 25+) and the row-write paths skip
1557 /// the trigger when `!enabled`.
1558 SetTriggerEnabled {
1559 which: TriggerSelector,
1560 enabled: bool,
1561 },
1562 /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1563 /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1564 /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1565 /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1566 SetRowSecurity {
1567 enabled: Option<bool>,
1568 force: Option<bool>,
1569 },
1570 /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1571 /// <bounds>`. Promotes an existing table `child` to a partition
1572 /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1573 /// Engine validates that `child`'s columns are layout-compatible
1574 /// with `parent` and that every row in `child` satisfies the
1575 /// bound before installing the role.
1576 AttachPartition {
1577 child: String,
1578 bounds: PartitionOfBoundsAst,
1579 },
1580 /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1581 /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1582 /// to a standalone table (clears `partition_role`) and removes
1583 /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1584 /// is parser-accepted; engine performs the same atomic detach
1585 /// (single-engine, no replication lag — the PG semantics that
1586 /// require the two-phase split don't apply).
1587 DetachPartition {
1588 child: String,
1589 concurrently: bool,
1590 finalize: bool,
1591 },
1592 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1593 /// <expr>`. Engine re-parses + freezes the literal at this point,
1594 /// matching CREATE TABLE-side default semantics. Volatile shapes
1595 /// (`now()` / `nextval`) take the runtime-default path.
1596 AlterColumnSetDefault { column: String, default_expr: Expr },
1597 /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1598 AlterColumnDropDefault { column: String },
1599 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1600 /// Engine validates that no existing row has NULL in that column
1601 /// before flipping the flag (PG semantics — partial NOT NULL
1602 /// would surface inconsistently).
1603 AlterColumnSetNotNull { column: String },
1604 /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1605 AlterColumnDropNotNull { column: String },
1606 /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1607 /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1608 /// column's start value = 1). Engine records a next-value floor over
1609 /// SPG's max+1 identity allocation.
1610 AlterColumnRestart { column: String, with: Option<i64> },
1611 /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1612 /// EXPRESSION` turns a stored generated column into a plain column
1613 /// (its generation expression is removed; existing values are kept).
1614 AlterColumnDropExpression { column: String, if_exists: bool },
1615 /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1616 /// de-generate an identity column into a plain column.
1617 AlterColumnDropIdentity { column: String, if_exists: bool },
1618 /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1619 /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1620 /// expression and recomputes every existing row.
1621 AlterColumnSetExpression { column: String, expr: Expr },
1622 /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1623 /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1624 /// (PG: `type "x" does not exist`).
1625 OfType { type_name: String },
1626 /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1627 /// identity setting no-ops (SPG has no logical replication consumer);
1628 /// the INDEX must exist on this table (PG: `index "i" for table "t"
1629 /// does not exist`).
1630 ReplicaIdentityUsingIndex { index: String },
1631}
1632
1633/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1634/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1635/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1636/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1637/// shouldn't surface from a dump.
1638#[derive(Debug, Clone, PartialEq, Eq)]
1639pub enum TriggerSelector {
1640 /// Every trigger on the table.
1641 All,
1642 /// A specific trigger by name.
1643 Named(String),
1644}
1645
1646/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1647/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1648/// bitflags word or a nested options struct would only relocate the lint
1649/// while making the option each caller sets harder to read.
1650#[allow(clippy::struct_excessive_bools)]
1651#[derive(Debug, Clone, PartialEq)]
1652pub struct ExplainStatement {
1653 pub analyze: bool,
1654 /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1655 /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1656 /// `Insert on / Update on / Delete on` trees for them.
1657 pub inner: Box<Statement>,
1658 /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1659 /// advisor pass: after the regular plan tree, the engine
1660 /// emits one suggestion line per column referenced in the
1661 /// query's WHERE / JOIN that has no covering index on the
1662 /// owning table.
1663 pub suggest: bool,
1664 /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1665 /// `elapsed=…us` annotations from the Total line (and any
1666 /// future cost-bearing lines). PG-standard option used by
1667 /// regression suites and diff-friendly EXPLAIN output. When
1668 /// `true`, takes precedence over the per-session
1669 /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1670 pub costs_off: bool,
1671 /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1672 /// option that surfaces hot/cold/shared block counters. SPG's
1673 /// hot-tier scan path counts examined rows; the BUFFERS option
1674 /// makes that an explicit per-operator annotation.
1675 pub buffers: bool,
1676 /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1677 /// uses this to disable per-operator timing while still
1678 /// emitting actual-row counts (cheaper than ANALYZE). Default
1679 /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1680 /// timing portion of the Total line. Decoupled from `costs_off`:
1681 /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1682 /// measured wall-clock.
1683 pub timing_off: bool,
1684 /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1685 /// modified GUC values to the plan output. SPG emits the
1686 /// session params that diverge from default after the main
1687 /// plan body.
1688 pub settings: bool,
1689 /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1690 /// bytes / records / FPI emitted by the query. SPG's
1691 /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1692 /// ANALYZE) report against the engine WAL counter delta.
1693 pub wal: bool,
1694 /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1695 /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1696 /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1697 /// this is set.
1698 pub summary_off: bool,
1699 /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1700 /// PG's standard format selector. Default is text. JSON / XML
1701 /// / YAML emit a single-row TEXT result whose body wraps the
1702 /// existing line-per-operator text in the chosen container —
1703 /// PG-compatible just enough for dashboards that parse those
1704 /// container shapes (pgAdmin's JSON path picker, etc.).
1705 pub format: ExplainFormat,
1706}
1707
1708#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1709pub enum ExplainFormat {
1710 #[default]
1711 Text,
1712 Json,
1713 Xml,
1714 Yaml,
1715}
1716
1717/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1719pub enum PolicyCmd {
1720 All,
1721 Select,
1722 Insert,
1723 Update,
1724 Delete,
1725}
1726
1727/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1728/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1729#[derive(Debug, Clone, PartialEq)]
1730pub struct CreatePolicyStatement {
1731 pub name: String,
1732 pub table: String,
1733 /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1734 pub permissive: bool,
1735 pub cmd: PolicyCmd,
1736 /// Empty = PUBLIC.
1737 pub roles: Vec<String>,
1738 pub using: Option<Expr>,
1739 pub with_check: Option<Expr>,
1740}
1741
1742/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1743/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1744/// or the command (matches PG).
1745#[derive(Debug, Clone, PartialEq)]
1746pub struct AlterPolicyStatement {
1747 pub name: String,
1748 pub table: String,
1749 pub rename_to: Option<String>,
1750 pub roles: Option<Vec<String>>,
1751 pub using: Option<Expr>,
1752 pub with_check: Option<Expr>,
1753}
1754
1755/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1756#[derive(Debug, Clone, PartialEq, Eq)]
1757pub struct DropPolicyStatement {
1758 pub name: String,
1759 pub table: String,
1760 pub if_exists: bool,
1761}
1762
1763#[derive(Debug, Clone, PartialEq, Eq)]
1764pub struct CreateUserStatement {
1765 pub name: String,
1766 /// Empty when the statement carried no PASSWORD — legal for a bare
1767 /// `CREATE ROLE`, which cannot log in anyway.
1768 pub password: String,
1769 /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1770 /// the parser; the engine validates against `Role::parse` so a
1771 /// typo lands as a runtime error with a clear message rather than
1772 /// a parse failure.
1773 pub role: String,
1774 /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1775 /// statement did not say, so the default for its spelling applies:
1776 /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1777 /// both default to INHERIT and NOSUPERUSER.
1778 pub login: Option<bool>,
1779 pub inherit: Option<bool>,
1780 pub superuser: Option<bool>,
1781 /// `true` when spelled `CREATE USER` (LOGIN by default).
1782 pub is_user: bool,
1783}
1784
1785/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1786/// it tells the planner how far a call may be moved or folded. SPG records
1787/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1788/// yet exploit it for constant folding.
1789#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1790pub enum FunctionVolatility {
1791 Immutable,
1792 Stable,
1793 #[default]
1794 Volatile,
1795}
1796
1797impl FunctionVolatility {
1798 /// PG's one-character `pg_proc.provolatile` code.
1799 #[must_use]
1800 pub const fn as_pg_char(self) -> &'static str {
1801 match self {
1802 Self::Immutable => "i",
1803 Self::Stable => "s",
1804 Self::Volatile => "v",
1805 }
1806 }
1807
1808 #[must_use]
1809 pub const fn as_sql(self) -> &'static str {
1810 match self {
1811 Self::Immutable => "IMMUTABLE",
1812 Self::Stable => "STABLE",
1813 Self::Volatile => "VOLATILE",
1814 }
1815 }
1816}
1817
1818/// v7.39 (round 322, V46) — PG's parallel-safety class.
1819#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1820pub enum FunctionParallel {
1821 #[default]
1822 Unsafe,
1823 Restricted,
1824 Safe,
1825}
1826
1827impl FunctionParallel {
1828 /// PG's one-character `pg_proc.proparallel` code.
1829 #[must_use]
1830 pub const fn as_pg_char(self) -> &'static str {
1831 match self {
1832 Self::Unsafe => "u",
1833 Self::Restricted => "r",
1834 Self::Safe => "s",
1835 }
1836 }
1837
1838 #[must_use]
1839 pub const fn as_sql(self) -> &'static str {
1840 match self {
1841 Self::Unsafe => "PARALLEL UNSAFE",
1842 Self::Restricted => "PARALLEL RESTRICTED",
1843 Self::Safe => "PARALLEL SAFE",
1844 }
1845 }
1846}
1847
1848/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1849/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1850/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1851/// language's default cost / rows.
1852#[derive(Debug, Clone, Copy, PartialEq, Default)]
1853pub struct FunctionAttrs {
1854 pub volatility: FunctionVolatility,
1855 /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1856 /// argument returns NULL without running the body.
1857 pub strict: bool,
1858 pub security_definer: bool,
1859 pub leakproof: bool,
1860 pub parallel: FunctionParallel,
1861 /// `COST n` — `None` leaves PG's per-language default.
1862 pub cost: Option<f64>,
1863 /// `ROWS n` — set-returning functions only; `None` = default.
1864 pub rows: Option<f64>,
1865}
1866
1867impl FunctionAttrs {
1868 /// The attribute words `pg_get_functiondef` puts on their own line,
1869 /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1870 /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1871 /// at its default — PG then emits no such line at all.
1872 #[must_use]
1873 pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1874 let mut out = alloc::vec::Vec::new();
1875 if self.volatility != FunctionVolatility::Volatile {
1876 out.push(alloc::string::String::from(self.volatility.as_sql()));
1877 }
1878 if self.parallel != FunctionParallel::Unsafe {
1879 out.push(alloc::string::String::from(self.parallel.as_sql()));
1880 }
1881 if self.strict {
1882 out.push(alloc::string::String::from("STRICT"));
1883 }
1884 if self.security_definer {
1885 out.push(alloc::string::String::from("SECURITY DEFINER"));
1886 }
1887 if self.leakproof {
1888 out.push(alloc::string::String::from("LEAKPROOF"));
1889 }
1890 if let Some(c) = self.cost {
1891 out.push(alloc::format!("COST {}", render_attr_number(c)));
1892 }
1893 if let Some(r) = self.rows {
1894 out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1895 }
1896 out
1897 }
1898}
1899
1900/// PG prints a whole-numbered cost / rows without a decimal point.
1901fn render_attr_number(v: f64) -> alloc::string::String {
1902 // no_std: `f64::fract` lives in std, so compare against the truncation.
1903 let whole = v as i64;
1904 if v.abs() < 1e15 && (whole as f64) == v {
1905 alloc::format!("{whole}")
1906 } else {
1907 alloc::format!("{v}")
1908 }
1909}
1910
1911/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1912/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1913/// (the row-level trigger body the CREATE TRIGGER below references).
1914/// Non-trigger user-defined functions parse but error at execution
1915/// time with a clear unsupported message; that surface lands in
1916/// v7.12.5+.
1917#[derive(Debug, Clone, PartialEq)]
1918pub struct CreateFunctionStatement {
1919 pub name: String,
1920 /// `OR REPLACE` was present; an existing function with the
1921 /// same name is overwritten instead of erroring.
1922 pub or_replace: bool,
1923 /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1924 /// list `()` (sufficient for trigger functions). Other shapes
1925 /// parse and store the args but the executor refuses to call
1926 /// them.
1927 pub args: Vec<FunctionArg>,
1928 /// `RETURNS <type>` — `trigger` is the supported shape for
1929 /// v7.12.4; arbitrary return types parse to
1930 /// [`FunctionReturn::Other`].
1931 pub returns: FunctionReturn,
1932 /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1933 /// side of `AS $$...$$`; the parser canonicalises to one slot.
1934 /// `plpgsql` and `sql` are the two interesting values.
1935 pub language: String,
1936 /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1937 /// a structured AST; non-trigger / non-plpgsql bodies stay as
1938 /// the raw source text so the v7.12.5+ executor can pick them
1939 /// up without a parser rev.
1940 pub body: FunctionBody,
1941 /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1942 /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1943 /// on either side of the body; before this they were a parse error, so
1944 /// PG's own `pg_dump` output would not restore.
1945 pub attrs: FunctionAttrs,
1946}
1947
1948/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1949#[derive(Debug, Clone, PartialEq)]
1950pub struct FunctionArg {
1951 /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1952 /// (the default); `OUT` / `INOUT` parse but the executor
1953 /// refuses them.
1954 pub mode: FunctionArgMode,
1955 /// Optional arg name. Trigger functions traditionally don't
1956 /// name their args (they read NEW/OLD instead), so `None` is
1957 /// the common case.
1958 pub name: Option<String>,
1959 /// Declared type, normalised to the SPG `DataType` mapping
1960 /// where one exists. Unknown / extension types parse as a
1961 /// raw string under [`FunctionArgType::Raw`].
1962 pub ty: FunctionArgType,
1963}
1964
1965#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1966pub enum FunctionArgMode {
1967 In,
1968 Out,
1969 InOut,
1970}
1971
1972#[derive(Debug, Clone, PartialEq)]
1973pub enum FunctionArgType {
1974 Typed(ColumnTypeName),
1975 /// Unknown / extension types — kept as the parser-side raw
1976 /// identifier so error messages can name them precisely.
1977 Raw(String),
1978}
1979
1980#[derive(Debug, Clone, PartialEq)]
1981pub enum FunctionReturn {
1982 /// `RETURNS TRIGGER` — the row-level trigger function shape.
1983 /// v7.12.4 ships exactly this for execution.
1984 Trigger,
1985 /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
1986 /// the function is unused (since v7.12.4 doesn't ship scalar
1987 /// function invocation).
1988 Void,
1989 /// `RETURNS <type>` for any concrete data type. Reserved for
1990 /// v7.12.5+'s scalar UDF surface.
1991 Type(ColumnTypeName),
1992 /// `RETURNS <ident>` for types SPG doesn't know — extension
1993 /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
1994 Other(String),
1995}
1996
1997#[derive(Debug, Clone, PartialEq)]
1998pub enum FunctionBody {
1999 /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
2000 /// trigger-function executor walks this directly without
2001 /// re-parsing.
2002 PlPgSql(PlPgSqlBlock),
2003 /// Raw source text — parser couldn't (or didn't try to)
2004 /// structure-parse the body. Used for `LANGUAGE sql`
2005 /// functions and any PL/pgSQL body that contains v7.12.5+
2006 /// features the v7.12.4 parser doesn't yet recognise. The
2007 /// executor returns an unsupported error when invoked.
2008 Raw(String),
2009}
2010
2011/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
2012/// from assignment + return to a real-PL/pgSQL surface:
2013/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
2014/// control flow, `RAISE` diagnostics, and embedded SQL
2015/// statements that execute through the regular engine path.
2016/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
2017/// which mailrs's trigger doesn't need but other PG customers
2018/// may; deferred to a future minor release.
2019#[derive(Debug, Clone, PartialEq)]
2020pub struct PlPgSqlBlock {
2021 /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
2022 /// preceding `BEGIN`. Empty when the body opens directly with
2023 /// `BEGIN`. Declarations execute in order; each may reference
2024 /// earlier-declared locals in its init expression.
2025 pub declarations: Vec<PlPgSqlDeclare>,
2026 pub statements: Vec<PlPgSqlStmt>,
2027 /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
2028 /// <body>` handlers appended to the block. Empty when no
2029 /// EXCEPTION clause is present. When a body statement raises
2030 /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
2031 /// handlers are tried in order; the first matching condition
2032 /// runs its body and the block terminates cleanly. `OTHERS`
2033 /// matches any exception. Unhandled exceptions propagate.
2034 pub exception_handlers: Vec<ExceptionHandler>,
2035}
2036
2037/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
2038/// arm inside an EXCEPTION block.
2039#[derive(Debug, Clone, PartialEq)]
2040pub struct ExceptionHandler {
2041 /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
2042 /// conditions joined by `OR` share one handler body.
2043 pub conditions: Vec<String>,
2044 /// Statements to run when a matching exception is caught.
2045 pub body: Vec<PlPgSqlStmt>,
2046}
2047
2048/// v7.12.6 — single `DECLARE` entry: variable name + declared
2049/// type + optional initialiser. Variables default to SQL NULL
2050/// when no init is given (matches PG).
2051#[derive(Debug, Clone, PartialEq)]
2052pub struct PlPgSqlDeclare {
2053 pub name: String,
2054 /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
2055 /// knows it; raw text otherwise).
2056 pub ty: FunctionArgType,
2057 pub default: Option<Expr>,
2058}
2059
2060#[derive(Debug, Clone, PartialEq)]
2061pub enum PlPgSqlStmt {
2062 /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
2063 /// for clarity in error reporting (PG also forbids it) — the
2064 /// executor errors with a clear "OLD is read-only" message.
2065 Assign { target: AssignTarget, value: Expr },
2066 /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
2067 /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
2068 /// the SELECT statement with the INTO clause stripped; the
2069 /// engine runs it via `Engine::execute`, takes the first
2070 /// row's first column, and assigns to the local variable
2071 /// in the DECLARE scope. Single-column / single-row
2072 /// queries only at v7.16.2; multi-target (`INTO a, b`) is
2073 /// a v7.16.x follow-up.
2074 SelectInto {
2075 var: String,
2076 body: Box<SelectStatement>,
2077 },
2078 /// `RETURN <target>;` — trigger functions canonically return
2079 /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
2080 /// expression for forward compatibility with scalar UDFs.
2081 Return(ReturnTarget),
2082 /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
2083 /// set a SETOF function is building, and KEEP GOING. Not a return.
2084 ReturnNext(Expr),
2085 /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
2086 /// query yields, and keep going. It used to desugar to a side-effect
2087 /// statement whose result was DISCARDED — in a SETOF function that is the
2088 /// whole answer thrown away.
2089 ReturnQuery(Box<SelectStatement>),
2090 /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
2091 /// twin. Its rows go to the set too; it used to run and discard them.
2092 ReturnQueryExecute { sql: Expr },
2093 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
2094 /// [ELSE body] END IF;`. Branches are tried in order; first
2095 /// truthy condition wins; the optional ELSE runs when no
2096 /// condition matched.
2097 If {
2098 branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
2099 else_branch: Vec<PlPgSqlStmt>,
2100 },
2101 /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
2102 /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
2103 /// (logging — observable side effect only) or `EXCEPTION`
2104 /// (aborts the trigger and propagates as an error). v7.12.6
2105 /// supports the basic format-string substitution PG uses
2106 /// (`%` placeholders consumed positionally).
2107 Raise {
2108 level: RaiseLevel,
2109 message: String,
2110 args: Vec<Expr>,
2111 },
2112 /// v7.12.6 — embedded SQL statement inside the trigger body
2113 /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
2114 /// NEW.col / OLD.col references inside the embedded
2115 /// statement's expression tree are substituted with the
2116 /// current trigger context before the engine re-executes the
2117 /// statement. Recursion depth into nested triggers is
2118 /// bounded by the engine's existing trigger-fire guard.
2119 EmbeddedSql(Box<Statement>),
2120 /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
2121 /// the condition evaluates falsy the trigger / DO block aborts
2122 /// with the message (defaulting to a generic shape when none
2123 /// is provided). Same propagation shape as `RAISE EXCEPTION`
2124 /// — the error reaches the caller's query path. PG's behaviour
2125 /// is identical except for a `plpgsql.check_asserts` GUC that
2126 /// can disable the check globally; SPG always evaluates.
2127 Assert {
2128 condition: Expr,
2129 message: Option<Expr>,
2130 },
2131 /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2132 /// Iterate the body while condition evaluates truthy. Iteration
2133 /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2134 /// loops; the executor errors out when reached. EXIT / CONTINUE
2135 /// inside the body queue with 20.2.
2136 While {
2137 condition: Expr,
2138 body: Vec<PlPgSqlStmt>,
2139 },
2140 /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2141 /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2142 /// bounds inclusive on both sides. REVERSE walks backward.
2143 /// Iteration budget guards runaway.
2144 ForRange {
2145 var: String,
2146 start: Expr,
2147 end: Expr,
2148 reverse: bool,
2149 body: Vec<PlPgSqlStmt>,
2150 },
2151 /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2152 /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2153 /// budget guards runaway.
2154 Loop { body: Vec<PlPgSqlStmt> },
2155 /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2156 /// Unconditional (no WHEN) or conditional (only breaks when
2157 /// condition is truthy). Bubbles up as BodyOutcome::Break which
2158 /// the enclosing loop catches. Outside a loop it's a no-op.
2159 Exit { when: Option<Expr> },
2160 /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2161 /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2162 /// which the enclosing loop catches, skipping the remainder of
2163 /// the body and jumping to the next iteration.
2164 Continue { when: Option<Expr> },
2165 /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2166 /// computed SQL statement. The expression is evaluated to a
2167 /// text value, the resulting string is parsed and dispatched
2168 /// through the engine like an EmbeddedSql. USING <param_list>
2169 /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2170 ExecuteDynamic { sql: Expr },
2171 /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2172 /// END LOOP;`. Runs the SELECT once, iterates the resulting
2173 /// rows, binds the first column of each row to `var` as a
2174 /// scalar Value, then runs the body per iteration. EXIT /
2175 /// CONTINUE / ASSERT / RAISE etc. propagate through the
2176 /// enclosing loop's BodyOutcome discipline the same way
2177 /// FOR range and WHILE do. Full record-binding (var as
2178 /// composite carrying all columns) queues with v7.40 record
2179 /// type infrastructure.
2180 ForQuery {
2181 var: String,
2182 query: Box<SelectStatement>,
2183 body: Vec<PlPgSqlStmt>,
2184 },
2185 /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2186 /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2187 /// computed at runtime from a text expression, parsed on the
2188 /// fly, then iterated. Enables dynamic queries where the
2189 /// projection / FROM / WHERE clauses depend on runtime values.
2190 ForExecute {
2191 var: String,
2192 sql_expr: Expr,
2193 body: Vec<PlPgSqlStmt>,
2194 },
2195}
2196
2197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2198pub enum RaiseLevel {
2199 /// `RAISE NOTICE` — diagnostic message, observable in the
2200 /// server log. Does not affect the trigger's outcome.
2201 Notice,
2202 /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2203 Warning,
2204 /// `RAISE INFO` — like NOTICE, slightly quieter.
2205 Info,
2206 /// `RAISE LOG` — like NOTICE, lower priority.
2207 Log,
2208 /// `RAISE DEBUG` — like NOTICE, lowest priority.
2209 Debug,
2210 /// `RAISE EXCEPTION` — aborts the trigger function with the
2211 /// given message, propagating up to the caller as a query-
2212 /// level error.
2213 Exception,
2214}
2215
2216#[derive(Debug, Clone, PartialEq)]
2217pub enum AssignTarget {
2218 NewColumn(String),
2219 OldColumn(String),
2220 /// Reserved for v7.12.5 DECLARE'd local variables.
2221 Local(String),
2222}
2223
2224#[derive(Debug, Clone, PartialEq)]
2225pub enum ReturnTarget {
2226 /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2227 /// actually gets written (possibly with NEW.col mutations
2228 /// applied). For AFTER triggers, the return value is ignored.
2229 New,
2230 /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2231 /// the delete proceed; for BEFORE UPDATE / INSERT it's
2232 /// equivalent to dropping the write.
2233 Old,
2234 /// `RETURN NULL;` — for BEFORE triggers, skips the write
2235 /// entirely. For AFTER, the return value is ignored.
2236 Null,
2237 /// `RETURN <expr>;` — non-row return shape; reserved for the
2238 /// scalar UDF surface in v7.12.5+. Executor errors when used
2239 /// inside a trigger function.
2240 Expr(Expr),
2241}
2242
2243/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2244/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2245/// but the executor refuses them. `WHEN (cond)` clauses are out
2246/// of scope; the trigger function can short-circuit on a leading
2247/// IF inside its body once v7.12.5 lands IF.
2248#[derive(Debug, Clone, PartialEq)]
2249pub struct CreateTriggerStatement {
2250 pub name: String,
2251 pub or_replace: bool,
2252 pub timing: TriggerTiming,
2253 /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2254 /// three entries in order.
2255 pub events: Vec<TriggerEvent>,
2256 pub table: String,
2257 /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2258 /// only `Row`; `Statement` parses but the executor refuses.
2259 pub for_each: TriggerForEach,
2260 /// Name of the function to invoke. The function must exist at
2261 /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2262 /// forward reference (`function no_such_fn() does not exist`), so
2263 /// requiring it IS the PG behaviour (the old note claimed the
2264 /// opposite).
2265 pub function: String,
2266 /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2267 /// (mailrs round-5 G7). Non-empty only when the events list
2268 /// contains UPDATE and the user wrote the column-list filter.
2269 /// PG fires the trigger only when at least one of these
2270 /// columns appears in the SET clause; SPG conservatively
2271 /// fires on any UPDATE matching the listed columns or
2272 /// rewriting them at the row level. Empty vec = no filter
2273 /// (fire on every UPDATE).
2274 pub update_columns: Vec<String>,
2275 /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2276 /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2277 /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2278 pub when_condition: Option<Expr>,
2279}
2280
2281/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2282#[derive(Debug, Clone, PartialEq)]
2283pub struct CreateRuleStatement {
2284 pub name: String,
2285 pub or_replace: bool,
2286 /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2287 pub event: String,
2288 pub table: String,
2289 /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2290 /// (run alongside; PG's default when neither keyword is written).
2291 pub instead: bool,
2292 /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2293 pub when_condition: Option<Expr>,
2294 /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2295 pub commands: Vec<Statement>,
2296}
2297
2298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2299pub enum TriggerTiming {
2300 /// Fires before the row is written; the trigger function's
2301 /// return value (NEW or NULL) decides the row content and
2302 /// whether the write proceeds at all.
2303 Before,
2304 /// Fires after the row is written; the return value is
2305 /// ignored.
2306 After,
2307 /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2308 /// v7.12.4 (SPG has no updatable-view surface).
2309 InsteadOf,
2310}
2311
2312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2313pub enum TriggerEvent {
2314 Insert,
2315 Update,
2316 Delete,
2317 /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2318 /// so the trigger never fires.
2319 Truncate,
2320}
2321
2322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2323pub enum TriggerForEach {
2324 Row,
2325 Statement,
2326}
2327
2328/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2329///
2330/// SPG's index does not scan in a direction, but `indexdef` reproduces
2331/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2332/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2333/// which case PG's default applies — LAST for ascending, FIRST for
2334/// descending, and neither is rendered.
2335#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2336pub struct IndexColumnOrder {
2337 pub descending: bool,
2338 pub nulls_first: Option<bool>,
2339}
2340
2341#[derive(Debug, Clone, PartialEq)]
2342pub struct CreateIndexStatement {
2343 pub name: String,
2344 /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2345 /// either way, so this changes nothing about how the index is made
2346 /// — it is carried because PG refuses the CONCURRENTLY form inside
2347 /// a transaction block and accepts the plain one, and the engine
2348 /// cannot tell them apart without it.
2349 pub concurrently: bool,
2350 /// v7.39 (round 537) — the leading key column's ordering clause,
2351 /// which is the column SPG indexes.
2352 pub key_order: IndexColumnOrder,
2353 /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2354 /// written. SPG orders text by bytes, so honouring it changes
2355 /// nothing; PG prints it, because an explicitly named collation and
2356 /// the one a column inherits are different objects.
2357 pub key_collation: Option<String>,
2358 pub table: String,
2359 pub column: String,
2360 /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2361 /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2362 /// any NULL in the key exempts the row from the uniqueness check.
2363 pub nulls_not_distinct: bool,
2364 /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2365 /// graph for vector kNN); unspecified is the default B-tree index.
2366 pub method: IndexMethod,
2367 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2368 /// index name already exists, instead of raising `DuplicateIndex`.
2369 pub if_not_exists: bool,
2370 /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2371 /// non-key columns the planner should treat as "covered" by
2372 /// this index when checking whether a query can run as an
2373 /// index-only scan. Empty when no `INCLUDE` clause was given.
2374 pub included_columns: Vec<String>,
2375 /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2376 /// for which `<expr>` evaluates truthy enter the index;
2377 /// queries whose `WHERE` clause's canonical Display form
2378 /// matches this expression's Display form can be served by the
2379 /// partial index. Stored as a parsed `Expr` so the engine
2380 /// re-uses the existing evaluation path; storage persists the
2381 /// Display form on the catalog snapshot.
2382 pub partial_predicate: Option<Expr>,
2383 /// v6.8.2 — expression-based index. When `Some(expr)`, the
2384 /// index key is the result of `expr` evaluated on each row
2385 /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2386 /// field still names the *primary* column the expression
2387 /// touches so existing planner shortcuts that resolve a
2388 /// column position stay valid. `None` = plain
2389 /// column-reference index (the legacy shape).
2390 pub expression: Option<Expr>,
2391 /// v7.9.14 — extra column names after the leading column in a
2392 /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2393 /// planner today still only uses the leading column for index
2394 /// seeks; the extras are tracked verbatim so the same DDL
2395 /// round-trips through WAL replay + catalog snapshot, and so
2396 /// the engine can emit a clear warning at INDEX CREATE time
2397 /// that only the leading column is currently honoured.
2398 /// Composite BTree index keys land in v7.10.
2399 pub extra_columns: Vec<String>,
2400 /// v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
2401 /// / `NULLS LAST`, positionally aligned with `extra_columns`. The
2402 /// parser used to discard these, so a composite index's direction
2403 /// survived only on the leading column and `pg_get_indexdef`
2404 /// rendered `(a, b DESC)` back as `(a, b)`.
2405 pub extra_orders: Vec<IndexColumnOrder>,
2406 /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2407 /// enforces uniqueness on the indexed key (combined with the
2408 /// `partial_predicate` filter — only rows where the predicate
2409 /// evaluates truthy enter the uniqueness check). Standard SQL
2410 /// and PG's canonical way to express conditional uniqueness.
2411 /// mailrs K1.
2412 pub is_unique: bool,
2413 /// v7.15.0 — operator class on the leading column, when the
2414 /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2415 /// Lower-cased. Most opclasses are still informational; the
2416 /// engine routes on `gin_trgm_ops` specifically to build a
2417 /// trigram-shingle GIN over a TEXT column, and otherwise
2418 /// keeps the current "accepted and discarded" behaviour for
2419 /// pg_dump compatibility.
2420 pub opclass: Option<String>,
2421 /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2422 /// there was no `USING` clause.
2423 ///
2424 /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2425 /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2426 /// implementation for still load. That degradation is deliberate, but
2427 /// it loses the name — and the operator-class check needs it, both to
2428 /// look the class up under the AM the user actually named and to say
2429 /// which AM it was missing from, the way PG's message does.
2430 pub method_name: Option<String>,
2431}
2432
2433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2434pub enum IndexMethod {
2435 /// Default — B-tree over `IndexKey`. Used for equality / range
2436 /// lookups on scalar columns.
2437 BTree,
2438 /// `USING hnsw` — NSW graph for kNN over a vector column.
2439 Hnsw,
2440 /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2441 /// metadata that records (min_key, max_key) for each page in a
2442 /// cold-tier segment, on the indexed column. The optimizer
2443 /// can use these summaries to skip pages whose range does NOT
2444 /// overlap a query's WHERE predicate. BRIN indexes carry no
2445 /// in-memory data — the summaries live in the segment v2
2446 /// envelope's sidecar. Created via the standard
2447 /// `CREATE INDEX … USING brin (col)` syntax.
2448 Brin,
2449 /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2450 /// column. Posting lists map `lexeme word` → row locators; the
2451 /// planner uses them to narrow `WHERE col @@ tsquery` to the
2452 /// candidate rows whose vectors contain a matching term, then
2453 /// re-evaluates the full `@@` semantics on each candidate.
2454 /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2455 /// silently degraded to a full scan at query time.
2456 Gin,
2457}
2458
2459/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2460/// inside a CREATE TABLE column list.
2461///
2462/// The source table's shape can only be read from the catalog, so the
2463/// parser records the clause and the engine expands it. `at` is how many
2464/// explicit columns preceded it: PG keeps the written order, so
2465/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2466#[derive(Debug, Clone, PartialEq)]
2467pub struct LikeSpec {
2468 pub source: String,
2469 pub at: usize,
2470 pub options: LikeOptions,
2471 /// v7.40.0 — MySQL's `CREATE TABLE b LIKE a` keeps the source's
2472 /// index names (`PRIMARY`, `ks`); PostgreSQL's
2473 /// `CREATE TABLE b (LIKE a INCLUDING ALL)` renames the copies after
2474 /// the new table (`lb_pkey`, `lb_s_idx`). Both measured. The
2475 /// spelling says which engine's rule applies.
2476 pub keep_index_names: bool,
2477}
2478
2479/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2480/// types and NOT NULL and nothing else — measured on PG18, where a
2481/// copied generated column becomes a plain one and a copied identity
2482/// column loses its identity.
2483#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2484pub struct LikeOptions {
2485 pub defaults: bool,
2486 pub constraints: bool,
2487 pub identity: bool,
2488 pub generated: bool,
2489 pub indexes: bool,
2490 pub comments: bool,
2491}
2492
2493#[derive(Debug, Clone, PartialEq)]
2494pub struct CreateTableStatement {
2495 /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2496 /// creating session's own namespace: it shadows a permanent table of the
2497 /// same name, other sessions never see it, and it is dropped when the
2498 /// session ends. A `bool` here lands in the struct's existing padding.
2499 pub temporary: bool,
2500 pub name: String,
2501 /// v7.39 — the `ENGINE=` a MySQL dump names. Consumed and discarded
2502 /// before, so `ENGINE=NONSUCH` built a table where MySQL 9.7.2
2503 /// answers `ERROR 1286`, and `sql_mode` claimed
2504 /// `NO_ENGINE_SUBSTITUTION` while doing it.
2505 pub engine: Option<String>,
2506 /// v7.40.0 — the `AUTO_INCREMENT=N` table option: the next value
2507 /// the table hands out. It was consumed and dropped, so the first
2508 /// row of a table declared `AUTO_INCREMENT=100` got 1 where MySQL
2509 /// 9.7.2 gives it 100 — and `SHOW CREATE TABLE`, which reproduces
2510 /// the option from the counter, round-tripped a different number.
2511 pub auto_increment: Option<i64>,
2512 pub columns: Vec<ColumnDef>,
2513 /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2514 /// the order written. Empty for a table that has none.
2515 pub like_specs: Vec<LikeSpec>,
2516 /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2517 /// Empty for a table that inherits from nothing. Order matters:
2518 /// the child takes each parent's columns in this order before its
2519 /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2520 pub inherits: Vec<String>,
2521 /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2522 /// table name already exists, instead of raising `DuplicateTable`.
2523 pub if_not_exists: bool,
2524 /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2525 /// constraints. Column-level `REFERENCES` (single-column inline
2526 /// form) is normalised into this vec at parse time so the engine
2527 /// sees one uniform list.
2528 pub foreign_keys: Vec<ForeignKeyConstraint>,
2529 /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2530 /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2531 /// Engine resolves each into a BTree index named after the
2532 /// constraint's leading column at CREATE TABLE time; INSERT
2533 /// path enforces composite uniqueness via row scan on the
2534 /// leading column index.
2535 pub table_constraints: Vec<TableConstraint>,
2536 /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2537 /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2538 /// the engine creates a parent table whose own rows stay
2539 /// empty and routes INSERT/SELECT through children. Mutually
2540 /// exclusive with `partition_of` (parser enforces).
2541 pub partition_by: Option<PartitionBySpec>,
2542 /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2543 /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2544 /// the table inherits its column list from `parent` (the
2545 /// parser rejects an explicit column list when this is set);
2546 /// engine routes child rows back to the parent at INSERT.
2547 pub partition_of: Option<PartitionOfSpec>,
2548}
2549
2550/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2551/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2552/// future LIST / HASH without breaking the public AST shape.
2553#[derive(Debug, Clone, PartialEq)]
2554pub struct PartitionBySpec {
2555 pub kind: PartitionKindAst,
2556 /// One or more ident references into the parent's column list.
2557 /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2558 /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2559 /// shape PG-compatible.
2560 pub key_columns: Vec<String>,
2561}
2562
2563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2564pub enum PartitionKindAst {
2565 Range,
2566 /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2567 /// `FOR VALUES IN (lit, lit, …)`.
2568 List,
2569 /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2570 /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2571 Hash,
2572}
2573
2574/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2575/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2576/// or the catch-all `DEFAULT` partition.
2577#[derive(Debug, Clone, PartialEq)]
2578pub struct PartitionOfSpec {
2579 pub parent_name: String,
2580 pub bounds: PartitionOfBoundsAst,
2581}
2582
2583#[derive(Debug, Clone, PartialEq)]
2584pub enum PartitionOfBoundsAst {
2585 /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2586 /// (lits include vector bodies), so we box both bounds to keep
2587 /// the variant size in line with `Default` for clippy and to
2588 /// minimise per-statement footprint when the partition shape
2589 /// isn't in use.
2590 Range {
2591 lower: Box<Expr>,
2592 upper: Box<Expr>,
2593 },
2594 /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2595 /// expr resolves to a typed literal at child-create time.
2596 List {
2597 values: Vec<Expr>,
2598 },
2599 /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2600 /// PG enforces `0 ≤ r < m`; m must be positive.
2601 Hash {
2602 modulus: u32,
2603 remainder: u32,
2604 },
2605 Default,
2606}
2607
2608/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2609/// column list. Either a composite PRIMARY KEY or a UNIQUE
2610/// (single- or multi-column).
2611#[derive(Debug, Clone, PartialEq)]
2612pub enum TableConstraint {
2613 /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2614 /// referenced column. Engine builds a BTree index named
2615 /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2616 PrimaryKey {
2617 name: Option<String>,
2618 columns: Vec<String>,
2619 /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2620 /// Round 621 consumed the clauses; these carry them.
2621 deferrable: bool,
2622 initially_deferred: bool,
2623 },
2624 /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2625 /// named `<table>_<leading_col>_key` (single-column) or
2626 /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2627 /// uniqueness on INSERT.
2628 Unique {
2629 name: Option<String>,
2630 columns: Vec<String>,
2631 /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2632 /// G10). PG 15+ flips the NULL handling so any number of
2633 /// NULL rows collide on the constraint. Default is
2634 /// `false` (NULLS DISTINCT, standard SQL behaviour).
2635 nulls_not_distinct: bool,
2636 /// v7.39 (round 711) — see PrimaryKey.
2637 deferrable: bool,
2638 initially_deferred: bool,
2639 /// v7.40.0 — MySQL's per-column index prefix on a
2640 /// `UNIQUE KEY k (b(4))`, aligned with `columns`. Unlike a
2641 /// plain KEY's, this one CHANGES what the constraint accepts:
2642 /// MySQL rejects two rows sharing the first four characters.
2643 /// Empty for every PostgreSQL spelling.
2644 prefix_lengths: Vec<Option<u32>>,
2645 },
2646 /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2647 /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2648 /// this same variant at parse time. Engine evaluates the
2649 /// predicate against each INSERT/UPDATE candidate row; a
2650 /// false / NULL result rejects the mutation.
2651 /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2652 /// PG adds such a constraint without scanning the existing rows: new
2653 /// rows are checked, the ones already there are grandfathered in, and
2654 /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2655 /// scans and flips it. pg_dump emits the suffix for exactly those, so
2656 /// validating them on restore would refuse a dump PG itself produced.
2657 Check {
2658 name: Option<String>,
2659 expr: Expr,
2660 not_valid: bool,
2661 },
2662 /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2663 /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2664 /// every element (the booking/scheduling non-overlap constraint,
2665 /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2666 /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2667 /// enforcement doesn't build the index yet). Each element pairs a
2668 /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2669 Exclude {
2670 name: Option<String>,
2671 method: Option<String>,
2672 elements: Vec<(String, String)>,
2673 },
2674 /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2675 /// non-unique secondary-index declaration inline in CREATE
2676 /// TABLE. Engine builds a BTree index on the leading column
2677 /// (composite columns parse but only the leading column is
2678 /// honoured at v7.15 — matches the existing
2679 /// `CreateIndexStatement::extra_columns` semantics). Useful
2680 /// for `mysql/blog`-style schemas that lean on routine
2681 /// secondary indexes for ORM lookups.
2682 Index {
2683 name: Option<String>,
2684 columns: Vec<String>,
2685 /// v7.40.0 — MySQL's per-column index prefix, `KEY k (b(4))`,
2686 /// positionally aligned with `columns`. `None` for a column
2687 /// written without one, which is every PostgreSQL index key.
2688 ///
2689 /// It was skipped by the parser and dropped, so the declaration
2690 /// was accepted and the index built over the whole column with
2691 /// nothing recording that a prefix had been asked for —
2692 /// `SHOW INDEX` then reported `Sub_part` NULL and
2693 /// `SHOW CREATE TABLE` printed `(b)` where MySQL 9.7.2 prints
2694 /// `(b(4))`.
2695 prefix_lengths: Vec<Option<u32>>,
2696 },
2697 /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2698 /// (cols)` inline declaration. Pre-v7.17 the parser
2699 /// silently dropped these so MyISAM-imported FULLTEXT
2700 /// indexes vanished; v7.17 routes them through the
2701 /// existing tsvector-GIN engine path so MATCH AGAINST
2702 /// queries get a real inverted index instead of falling
2703 /// back to a full scan. Multi-column FULLTEXT KEYs build
2704 /// one GIN per column at v7.17 (per-column posting lists);
2705 /// the leading column drives query planning.
2706 FulltextIndex {
2707 name: Option<String>,
2708 columns: Vec<String>,
2709 },
2710}
2711
2712#[derive(Debug, Clone, PartialEq)]
2713#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2714pub struct ColumnDef {
2715 pub name: String,
2716 pub ty: ColumnTypeName,
2717 pub nullable: bool,
2718 /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2719 /// evaluates this once (with an empty row) and caches the resulting
2720 /// `Value` on the column schema.
2721 pub default: Option<Expr>,
2722 /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2723 /// per such column and fills the slot when INSERT leaves it
2724 /// unbound (omitted from a column-list INSERT or explicitly NULL).
2725 pub auto_increment: bool,
2726 /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2727 /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2728 /// an implicit BTree index named `<table>_pkey` over this
2729 /// column at CREATE TABLE time, satisfying the parent-side
2730 /// index requirement for any FOREIGN KEY pointing at it.
2731 pub is_primary_key: bool,
2732 /// v7.13.0 — inline `UNIQUE` column constraint
2733 /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2734 /// into a single-column `TableConstraint::Unique` so the
2735 /// engine path stays uniform with table-level UNIQUE.
2736 pub is_unique: bool,
2737 /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2738 /// inline column constraint: treat NULL keys as equal so only one NULL
2739 /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2740 /// `TableConstraint::Unique { nulls_not_distinct }`.
2741 pub unique_nulls_not_distinct: bool,
2742 /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2743 /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2744 /// since this round so the fold into the table-level constraint keeps it.
2745 pub constraint_deferrable: bool,
2746 pub constraint_initially_deferred: bool,
2747 /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2748 /// (mailrs round-5 G3). Stored alongside the column so the
2749 /// CREATE TABLE handler can fold these into table-level
2750 /// CHECK constraints. Multiple inline CHECKs on the same
2751 /// column are concatenated with AND at the table level.
2752 pub check: Option<Expr>,
2753 /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2754 /// parser sees an unknown column-type ident (anything not in
2755 /// the built-in `parse_column_type_name` table), it sets
2756 /// `ty = ColumnTypeName::Text` and records the original name
2757 /// here. The engine resolves at CREATE TABLE time: if a
2758 /// catalog enum/domain with this name exists, the column is
2759 /// bound to it (label-checked on INSERT for enums; CHECK-
2760 /// constrained for domains); otherwise the CREATE TABLE
2761 /// errors with "unknown type".
2762 pub user_type_ref: Option<String>,
2763 /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2764 /// CURRENT_TIMESTAMP` column attribute. When set, an
2765 /// UPDATE that does NOT explicitly bind this column
2766 /// overrides the new value with `now()` (engine clock).
2767 /// Pre-v7.17 SPG silently accepted the syntax and never
2768 /// fired the override — `updated_at` columns from mysqldump
2769 /// stayed pinned at their initial DEFAULT forever, an
2770 /// audit Tier-S silent-failure. Generalised as a stored
2771 /// expression source so future shapes (`ON UPDATE
2772 /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2773 /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2774 pub on_update_runtime: Option<Expr>,
2775 /// v7.17.0 Phase 2.5 — text collation derived from the
2776 /// post-fix `COLLATE <name>` clause (and / or the table-level
2777 /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2778 /// per column). Pre-2.5 SPG accepted the clause and
2779 /// discarded the name, leaving every column byte-compared
2780 /// — a Tier-S silent failure when the customer expected
2781 /// `_ci` / `case_insensitive` semantics. Parser normalises
2782 /// the raw collation name into the variants in `Collation`.
2783 /// Default `Binary` preserves the legacy compare path.
2784 pub collation: Collation,
2785 /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2786 /// explicit `COLLATE <name>` clause rather than the default. Under the
2787 /// MySQL dialect a text column with NO explicit clause takes the
2788 /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2789 /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2790 /// flag is the only thing that tells them apart.
2791 pub collation_explicit: bool,
2792 /// v7.39 (round 676) — the collation name AS WRITTEN, because
2793 /// `collation` above cannot carry it: `Collation` is a two-variant
2794 /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2795 /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2796 /// tell them apart.
2797 pub collation_name: Option<String>,
2798 /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2799 /// 4.4 SPG accepted and discarded the keyword, leaving
2800 /// negative values silently accepted on a column the
2801 /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2802 /// rejects negative INSERT / UPDATE values on UNSIGNED int
2803 /// columns. SPG widening to `u64`-shaped storage is out of
2804 /// v7.17 scope; the upper bound remains the signed-type max
2805 /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2806 /// exceeds what every mailrs / Rails app actually uses.
2807 pub is_unsigned: bool,
2808 /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2809 /// value list captured at parse time. When `Some`, the parser
2810 /// recognised `ENUM(...)` in the type slot; the engine
2811 /// validates INSERT cells against this list at
2812 /// column_def_to_schema time and persists the variants on
2813 /// `ColumnSchema.inline_enum_variants`. None for all
2814 /// non-ENUM columns.
2815 pub inline_enum_variants: Option<Vec<String>>,
2816 /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2817 /// value list. Distinct from ENUM (subset semantics rather
2818 /// than pick-one). None for all non-SET columns.
2819 pub inline_set_variants: Option<Vec<String>>,
2820 /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2821 /// STORED` computed-column source. When `Some`, the engine
2822 /// stores the Display-form of the parsed expression on
2823 /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2824 /// and re-evaluates the expression against every INSERT /
2825 /// UPDATE candidate row, overwriting whatever the caller
2826 /// supplied for this column. Boxed to keep `ColumnDef` from
2827 /// blowing past the `large_enum_variant` clippy ceiling
2828 /// (`Expr` widens with vector literals).
2829 pub generated_stored_expr: Option<Box<Expr>>,
2830 /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2831 /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2832 /// `auto_increment`; this additionally marks the ALWAYS one, whose
2833 /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2834 /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2835 /// VALUE`. Only meaningful when the column is also an identity column.
2836 pub identity_always: bool,
2837 /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2838 /// integer width (TINYINT / MEDIUMINT), captured before the type
2839 /// collapses to SmallInt / Int. The engine copies it to
2840 /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2841 /// path can enforce the real range. None for every other column and
2842 /// under the PG dialect.
2843 pub mysql_int_width: Option<MysqlIntWidth>,
2844 /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2845 /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2846 /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2847 /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2848 /// CREATE TABLE time so the write path can truncate and the render path
2849 /// can pad. None under the PG dialect, where temporal columns keep full
2850 /// microseconds.
2851 pub mysql_fsp: Option<u8>,
2852 /// v7.39.2 — the column was written `TIMESTAMP` rather than
2853 /// `DATETIME` in a MySQL session. The engine copies it to
2854 /// `ColumnSchema.mysql_declared_timestamp` at CREATE TABLE.
2855 pub mysql_declared_timestamp: bool,
2856 /// v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair, copied to
2857 /// `ColumnSchema.mysql_float_md` at CREATE TABLE.
2858 pub mysql_float_md: Option<(u8, u8)>,
2859}
2860
2861/// v7.17.0 Phase 2.5 — text collation classification surfaced
2862/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2863/// engine bridges between the two at CREATE TABLE time.
2864///
2865/// Recognised collation-name patterns (case-insensitive):
2866/// * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase` → CaseInsensitive
2867/// * Everything else (`C`, `POSIX`, `default`,
2868/// `pg_catalog.default`, `*_cs`, `*_bin`, unknown names) → Binary
2869#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2870pub enum Collation {
2871 Binary,
2872 CaseInsensitive,
2873}
2874
2875/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2876/// integer width for a column whose `ColumnTypeName` is too wide to carry
2877/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2878/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2879/// TABLE time. Only recorded under the MySQL dialect.
2880#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2881pub enum MysqlIntWidth {
2882 Tiny,
2883 Small,
2884 Medium,
2885 Int,
2886 /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2887 Big,
2888}
2889
2890#[allow(clippy::derivable_impls)]
2891impl Default for Collation {
2892 fn default() -> Self {
2893 Self::Binary
2894 }
2895}
2896
2897impl Collation {
2898 /// Classify a `COLLATE <name>` ident into one of the supported
2899 /// variants. Empty / unknown names fall back to `Binary` —
2900 /// matches the pre-2.5 silent-accept behaviour for snapshots
2901 /// that load through but don't actually depend on the
2902 /// collation semantics.
2903 #[must_use]
2904 pub fn from_collation_name(name: &str) -> Self {
2905 let lc = name.trim().to_ascii_lowercase();
2906 // Strip any quotes / schema-qualifier the parser left on
2907 // (e.g. `pg_catalog.default`).
2908 let bare = lc
2909 .trim_matches(|c: char| c == '"' || c == '\'')
2910 .rsplit('.')
2911 .next()
2912 .unwrap_or("");
2913 if bare.is_empty() {
2914 return Self::Binary;
2915 }
2916 if bare == "case_insensitive" || bare == "nocase" {
2917 return Self::CaseInsensitive;
2918 }
2919 // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2920 // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2921 if bare.ends_with("_ci") {
2922 return Self::CaseInsensitive;
2923 }
2924 Self::Binary
2925 }
2926}
2927
2928/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2929/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2930/// parse into this shape — the column-level form has a single-entry
2931/// `columns` / `parent_columns`.
2932#[derive(Debug, Clone, PartialEq)]
2933pub struct ForeignKeyConstraint {
2934 /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2935 /// today but parses + stores it so a future ALTER TABLE DROP
2936 /// CONSTRAINT can target by name (v7.6.8).
2937 pub name: Option<String>,
2938 /// Local columns participating in the FK (≥ 1).
2939 pub columns: Vec<String>,
2940 /// Referenced parent table.
2941 pub parent_table: String,
2942 /// Referenced parent columns. Must have the same arity as
2943 /// `columns`; engine validates parent has a PK / UNIQUE index
2944 /// on exactly this column set (v7.6.1).
2945 pub parent_columns: Vec<String>,
2946 /// `ON DELETE` action. Defaults to `Restrict` if absent.
2947 pub on_delete: FkAction,
2948 /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2949 pub on_update: FkAction,
2950 /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2951 pub match_type: MatchType,
2952 /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2953 /// dropped on the floor, so a constraint declared DEFERRABLE was
2954 /// enforced immediately and a circular-FK migration could not load.
2955 pub deferrable: bool,
2956 /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2957 /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2958 pub initially_deferred: bool,
2959}
2960
2961/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2962/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2963/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2964#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2965pub enum MatchType {
2966 #[default]
2967 Simple,
2968 Full,
2969}
2970
2971/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2972#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2973pub enum FkAction {
2974 /// Reject the parent mutation if any child row references it.
2975 /// SQL spec default; SPG default when no clause is given.
2976 Restrict,
2977 /// Recursively propagate the parent's delete / update to the
2978 /// child rows. Same TX.
2979 Cascade,
2980 /// Set the child FK column(s) to NULL. Requires the FK columns
2981 /// to be NULL-able.
2982 SetNull,
2983 /// Set the child FK column(s) to their declared DEFAULT.
2984 /// Requires the child column(s) to have DEFAULT.
2985 SetDefault,
2986 /// SQL spec `NO ACTION` (deferred check). SPG treats this as
2987 /// `Restrict` because the single-writer model has no deferred
2988 /// constraint window; the keyword is accepted for compatibility.
2989 NoAction,
2990}
2991
2992/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
2993/// optional `USING <encoding>` clause; omitting it keeps the
2994/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
2995/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
2996/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
2997/// binary16 (2× compression, ~3 decimal digits of precision).
2998#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2999pub enum VecEncoding {
3000 /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
3001 /// uncompressed `vector` type wire / storage layout.
3002 #[default]
3003 F32,
3004 /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
3005 /// `spg_storage::quantize::Sq8Vector` for the math + recall
3006 /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
3007 /// dim ≥ 32).
3008 Sq8,
3009 /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
3010 /// per-element. DDL keyword `HALF` (pgvector convention).
3011 /// Bit-exact dequantise to f32 at the storage layer; no
3012 /// rerank pass needed for kNN search.
3013 F16,
3014}
3015
3016impl fmt::Display for VecEncoding {
3017 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3018 match self {
3019 Self::F32 => f.write_str("F32"),
3020 Self::Sq8 => f.write_str("SQ8"),
3021 // pgvector convention: DDL keyword is `HALF`, not `F16`.
3022 Self::F16 => f.write_str("HALF"),
3023 }
3024 }
3025}
3026
3027/// SQL-level type names. The mapping to the storage runtime's `DataType`
3028/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
3029#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3030pub enum ColumnTypeName {
3031 /// v7.39 (round 291) — PG's `name`, the identifier type its
3032 /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
3033 /// answered `type "name" does not exist` to.
3034 Name,
3035 /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
3036 /// 32-bit wrapping counter the row header carries; `xid8` is the
3037 /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
3038 /// SPG answered `type "xid" does not exist` to.
3039 Xid,
3040 Xid8,
3041 /// v7.39 (round 667) — `OID`. `XID` was already a column type here
3042 /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
3043 /// `type "oid" does not exist` while `t(x XID)` built fine.
3044 Oid,
3045 SmallInt,
3046 Int,
3047 BigInt,
3048 Float,
3049 /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
3050 /// IEEE. It used to map to [`Self::Float`] on the theory that a
3051 /// wider float is harmless, but the width is observable: a `real`
3052 /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
3053 /// answered false where PG answers true.
3054 Real,
3055 Text,
3056 /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
3057 Varchar(u32),
3058 /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
3059 Char(u32),
3060 Bool,
3061 /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
3062 /// `USING <encoding>` clause; omitting it surfaces as
3063 /// `encoding = VecEncoding::F32` (the pre-v6 default).
3064 Vector {
3065 dim: u32,
3066 encoding: VecEncoding,
3067 },
3068 /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
3069 /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
3070 /// v7.39 (round 271) — scale widened to u16 alongside the value's.
3071 /// v7.39 (round 272) — precision too: PG's runs to 1000.
3072 /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
3073 /// a negative one rounds to tens / hundreds. A VALUE's display scale
3074 /// stays unsigned.
3075 Numeric(u16, i16),
3076 /// `DATE` — calendar day, no time-of-day component.
3077 Date,
3078 /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
3079 /// precision.
3080 Timestamp,
3081 /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
3082 /// stores all timestamps as UTC microseconds-since-epoch and
3083 /// does not carry per-row offset (PG's internal representation
3084 /// is the same — TZ is a display convention). The distinction
3085 /// from `TIMESTAMP` exists for the PG-wire layer to advertise
3086 /// OID 1184 so sqlx-style clients decode into
3087 /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
3088 Timestamptz,
3089 /// v4.9 `JSON` — text-backed JSON document. No parse-time
3090 /// validation; the engine round-trips the literal verbatim.
3091 /// PG OID 114 on the wire.
3092 Json,
3093 /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
3094 /// PG OID 3802 on the wire so sqlx-style binary-typed clients
3095 /// decode without a custom type registration.
3096 Jsonb,
3097 /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
3098 /// Literal forms (decoded by the engine at coercion time):
3099 /// - PG hex form: `'\xDEADBEEF'`
3100 /// - Escape form: `'foo\\000bar'` (backslash octal triples)
3101 Bytes,
3102 /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
3103 /// OID 1009. Literal forms accepted by the parser:
3104 /// - `ARRAY['a', 'b', NULL]`
3105 /// - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
3106 /// form at coerce time)
3107 TextArray,
3108 /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
3109 /// 1007. Same literal forms as TEXT[] (substituting integer
3110 /// elements).
3111 IntArray,
3112 /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
3113 /// OID 1016.
3114 BigIntArray,
3115 /// v7.40.0 `OID[]` — single-dimension oid array. PG wire OID
3116 /// 1028.
3117 ///
3118 /// `DataType::OidArray` and its value, codec tag, wire encoding
3119 /// and every naming surface have existed since v7.39 (round 694);
3120 /// what was missing was only the DDL spelling, so
3121 /// `CREATE TABLE t (c oid[])` answered `Oid[] not yet supported`
3122 /// while PostgreSQL 18.6 accepts it. Capability present, routing
3123 /// absent — the same shape as this repository's other hand-kept
3124 /// lists.
3125 OidArray,
3126 /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
3127 /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
3128 /// external form). G-CRIT-3.
3129 TsVector,
3130 /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
3131 /// wire OID 3615.
3132 TsQuery,
3133 /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
3134 /// Literal input accepts canonical hyphenated, unhyphenated,
3135 /// uppercase, and `{...}`-braced forms; display normalises to
3136 /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
3137 /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
3138 /// gen_random_uuid()`.
3139 Uuid,
3140 /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
3141 /// microseconds since 00:00:00. PG wire OID 1083. Literal
3142 /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
3143 /// (6-digit microsecond precision). Display normalises to
3144 /// the canonical `HH:MM:SS[.ffffff]`.
3145 Time,
3146 /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
3147 /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
3148 /// PG OID; advertised as INT4 on the wire. Display always
3149 /// 4 digits zero-padded.
3150 Year,
3151 /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
3152 /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
3153 /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
3154 /// Offset range: ±14 hours.
3155 TimeTz,
3156 /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
3157 /// (locale-independent storage). Wire OID 790. Literal input
3158 /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
3159 /// major units), optional leading `-`. Display: en_US locale.
3160 Money,
3161 /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
3162 /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
3163 /// — the engine bridges to `DataType::Range(RangeKind)`.
3164 Range(RangeKindAst),
3165 /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
3166 /// `text => text` map with NULL value support.
3167 Hstore,
3168 /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
3169 IntArray2D,
3170 BigIntArray2D,
3171 TextArray2D,
3172 /// v7.39 (read01 round 75) — `bool[][]`.
3173 BoolArray2D,
3174 /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
3175 /// three-field {months, days, micros} struct (PG-byte-equal),
3176 /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
3177 /// β-P2 `INTERVAL` was runtime-only — literal in expression
3178 /// position but rejected at CREATE TABLE.
3179 Interval,
3180 /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
3181 /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
3182 /// PG external form quotes each non-NULL element because
3183 /// interval text contains spaces / colons
3184 /// (`{"1 day","24:00:00",NULL}`).
3185 IntervalArray,
3186 /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3187 /// mirrors a scalar `ColumnTypeName` that already existed.
3188 BoolArray,
3189 SmallIntArray,
3190 FloatArray,
3191 NumericArray,
3192 DateArray,
3193 TimestampArray,
3194 TimestamptzArray,
3195 UuidArray,
3196 JsonArray,
3197 JsonbArray,
3198 BytesArray,
3199 VarcharArray,
3200 CharArray,
3201 /// v7.40.0 — five array spellings PG 18.6 accepts at
3202 /// `CREATE TABLE` and SPG refused at the type name. The
3203 /// element types were all present; only the `[]` step was.
3204 RealArray,
3205 TimeArray,
3206 TimeTzArray,
3207 InetArray,
3208 XmlArray,
3209 /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3210 /// as `Range(RangeKindAst)` — one column type variant covers
3211 /// all six builtin multiranges, kind pins the element type.
3212 /// Wire OIDs in pgwire.
3213 Multirange(RangeKindAst),
3214 /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3215 /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3216 /// Wire OIDs in pgwire.
3217 Point,
3218 Lseg,
3219 Path,
3220 PgBox,
3221 Polygon,
3222 Line,
3223 Circle,
3224 /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3225 Inet,
3226 Cidr,
3227 Macaddr,
3228 Macaddr8,
3229 /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3230 Bit(u32),
3231 /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3232 BitVarying(u32),
3233 Xml,
3234 Char1,
3235 MoneyArray,
3236}
3237
3238/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3239/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3240/// crate doesn't depend on storage. Bridged at engine boundary.
3241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3242pub enum RangeKindAst {
3243 Int4,
3244 Int8,
3245 Num,
3246 Ts,
3247 TsTz,
3248 Date,
3249}
3250
3251impl fmt::Display for ColumnTypeName {
3252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3253 match self {
3254 Self::SmallInt => f.write_str("SMALLINT"),
3255 Self::Int => f.write_str("INT"),
3256 Self::BigInt => f.write_str("BIGINT"),
3257 Self::Float => f.write_str("FLOAT"),
3258 Self::Real => f.write_str("REAL"),
3259 Self::Text => f.write_str("TEXT"),
3260 Self::Name => f.write_str("name"),
3261 Self::Xid => f.write_str("xid"),
3262 Self::Xid8 => f.write_str("xid8"),
3263 Self::Oid => f.write_str("oid"),
3264 Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3265 Self::Char(n) => write!(f, "CHAR({n})"),
3266 Self::Bool => f.write_str("BOOL"),
3267 Self::Vector { dim, encoding } => match encoding {
3268 VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3269 VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3270 VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3271 },
3272 Self::Json => f.write_str("JSON"),
3273 Self::Jsonb => f.write_str("JSONB"),
3274 Self::Bytes => f.write_str("BYTEA"),
3275 Self::TextArray => f.write_str("TEXT[]"),
3276 Self::IntArray => f.write_str("INT[]"),
3277 Self::BigIntArray => f.write_str("BIGINT[]"),
3278 Self::OidArray => f.write_str("oid[]"),
3279 Self::TsVector => f.write_str("TSVECTOR"),
3280 Self::TsQuery => f.write_str("TSQUERY"),
3281 Self::Uuid => f.write_str("UUID"),
3282 Self::Numeric(p, s) => {
3283 if *s == 0 {
3284 write!(f, "NUMERIC({p})")
3285 } else {
3286 write!(f, "NUMERIC({p}, {s})")
3287 }
3288 }
3289 Self::Date => f.write_str("DATE"),
3290 Self::Timestamp => f.write_str("TIMESTAMP"),
3291 Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3292 Self::Time => f.write_str("TIME"),
3293 Self::Year => f.write_str("YEAR"),
3294 Self::TimeTz => f.write_str("TIMETZ"),
3295 Self::Money => f.write_str("MONEY"),
3296 Self::Range(k) => f.write_str(match k {
3297 RangeKindAst::Int4 => "INT4RANGE",
3298 RangeKindAst::Int8 => "INT8RANGE",
3299 RangeKindAst::Num => "NUMRANGE",
3300 RangeKindAst::Ts => "TSRANGE",
3301 RangeKindAst::TsTz => "TSTZRANGE",
3302 RangeKindAst::Date => "DATERANGE",
3303 }),
3304 Self::Hstore => f.write_str("HSTORE"),
3305 Self::Interval => f.write_str("INTERVAL"),
3306 Self::IntervalArray => f.write_str("INTERVAL[]"),
3307 Self::BoolArray => f.write_str("BOOL[]"),
3308 Self::SmallIntArray => f.write_str("SMALLINT[]"),
3309 Self::FloatArray => f.write_str("FLOAT[]"),
3310 Self::NumericArray => f.write_str("NUMERIC[]"),
3311 Self::DateArray => f.write_str("DATE[]"),
3312 Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3313 Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3314 Self::UuidArray => f.write_str("UUID[]"),
3315 Self::JsonArray => f.write_str("JSON[]"),
3316 Self::JsonbArray => f.write_str("JSONB[]"),
3317 Self::BytesArray => f.write_str("BYTEA[]"),
3318 Self::VarcharArray => f.write_str("VARCHAR[]"),
3319 Self::CharArray => f.write_str("CHAR[]"),
3320 Self::RealArray => f.write_str("REAL[]"),
3321 Self::TimeArray => f.write_str("TIME[]"),
3322 Self::TimeTzArray => f.write_str("TIMETZ[]"),
3323 Self::InetArray => f.write_str("INET[]"),
3324 Self::XmlArray => f.write_str("XML[]"),
3325 Self::Multirange(k) => f.write_str(match k {
3326 RangeKindAst::Int4 => "INT4MULTIRANGE",
3327 RangeKindAst::Int8 => "INT8MULTIRANGE",
3328 RangeKindAst::Num => "NUMMULTIRANGE",
3329 RangeKindAst::Ts => "TSMULTIRANGE",
3330 RangeKindAst::TsTz => "TSTZMULTIRANGE",
3331 RangeKindAst::Date => "DATEMULTIRANGE",
3332 }),
3333 Self::Point => f.write_str("POINT"),
3334 Self::Lseg => f.write_str("LSEG"),
3335 Self::Path => f.write_str("PATH"),
3336 Self::PgBox => f.write_str("BOX"),
3337 Self::Polygon => f.write_str("POLYGON"),
3338 Self::Line => f.write_str("LINE"),
3339 Self::Circle => f.write_str("CIRCLE"),
3340 Self::Inet => f.write_str("INET"),
3341 Self::Cidr => f.write_str("CIDR"),
3342 Self::Macaddr => f.write_str("MACADDR"),
3343 Self::Macaddr8 => f.write_str("MACADDR8"),
3344 Self::Bit(0) => f.write_str("BIT"),
3345 Self::Bit(n) => write!(f, "BIT({n})"),
3346 Self::BitVarying(0) => f.write_str("VARBIT"),
3347 Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3348 Self::Xml => f.write_str("XML"),
3349 Self::Char1 => f.write_str("\"char\""),
3350 Self::MoneyArray => f.write_str("MONEY[]"),
3351 Self::IntArray2D => f.write_str("INT[][]"),
3352 Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3353 Self::TextArray2D => f.write_str("TEXT[][]"),
3354 Self::BoolArray2D => f.write_str("BOOL[][]"),
3355 }
3356 }
3357}
3358
3359/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3360/// engine evaluates `expr` per matched row in the table's row order
3361/// and rewrites cells in place. Indexed columns are dropped + re-
3362/// inserted into the affected B-tree on each row change.
3363/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3364/// tail on a DML statement. Boxed off the statement struct so the PG-only
3365/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3366/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3367/// the identical meaning, so both share this one payload rather than each
3368/// growing its own.
3369#[derive(Debug, Clone, PartialEq)]
3370pub struct DmlOrderLimit {
3371 pub order_by: Vec<OrderBy>,
3372 pub limit: Option<u32>,
3373}
3374
3375/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3376/// FROM, kept so the engine can finish the job.
3377///
3378/// The parser rewrites the statement onto correlated subqueries, and it
3379/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3380/// name belongs to the target or to a source needs their column lists,
3381/// which parse time does not have. Carrying the clause lets the engine
3382/// — which has the catalog — resolve the rest.
3383#[derive(Debug, Clone, PartialEq)]
3384pub struct UpdateFromSources {
3385 pub from: FromClause,
3386 pub sub_where: Option<Expr>,
3387}
3388
3389#[derive(Debug, Clone, PartialEq)]
3390pub struct UpdateStatement {
3391 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3392 /// level UPDATE. Empty for a plain UPDATE.
3393 pub ctes: Vec<Cte>,
3394 pub table: String,
3395 /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3396 /// to `t`'s own rows and not to anything that descends from it.
3397 ///
3398 /// Round 644 taught the FROM clause the keyword and left DML behind
3399 /// because it needed a field here, and this struct carries a warning
3400 /// that round 413 measured widening it in place overflowing the
3401 /// parser's nesting stack. That warning was about `from_sources`, a
3402 /// struct wide enough to need boxing; a `bool` lands in the padding
3403 /// already present — same as `CreateTableStatement::temporary`.
3404 ///
3405 /// It also earns its keep beyond the spelling: the inheritance
3406 /// fan-out needs a way to say "the parent's own rows" as a
3407 /// statement, or running one on the parent recurses forever.
3408 pub only: bool,
3409 /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3410 /// statement's expressions refer to the target row by. PG allows the
3411 /// bare spelling here (unlike INSERT, which requires AS).
3412 pub alias: Option<String>,
3413 pub assignments: Vec<(String, Expr)>,
3414 /// v7.39 (round 533) — boxed: round 413 measured that widening this
3415 /// struct in place overflows the parser's nesting stack.
3416 pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3417 pub where_: Option<Expr>,
3418 /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3419 /// mutate the first `limit` rows in the given order. PG has no such
3420 /// clause; the parser accepts it only under the MySQL dialect. Boxed
3421 /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3422 /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3423 /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3424 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3425 /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3426 /// clause (legacy CommandComplete path). Some = engine
3427 /// evaluates the projection over each mutated row and
3428 /// streams the result as a Rows QueryResult.
3429 pub returning: Option<Vec<SelectItem>>,
3430}
3431
3432/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3433/// from the active catalog and prunes them from every index.
3434#[derive(Debug, Clone, PartialEq)]
3435pub struct DeleteStatement {
3436 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3437 /// level DELETE. Empty for a plain DELETE.
3438 pub ctes: Vec<Cte>,
3439 pub table: String,
3440 /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3441 /// to `t`'s own rows and not to anything that descends from it.
3442 ///
3443 /// Round 644 taught the FROM clause the keyword and left DML behind
3444 /// because it needed a field here, and this struct carries a warning
3445 /// that round 413 measured widening it in place overflowing the
3446 /// parser's nesting stack. That warning was about `from_sources`, a
3447 /// struct wide enough to need boxing; a `bool` lands in the padding
3448 /// already present — same as `CreateTableStatement::temporary`.
3449 ///
3450 /// It also earns its keep beyond the spelling: the inheritance
3451 /// fan-out needs a way to say "the parent's own rows" as a
3452 /// statement, or running one on the parent recurses forever.
3453 pub only: bool,
3454 /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3455 /// the WHERE / RETURNING expressions refer to the target row by.
3456 pub alias: Option<String>,
3457 pub where_: Option<Expr>,
3458 /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3459 /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3460 /// form (round 413), so it shares that payload — and it is boxed for
3461 /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3462 /// statement tipped the parser's 512 KiB nesting stack.
3463 pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3464 /// v7.9.4 — `RETURNING <projection>`.
3465 pub returning: Option<Vec<SelectItem>>,
3466}
3467
3468/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3469/// One WHEN clause fires per source row depending on whether the
3470/// `on` condition matched any target row(s); the executor walks
3471/// `clauses` in declaration order and fires the first whose
3472/// `matched` kind and optional `condition` are both satisfied.
3473#[derive(Debug, Clone, PartialEq)]
3474pub struct MergeStatement {
3475 /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3476 /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3477 /// in PG). Each CTE materialises before the merge runs and its alias
3478 /// resolves as a source relation.
3479 pub ctes: Vec<Cte>,
3480 pub target: String,
3481 pub target_alias: Option<String>,
3482 pub source: String,
3483 pub source_alias: Option<String>,
3484 /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3485 /// the engine materialises this SELECT for the source rows and `source`
3486 /// is empty; the alias (required by PG for a subquery source) is in
3487 /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3488 pub source_select: Option<Box<SelectStatement>>,
3489 /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3490 /// positional column-alias list after the source alias. Empty when
3491 /// the statement carries none; the engine renames the materialised
3492 /// source columns positionally (PG's rule).
3493 pub source_column_aliases: Vec<String>,
3494 pub on: Expr,
3495 pub clauses: Vec<MergeWhenClause>,
3496 /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3497 /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3498 /// target/source aliases. `None` = no RETURNING (the common form).
3499 pub returning: Option<Vec<SelectItem>>,
3500}
3501
3502#[derive(Debug, Clone, PartialEq)]
3503pub struct MergeWhenClause {
3504 pub matched: MergeMatched,
3505 /// Optional `AND <expr>` filter — when present, the clause
3506 /// only fires for the source rows whose match-pair satisfies
3507 /// the predicate.
3508 pub condition: Option<Expr>,
3509 pub action: MergeAction,
3510}
3511
3512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3513pub enum MergeMatched {
3514 Matched,
3515 /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3516 /// target row (the classic insert branch).
3517 NotMatched,
3518 /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3519 /// row no source row matches. Actions are UPDATE / DELETE / DO
3520 /// NOTHING only (INSERT is a syntax error, as in PG).
3521 NotMatchedBySource,
3522}
3523
3524#[derive(Debug, Clone, PartialEq)]
3525pub enum MergeAction {
3526 /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3527 /// explicit column list (the bare `INSERT VALUES (vals)`
3528 /// shape lands later).
3529 Insert {
3530 columns: Vec<String>,
3531 values: Vec<Expr>,
3532 },
3533 /// `UPDATE SET col = expr [, …]` — applied to every matched
3534 /// target row for the firing source row.
3535 Update { assignments: Vec<(String, Expr)> },
3536 /// `DELETE` — drop every matched target row.
3537 Delete,
3538 /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3539 /// the clause and SPG mirrors so a customer-side MERGE that
3540 /// uses it for branch-control doesn't error).
3541 DoNothing,
3542}
3543
3544#[derive(Debug, Clone, PartialEq)]
3545pub struct InsertStatement {
3546 /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3547 /// level INSERT (writable CTE outer body). Empty for a plain
3548 /// INSERT. PG semantics: each CTE materialises before the
3549 /// outer INSERT runs, sharing the same transaction.
3550 pub ctes: Vec<Cte>,
3551 pub table: String,
3552 /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3553 /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3554 /// row by. PG requires the AS keyword in this position.
3555 pub alias: Option<String>,
3556 /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3557 /// `None`, every tuple is positional and must match the table arity.
3558 /// When `Some`, the engine maps each tuple slot to the named column and
3559 /// fills the rest with NULL (must be nullable).
3560 pub columns: Option<Vec<String>>,
3561 /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3562 /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3563 /// `select_source` is `Some` (the engine builds rows from the
3564 /// inner SELECT result set instead).
3565 pub rows: Vec<Vec<Expr>>,
3566 /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3567 /// round-5 G4). When present, `rows` is empty and the engine
3568 /// materialises the SELECT result, coerces each output tuple to
3569 /// the target column types, and inserts as a single batch.
3570 pub select_source: Option<Box<SelectStatement>>,
3571 /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3572 /// upsert clause. None = legacy INSERT (conflict raises a
3573 /// DuplicateKey error). mailrs migration blocker #2.
3574 pub on_conflict: Option<OnConflictClause>,
3575 /// v7.9.4 — `RETURNING <projection>`.
3576 pub returning: Option<Vec<SelectItem>>,
3577 /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3578 /// between the column list and VALUES. Governs how explicitly-supplied
3579 /// values interact with `GENERATED … AS IDENTITY` columns:
3580 /// * `None` — default. A `GENERATED ALWAYS` identity column rejects
3581 /// an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3582 /// * `System` — override the ALWAYS restriction: the explicit value
3583 /// is used verbatim, as for a `BY DEFAULT` column.
3584 /// * `User` — ignore any explicit value on a `BY DEFAULT` identity
3585 /// column and generate from the sequence instead (no effect on
3586 /// non-identity columns).
3587 pub overriding: Overriding,
3588 /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3589 /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3590 /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3591 /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3592 /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3593 /// into a NOT NULL column becomes the type's default), and the engine
3594 /// cannot recover that intent from the conflict clause alone. A plain
3595 /// `bool` lands in this struct's existing padding, so the AST does not
3596 /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3597 pub mysql_ignore: bool,
3598}
3599
3600/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3601#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3602pub enum Overriding {
3603 /// No `OVERRIDING` clause.
3604 #[default]
3605 None,
3606 /// `OVERRIDING SYSTEM VALUE`.
3607 System,
3608 /// `OVERRIDING USER VALUE`.
3609 User,
3610}
3611
3612/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3613#[derive(Debug, Clone, PartialEq)]
3614pub struct OnConflictClause {
3615 /// Local columns that identify the conflict (must match a
3616 /// UNIQUE / PRIMARY KEY index on the target table). Empty
3617 /// list means the user wrote `ON CONFLICT DO …` without a
3618 /// target — the engine arbitrates on every unique constraint
3619 /// (round 240).
3620 pub target_columns: Vec<String>,
3621 /// v7.39 (round 240) — the index predicate after the target list
3622 /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3623 /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3624 /// which satisfy any predicate, so it is parsed and carried but not
3625 /// consulted (recorded residual: partial-unique-index arbiters).
3626 pub index_where: Option<Expr>,
3627 /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3628 /// <name>`: the pg_dump conflict-target form. The engine
3629 /// resolves the name to the constraint's columns.
3630 pub constraint_name: Option<String>,
3631 /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3632 /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3633 /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3634 /// `ON CONFLICT DO UPDATE` is refused (42601).
3635 pub mysql_lowered: bool,
3636 /// The action on conflict.
3637 pub action: OnConflictAction,
3638}
3639
3640/// v7.9.7 — action on conflict.
3641#[derive(Debug, Clone, PartialEq)]
3642pub enum OnConflictAction {
3643 /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3644 /// silently skips conflicting ones.
3645 Nothing,
3646 /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3647 /// may reference `EXCLUDED.col` to read the incoming row's
3648 /// value (engine wires `EXCLUDED` as a virtual table).
3649 Update {
3650 assignments: Vec<(String, Expr)>,
3651 where_: Option<Expr>,
3652 },
3653}
3654
3655/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3656///
3657/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3658/// policies are spelled again here and mapped at the engine boundary.
3659/// v7.39 — the modes `BEGIN` / `START TRANSACTION` / `SET TRANSACTION`
3660/// / `SET SESSION CHARACTERISTICS` accept. `read_only` used to be parsed
3661/// and dropped on the floor, so `BEGIN READ ONLY` opened an ordinary
3662/// read-write transaction and every write in it was accepted.
3663///
3664/// `None` on either field means the statement did not name that mode, so
3665/// the session default applies — which is not the same as naming the
3666/// default explicitly.
3667#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3668pub struct TransactionModes {
3669 pub isolation: Option<IsolationLevel>,
3670 /// `Some(true)` = READ ONLY, `Some(false)` = READ WRITE.
3671 pub read_only: Option<bool>,
3672}
3673
3674/// v7.39 — what a read-only transaction refuses, and what PG calls it.
3675///
3676/// SPG did not enforce read-only transactions at all: `BEGIN READ ONLY;
3677/// INSERT …` answered `INSERT 0 1` and committed, and
3678/// `default_transaction_read_only = on` changed nothing. Both GUCs were
3679/// in the inventory, so a session could set one, read it back, and be
3680/// told it held a guarantee nothing was enforcing. Applications open
3681/// read-only transactions as a SAFETY measure — a reporting connection,
3682/// a read-only leg in a pool, a "this path must not write" discipline —
3683/// so accepting the writes is the worst possible answer.
3684///
3685/// `Some(tag)` means refuse with PG's message, `cannot execute {tag} in
3686/// a read-only transaction` (SQLSTATE 25006). Every tag below was read
3687/// back from PostgreSQL 18.6 by running the statement inside
3688/// `BEGIN READ ONLY`, one statement per transaction so no error could be
3689/// attributed to the wrong line.
3690///
3691/// Several answers were not what one would guess, which is why they were
3692/// measured rather than reasoned:
3693///
3694/// * `CREATE TEMP TABLE` is REFUSED, tagged `CREATE TABLE`.
3695/// * `NOTIFY`, `LISTEN` and `REINDEX` are ALLOWED.
3696/// * `PREPARE` of an INSERT is ALLOWED — only the EXECUTE writes.
3697/// * `UPDATE … WHERE false`, which changes nothing, is still REFUSED:
3698/// the verb decides, not the row count.
3699/// * `GRANT`, `COMMENT ON` and `SELECT … FOR SHARE` are all REFUSED.
3700///
3701/// The match is exhaustive on purpose. A new statement cannot be added
3702/// without deciding here whether it writes, which is the failure this
3703/// repository keeps meeting: one member of a family gets handled and its
3704/// siblings quietly do not.
3705impl Statement {
3706 #[must_use]
3707 pub fn read_only_violation_tag(&self) -> Option<&'static str> {
3708 match self {
3709 // v7.39.9 — MySQL's RENAME TABLE is DDL, refused read-only
3710 // for the same reason ALTER TABLE … RENAME TO is.
3711 Self::RenameTables(_) => Some("RENAME TABLE"),
3712 // ---- writes rows -------------------------------------------
3713 Self::Insert { .. } => Some("INSERT"),
3714 Self::Update { .. } => Some("UPDATE"),
3715 Self::Delete { .. } => Some("DELETE"),
3716 Self::Merge { .. } => Some("MERGE"),
3717 Self::Truncate { .. } => Some("TRUNCATE TABLE"),
3718 Self::CopyFromFile { .. } => Some("COPY FROM"),
3719
3720 // A SELECT that takes row locks writes lock state, and PG
3721 // names the strength it was asked for.
3722 Self::Select(sel) => sel.locking.as_ref().map(|l| match l.strength {
3723 LockStrength::Update => "SELECT FOR UPDATE",
3724 LockStrength::NoKeyUpdate => "SELECT FOR NO KEY UPDATE",
3725 LockStrength::Share => "SELECT FOR SHARE",
3726 LockStrength::KeyShare => "SELECT FOR KEY SHARE",
3727 }),
3728
3729 // ---- changes the catalog -----------------------------------
3730 Self::CreateTable { .. } => Some("CREATE TABLE"),
3731 Self::DropTable { .. } => Some("DROP TABLE"),
3732 Self::AlterTable { .. } => Some("ALTER TABLE"),
3733 Self::CreateIndex { .. } => Some("CREATE INDEX"),
3734 Self::DropIndex { .. } => Some("DROP INDEX"),
3735 Self::AlterIndex { .. } => Some("ALTER INDEX"),
3736 Self::CreateView { .. } => Some("CREATE VIEW"),
3737 Self::DropView { .. } => Some("DROP VIEW"),
3738 Self::CreateMaterializedView { .. } => Some("CREATE MATERIALIZED VIEW"),
3739 Self::RefreshMaterializedView { .. } => Some("REFRESH MATERIALIZED VIEW"),
3740 Self::DropMaterializedView { .. } => Some("DROP MATERIALIZED VIEW"),
3741 Self::CreateSequence { .. } => Some("CREATE SEQUENCE"),
3742 Self::AlterSequence { .. } => Some("ALTER SEQUENCE"),
3743 Self::DropSequence { .. } => Some("DROP SEQUENCE"),
3744 Self::CreateType { .. } => Some("CREATE TYPE"),
3745 Self::DropType { .. } => Some("DROP TYPE"),
3746 Self::AlterTypeAddValue { .. } | Self::AlterTypeRenameValue { .. } => {
3747 Some("ALTER TYPE")
3748 }
3749 Self::CreateDomain { .. } => Some("CREATE DOMAIN"),
3750 Self::AlterDomain { .. } => Some("ALTER DOMAIN"),
3751 Self::DropDomain { .. } => Some("DROP DOMAIN"),
3752 Self::CreateSchema { .. } => Some("CREATE SCHEMA"),
3753 Self::DropSchema { .. } => Some("DROP SCHEMA"),
3754 Self::CreateFunction { .. } => Some("CREATE FUNCTION"),
3755 Self::DropFunction { .. } => Some("DROP FUNCTION"),
3756 Self::CreateTrigger { .. } => Some("CREATE TRIGGER"),
3757 Self::DropTrigger { .. } => Some("DROP TRIGGER"),
3758 Self::CreateRule { .. } => Some("CREATE RULE"),
3759 Self::DropRule { .. } => Some("DROP RULE"),
3760 Self::CreateExtension { .. } => Some("CREATE EXTENSION"),
3761 Self::CreateStatistics { .. } => Some("CREATE STATISTICS"),
3762 Self::DropStatistics { .. } => Some("DROP STATISTICS"),
3763 Self::DropAggregate { .. } => Some("DROP AGGREGATE"),
3764 Self::CommentOn { .. } => Some("COMMENT"),
3765 Self::DropDatabase { .. } => Some("DROP DATABASE"),
3766 Self::CreatePublication { .. } => Some("CREATE PUBLICATION"),
3767 Self::DropPublication { .. } => Some("DROP PUBLICATION"),
3768 Self::CreateSubscription { .. } => Some("CREATE SUBSCRIPTION"),
3769 Self::DropSubscription { .. } => Some("DROP SUBSCRIPTION"),
3770
3771 // ---- changes roles / permissions ---------------------------
3772 Self::CreateUser { .. } => Some("CREATE ROLE"),
3773 Self::DropUser { .. } => Some("DROP ROLE"),
3774 Self::AlterRolePassword { .. } | Self::SetDbRoleSetting { .. } => Some("ALTER ROLE"),
3775 Self::Grant { .. } => Some("GRANT"),
3776 Self::Revoke { .. } => Some("REVOKE"),
3777 Self::CreatePolicy { .. } => Some("CREATE POLICY"),
3778 Self::AlterPolicy { .. } => Some("ALTER POLICY"),
3779 Self::DropPolicy { .. } => Some("DROP POLICY"),
3780 Self::AlterSystem { .. } => Some("ALTER SYSTEM"),
3781
3782 // ---- SPG's own writers -------------------------------------
3783 // Rewrites cold-tier segments on disk. PG has no equivalent to
3784 // ask, so the test is what it does, not what it is called.
3785 Self::CompactColdSegments => Some("COMPACT COLD SEGMENTS"),
3786
3787 // ---- allowed -----------------------------------------------
3788 // Reads, transaction control, session state, cursors, and the
3789 // maintenance statements PG itself permits. `REINDEX` really is
3790 // allowed in a read-only transaction (measured), which is why
3791 // `Maintain` is here.
3792 //
3793 // `Prepare`, `Execute`, `Call` and `DoBlock` are allowed at
3794 // this level for the reason PG allows them: the write inside
3795 // is refused when it runs, by this same check. Measured:
3796 // `PREPARE p AS INSERT …` succeeds; `DO $$ … INSERT … $$`
3797 // fails with `cannot execute INSERT`.
3798 Self::Explain { .. }
3799 | Self::CopyTo { .. }
3800 | Self::CopyToFile { .. }
3801 | Self::Analyze { .. }
3802 | Self::Maintain { .. }
3803 | Self::Vacuum { .. }
3804 | Self::Begin { .. }
3805 | Self::Commit
3806 | Self::Rollback
3807 | Self::Savepoint { .. }
3808 | Self::RollbackToSavepoint { .. }
3809 | Self::ReleaseSavepoint { .. }
3810 | Self::PrepareTransaction { .. }
3811 | Self::SetTransaction { .. }
3812 | Self::SetConstraints { .. }
3813 | Self::SetParameter { .. }
3814 | Self::SetParameterList { .. }
3815 | Self::SetUserVars { .. }
3816 | Self::SetRole { .. }
3817 | Self::ResetParameter { .. }
3818 | Self::ShowParameter { .. }
3819 | Self::Discard { .. }
3820 | Self::Prepare { .. }
3821 | Self::Execute { .. }
3822 | Self::Deallocate { .. }
3823 | Self::Call { .. }
3824 | Self::DoBlock { .. }
3825 | Self::DeclareCursor { .. }
3826 | Self::FetchCursor { .. }
3827 | Self::MoveCursor { .. }
3828 | Self::CloseCursor { .. }
3829 | Self::Listen { .. }
3830 | Self::Notify { .. }
3831 | Self::Unlisten { .. }
3832 | Self::Kill { .. }
3833 | Self::WaitForWalPosition { .. }
3834 | Self::ValidateOnly { .. }
3835 | Self::NoOpPreventedInTransaction { .. }
3836 | Self::Empty
3837 | Self::ShowTables
3838 | Self::ShowDatabases
3839 | Self::UseDatabase(_)
3840 | Self::ShowCreateTable { .. }
3841 | Self::ShowIndexes { .. }
3842 | Self::ShowStatus
3843 | Self::ShowVariables
3844 | Self::ShowVariablesLike { .. }
3845 | Self::ShowProcesslist
3846 | Self::ShowColumns { .. }
3847 | Self::ShowUsers
3848 | Self::ShowPublications
3849 | Self::ShowSubscriptions => None,
3850 }
3851 }
3852}
3853
3854#[derive(Debug, Clone, PartialEq, Eq)]
3855pub struct LockingClause {
3856 pub strength: LockStrength,
3857 /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3858 pub of_tables: Vec<String>,
3859 pub policy: LockWait,
3860}
3861
3862/// PG's four tuple-lock strengths, weakest first.
3863#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3864pub enum LockStrength {
3865 KeyShare,
3866 Share,
3867 NoKeyUpdate,
3868 Update,
3869}
3870
3871/// What to do when the row is already locked.
3872#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3873pub enum LockWait {
3874 /// Block until it is free — PG's default.
3875 #[default]
3876 Wait,
3877 /// `NOWAIT` — fail the statement with 55P03.
3878 NoWait,
3879 /// `SKIP LOCKED` — leave the row out of the result.
3880 SkipLocked,
3881}
3882
3883#[derive(Debug, Clone, PartialEq, Default)]
3884pub struct SelectStatement {
3885 /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3886 /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3887 /// whole syntax and locked nothing: two workers running the classic
3888 /// `SKIP LOCKED` queue take both took the same row.
3889 /// v7.39 (round 305) — boxed. A locking clause appears on a
3890 /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3891 /// cost every `SelectStatement` 32 bytes, and this struct sits in
3892 /// recursive evaluation frames where the engine already runs close to
3893 /// its stack budget (a 512 KB depth guard is the canary).
3894 pub locking: Option<alloc::boxed::Box<LockingClause>>,
3895 /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3896 /// expressions, materialised once at query start before the
3897 /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3898 /// only — no `WITH RECURSIVE` for v4.x.
3899 pub ctes: Vec<Cte>,
3900 pub distinct: bool,
3901 /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3902 /// keep the first row (per ORDER BY) of each group the
3903 /// expressions define. Empty = no DISTINCT ON.
3904 pub distinct_on: Vec<Expr>,
3905 pub items: Vec<SelectItem>,
3906 pub from: Option<FromClause>,
3907 pub where_: Option<Expr>,
3908 pub group_by: Option<Vec<Expr>>,
3909 /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3910 /// expands `group_by` to every non-aggregate SELECT-list item
3911 /// before the executor runs. Mutually exclusive with an
3912 /// explicit `group_by` list (the parser sets exactly one).
3913 pub group_by_all: bool,
3914 /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3915 /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3916 /// aggregate executor resolves them through the same synthetic
3917 /// schema used for the SELECT items.
3918 pub having: Option<Expr>,
3919 /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3920 /// itself a `SelectStatement` with `order_by = None` and `limit =
3921 /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3922 /// top of the chain).
3923 pub unions: Vec<(UnionKind, SelectStatement)>,
3924 /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3925 /// Keys are matched left-to-right: first key decides, ties break
3926 /// to the second, etc.
3927 pub order_by: Vec<OrderBy>,
3928 /// `LIMIT <n>` — bound on row output. `n` is an integer
3929 /// literal **or** (v7.9.24) a placeholder `$N` resolved
3930 /// against the prepared-statement Bind values. mailrs
3931 /// migration follow-up H2.
3932 pub limit: Option<LimitExpr>,
3933 /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3934 /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3935 pub offset: Option<LimitExpr>,
3936 /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3937 /// (SQL:2008). When true and an ORDER BY is present, the
3938 /// executor extends past the LIMIT-truncated tail to include
3939 /// every row whose ORDER BY key equals the last-kept row's
3940 /// key. Requires an ORDER BY; the executor errors otherwise
3941 /// (matching PG's `WITH TIES` rule). The parser was already
3942 /// accepting `WITH TIES` since Phase 5.1; this field captures
3943 /// the choice so the executor can act on it.
3944 pub limit_with_ties: bool,
3945 /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3946 /// that NOTHING referenced. PG analyses every definition whether
3947 /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3948 /// and silently succeeded here — the referenced ones get their columns
3949 /// resolved through the WindowFunction nodes they were inlined into,
3950 /// and the unreferenced ones used to be dropped at parse, unexamined.
3951 /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3952 ///
3953 /// Not part of `Display`: an unreferenced definition has no effect on
3954 /// the result, so a deparsed body (a stored view) omits it.
3955 pub window_check_exprs: Vec<Expr>,
3956}
3957
3958impl Expr {
3959 /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3960 /// directly inside this expression to `f`. `f` receives each nested
3961 /// statement once; descending further (into that statement's own
3962 /// clauses) is the caller's job, which keeps this walk finite and
3963 /// lets the caller order the recursion.
3964 ///
3965 /// The match is deliberately **wildcard-free**: a new `Expr` variant
3966 /// does not compile until it says whether it can carry a subquery.
3967 /// The row-count resolution pass is built on this, and a shape it
3968 /// silently failed to visit would leave a `LimitExpr::Expr` behind —
3969 /// which every row-count reader would take as "no limit", i.e. the
3970 /// whole table. Compile-time exhaustiveness is what rules that out.
3971 /// Iterative on purpose. Expression trees here get deep (long
3972 /// boolean chains, big IN lists), and this walk is on the path of
3973 /// every statement; recursing would add a frame per node to a stack
3974 /// budget the engine already runs close to — a depth guard that runs
3975 /// on a deliberately small stack caught exactly that. Depth costs
3976 /// heap here instead.
3977 pub fn for_each_subquery_mut<E>(
3978 &mut self,
3979 f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
3980 ) -> Result<(), E> {
3981 let mut stack: Vec<&mut Self> = alloc::vec![self];
3982 while let Some(e) = stack.pop() {
3983 match e {
3984 Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
3985 Self::NamedArg { expr, .. }
3986 | Self::Collate { expr, .. }
3987 | Self::Variadic(expr)
3988 | Self::Unary { expr, .. }
3989 | Self::Cast { expr, .. }
3990 | Self::FieldAccess { base: expr, .. }
3991 | Self::IsNull { expr, .. }
3992 | Self::BoolTest { expr, .. }
3993 | Self::Extract { source: expr, .. } => stack.push(expr),
3994 Self::Binary { lhs, rhs, .. } => {
3995 stack.push(lhs);
3996 stack.push(rhs);
3997 }
3998 Self::Like { expr, pattern, .. } => {
3999 stack.push(expr);
4000 stack.push(pattern);
4001 }
4002 Self::ArraySubscript { target, index } => {
4003 stack.push(target);
4004 stack.push(index);
4005 }
4006 Self::ArraySlice { target, lo, hi } => {
4007 stack.push(target);
4008 stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
4009 }
4010 Self::AnyAll { expr, array, .. } => {
4011 stack.push(expr);
4012 stack.push(array);
4013 }
4014 Self::FunctionCall { args, .. } | Self::Array(args) => {
4015 stack.extend(args.iter_mut());
4016 }
4017 Self::AggregateOrdered {
4018 call,
4019 order_by,
4020 filter,
4021 ..
4022 } => {
4023 stack.push(call);
4024 stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
4025 stack.extend(filter.iter_mut().map(|b| &mut **b));
4026 }
4027 Self::WindowFunction {
4028 args,
4029 partition_by,
4030 order_by,
4031 filter,
4032 ..
4033 } => {
4034 // `frame` bounds hold folded numbers / interval
4035 // parts, never expressions — nothing to visit there.
4036 stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
4037 stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
4038 stack.extend(filter.iter_mut().map(|b| &mut **b));
4039 }
4040 Self::InList { expr, list, .. } => {
4041 stack.push(expr);
4042 stack.extend(list.iter_mut());
4043 }
4044 Self::Case {
4045 operand,
4046 branches,
4047 else_branch,
4048 } => {
4049 stack.extend(
4050 operand
4051 .iter_mut()
4052 .chain(else_branch.iter_mut())
4053 .map(|b| &mut **b),
4054 );
4055 for (when, then) in branches.iter_mut() {
4056 stack.push(when);
4057 stack.push(then);
4058 }
4059 }
4060 Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
4061 Self::InSubquery { expr, subquery, .. } => {
4062 stack.push(expr);
4063 f(subquery)?;
4064 }
4065 Self::RowInSubquery { row, subquery, .. }
4066 | Self::RowCmpSubquery { row, subquery, .. } => {
4067 stack.extend(row.iter_mut());
4068 f(subquery)?;
4069 }
4070 }
4071 }
4072 Ok(())
4073 }
4074}
4075
4076/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
4077/// time or a placeholder `$N` resolved during extended-query
4078/// Bind. mailrs migration follow-up H2.
4079///
4080/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
4081/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
4082/// made the compiler point at every site that used to duplicate a
4083/// row-count out of the AST, which is exactly the set that must not
4084/// bypass the resolution pre-pass.
4085#[derive(Debug, Clone, PartialEq)]
4086pub enum LimitExpr {
4087 /// `LIMIT 10` — value known at parse time.
4088 Literal(u32),
4089 /// `LIMIT $N` — the 1-based parameter index, resolved against
4090 /// the bind values when the prepared statement executes.
4091 Placeholder(u16),
4092 /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
4093 /// greatest(2,3)`: a row-count expression that isn't constant, so
4094 /// it can't be folded at parse time. Evaluated once, before
4095 /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
4096 /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
4097 /// "no limit"). **No execution path may see this variant** —
4098 /// `as_literal` would report `None`, which every row-count reader
4099 /// takes to mean "unlimited", i.e. the whole table.
4100 Expr(alloc::boxed::Box<Expr>),
4101}
4102
4103impl fmt::Display for LimitExpr {
4104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4105 match self {
4106 Self::Literal(n) => write!(f, "{n}"),
4107 Self::Placeholder(n) => write!(f, "${n}"),
4108 // Parenthesised so the round-trip text re-parses as one
4109 // row-count expression (`LIMIT (SELECT 4)`), which is also
4110 // the only spelling `FETCH FIRST` accepts.
4111 Self::Expr(e) => write!(f, "({e})"),
4112 }
4113 }
4114}
4115
4116impl LimitExpr {
4117 /// Convenience for the simple-query path where no placeholders
4118 /// can possibly exist. Returns the literal value or `None` if
4119 /// this is a placeholder (caller must surface as Unsupported).
4120 ///
4121 /// v7.39 (round 305) — `None` is read by every row-count consumer as
4122 /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
4123 /// therefore silently return the whole table, so the engine's
4124 /// `resolve_limit_exprs` pre-pass rewrites the variant away before
4125 /// dispatch. The assertion makes a missed nesting site fail loudly
4126 /// in every test build rather than quietly widening a result set.
4127 #[must_use]
4128 pub fn as_literal(&self) -> Option<u32> {
4129 match self {
4130 Self::Literal(n) => Some(*n),
4131 Self::Placeholder(_) => None,
4132 Self::Expr(_) => {
4133 debug_assert!(
4134 false,
4135 "LimitExpr::Expr reached execution — resolve_limit_exprs \
4136 missed a nesting site; treating it as `no limit` would \
4137 return every row"
4138 );
4139 None
4140 }
4141 }
4142 }
4143}
4144
4145/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
4146/// the engine's `substitute_placeholders` pass these are
4147/// always Literal; in the simple-query path a Placeholder
4148/// shape returns None (executor surfaces as
4149/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
4150impl SelectStatement {
4151 #[must_use]
4152 pub fn limit_literal(&self) -> Option<u32> {
4153 self.limit.as_ref().and_then(LimitExpr::as_literal)
4154 }
4155 #[must_use]
4156 pub fn offset_literal(&self) -> Option<u32> {
4157 self.offset.as_ref().and_then(LimitExpr::as_literal)
4158 }
4159}
4160
4161#[derive(Debug, Clone, PartialEq)]
4162pub struct Cte {
4163 pub name: String,
4164 /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
4165 /// classical case) or a data-modifying statement
4166 /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
4167 /// CTE semantics. The modifying body's RETURNING projection
4168 /// becomes the materialised CTE table the outer query can
4169 /// reference; the modifying statement runs once before the
4170 /// outer query, within the same transaction.
4171 pub body: CteBody,
4172 /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
4173 /// RECURSIVE keyword. Applies to every CTE in the clause per
4174 /// PG semantics. A non-recursive body in a RECURSIVE WITH is
4175 /// allowed; the engine just runs it once.
4176 pub recursive: bool,
4177 /// v4.22: optional `WITH name(a, b, c)` column-name list. When
4178 /// non-empty, these override the body's output column names
4179 /// position-by-position; the engine errors out if the count
4180 /// doesn't match the body's projection width.
4181 pub column_overrides: Vec<String>,
4182 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
4183 /// SET seqcol` on a recursive CTE. Desugared at parse time into an
4184 /// extra ordering column on the body (see `rewrite_search_and_cycle`).
4185 pub search: Option<SearchClause>,
4186 /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
4187 /// USING pathcol` cycle detection, desugared at parse time.
4188 pub cycle: Option<CycleClause>,
4189}
4190
4191/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
4192#[derive(Debug, Clone, PartialEq)]
4193pub struct SearchClause {
4194 /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
4195 pub depth_first: bool,
4196 /// The CTE output columns the search orders by.
4197 pub by_columns: Vec<String>,
4198 /// The new column holding the ordering key (a row-array for depth,
4199 /// a `(depth, keys…)` row for breadth).
4200 pub set_column: String,
4201}
4202
4203/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
4204#[derive(Debug, Clone, PartialEq)]
4205pub struct CycleClause {
4206 /// Columns whose repetition along a path marks a cycle.
4207 pub columns: Vec<String>,
4208 /// The new boolean-ish column set to `mark_value` on a cycle.
4209 pub mark_column: String,
4210 /// Value written to `mark_column` when a cycle is detected (default
4211 /// `true`); `default_value` otherwise. PG allows any type; SPG carries
4212 /// them as literals.
4213 pub mark_value: Option<Literal>,
4214 pub default_value: Option<Literal>,
4215 /// The new column accumulating the visited-row path array.
4216 pub path_column: String,
4217}
4218
4219/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
4220/// (Insert / Update / Delete with optional RETURNING). The
4221/// data-modifying variants must carry a RETURNING projection for the
4222/// outer query to reference the CTE alias by; an empty RETURNING is
4223/// only valid if no outer reference materialises (rare — typically
4224/// caught at planning).
4225#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
4226#[derive(Debug, Clone, PartialEq)]
4227pub enum CteBody {
4228 Select(SelectStatement),
4229 Insert(Box<InsertStatement>),
4230 Update(Box<UpdateStatement>),
4231 Delete(Box<DeleteStatement>),
4232 /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
4233 /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
4234 Merge(Box<MergeStatement>),
4235}
4236
4237impl CteBody {
4238 /// Convenience accessor used by classical (read-only) CTE
4239 /// callsites that still expect a SELECT body. Returns None for
4240 /// data-modifying CTEs; callers must explicitly route those
4241 /// through `exec_with_ctes`'s modifying branch.
4242 #[must_use]
4243 pub fn as_select(&self) -> Option<&SelectStatement> {
4244 match self {
4245 Self::Select(s) => Some(s),
4246 _ => None,
4247 }
4248 }
4249
4250 #[must_use]
4251 pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
4252 match self {
4253 Self::Select(s) => Some(s),
4254 _ => None,
4255 }
4256 }
4257
4258 #[must_use]
4259 pub fn is_modifying(&self) -> bool {
4260 !matches!(self, Self::Select(_))
4261 }
4262}
4263
4264#[derive(Debug, Clone, PartialEq)]
4265pub struct OrderBy {
4266 pub expr: Expr,
4267 /// `false` = ASC (default), `true` = DESC.
4268 pub desc: bool,
4269 /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
4270 /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
4271 /// NULLS FIRST for DESC); the engine resolves the effective
4272 /// value via `nulls_first.unwrap_or(desc)`.
4273 pub nulls_first: Option<bool>,
4274 /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
4275 /// It lives here rather than in the expression for the same reason
4276 /// `desc` does: at an ORDER BY key a collation is ordering
4277 /// information, and nothing downstream of the sort needs it. A new
4278 /// `Expr` variant would instead put a new arm on `eval_expr`, which
4279 /// this repo has measured to overflow the debug stack.
4280 ///
4281 /// `None` means none was written, and the key falls back to whatever
4282 /// its COLUMN declares — which is every key that existed before this.
4283 pub collation: Option<String>,
4284}
4285
4286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4287pub enum UnionKind {
4288 /// `UNION` — dedupes the combined set.
4289 Distinct,
4290 /// `UNION ALL` — concatenates without dedup.
4291 All,
4292 /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
4293 /// present on both sides.
4294 Intersect,
4295 /// `INTERSECT ALL` — multiset intersection (min per-row count).
4296 IntersectAll,
4297 /// `EXCEPT` — distinct left rows absent from the right.
4298 Except,
4299 /// `EXCEPT ALL` — multiset subtraction.
4300 ExceptAll,
4301}
4302
4303#[derive(Debug, Clone, PartialEq)]
4304pub enum SelectItem {
4305 Wildcard,
4306 /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
4307 /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
4308 /// `NEW` pseudo-relation).
4309 QualifiedWildcard(String),
4310 Expr {
4311 expr: Expr,
4312 alias: Option<String>,
4313 },
4314}
4315
4316#[derive(Debug, Clone, PartialEq)]
4317pub struct TableRef {
4318 pub name: String,
4319 pub alias: Option<String>,
4320 /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
4321 /// children.
4322 ///
4323 /// The keyword used to be absorbed at parse time, on the reasoning
4324 /// that SPG's inheritance children are separate relations a plain
4325 /// scan does not descend into — so ONLY already described what the
4326 /// scan did. That stopped being true when a partition parent
4327 /// started unioning its children: measured, `SELECT count(*) FROM
4328 /// ONLY <partitioned parent>` answered 2 where PG answers 0.
4329 pub only: bool,
4330 /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
4331 /// When `Some(id)`, the scan restricts to rows that live in
4332 /// segment `<id>` only — useful for forensic inspection of a
4333 /// specific freezer-emitted segment without exposing the hot
4334 /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
4335 /// is STABILITY carve-out for v6.10 — needs the freezer to
4336 /// stamp each segment with a wall-clock at creation time.
4337 pub as_of_segment: Option<u32>,
4338 /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
4339 /// source. When `Some`, `name` is the alias (defaulting to
4340 /// `"unnest"` when no `AS` is given) and the engine builds a
4341 /// synthetic single-column table by evaluating the expression
4342 /// once at SELECT entry. Each TEXT[] element becomes one row;
4343 /// NULL elements become NULL cells. v7.11 supported
4344 /// uncorrelated UNNEST only as the FROM primary; v7.13.2
4345 /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
4346 /// position (cross-join with regular tables).
4347 pub unnest_expr: Option<Box<Expr>>,
4348 /// v7.13.2 — mailrs round-6 S5. PG-standard
4349 /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
4350 /// when non-empty, the first entry overrides the projected
4351 /// column name for the unnested column. Empty = fall back to
4352 /// the table alias (pre-v7.13.2 behaviour).
4353 pub unnest_column_aliases: Vec<String>,
4354 /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
4355 /// row-stream gains a trailing BIGINT column counting rows
4356 /// from 1 in element order. PG names it `ordinality`; a second
4357 /// entry in the column-alias list renames it.
4358 pub with_ordinality: bool,
4359 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4360 /// [, step])` set-returning source. When `Some`, the engine
4361 /// materialises a single-column virtual table by stepping
4362 /// `start` to `stop` inclusive. Args are the literal arg list
4363 /// (2 for default-step, 3 for explicit-step). Supports:
4364 /// * SmallInt / Int / BigInt with integer step (default = 1)
4365 /// * Timestamp with INTERVAL step (PG date-range pattern)
4366 /// Mutually exclusive with `unnest_expr` — both populate the
4367 /// same downstream dispatch slot. `name` defaults to
4368 /// `"generate_series"` when no alias is provided.
4369 pub generate_series_args: Option<Vec<Expr>>,
4370 /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
4371 /// table. When `Some`, the TableRef is a parenthesised SELECT
4372 /// that may reference columns from the preceding FROM items
4373 /// (correlated derived table). The executor materialises the
4374 /// subquery per left-row, substituting outer-column references
4375 /// against the current join row's values before running the
4376 /// inner SELECT, then cross-joins the result back.
4377 /// Mutually exclusive with `name` / `unnest_expr` /
4378 /// `generate_series_args`.
4379 pub lateral_subquery: Option<Box<SelectStatement>>,
4380 /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
4381 /// function as a FROM item. PG semantics: for each key/value
4382 /// pair in the JSONB object argument, emit one (key TEXT,
4383 /// value TEXT) row. When prefixed by `LATERAL` and joined via
4384 /// `CROSS JOIN LATERAL`, the argument may reference columns
4385 /// from a preceding FROM item, in which case the executor
4386 /// evaluates `<expr>` per outer row.
4387 /// Mutually exclusive with `unnest_expr` / `generate_series_args`
4388 /// / `lateral_subquery`. The optional `LATERAL` keyword does not
4389 /// require a separate flag — the executor evaluates per-row
4390 /// whenever the join sits in a JoinKind context.
4391 ///
4392 /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4393 /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4394 /// `json_each` / `json_each_text`) so the executor picks the
4395 /// value-column rendering (JSON text vs unwrapped text).
4396 pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4397 /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4398 /// function channel: `(lowercase fn name, args)`. Carries
4399 /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4400 /// dispatches by name.
4401 pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4402 /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4403 /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4404 /// reference to it yields the value, not a one-field composite
4405 /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4406 /// desugared shape is indistinguishable from a hand-written
4407 /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4408 /// only the parser knows which one it built, so it says so here.
4409 pub scalar_fn_item: bool,
4410 /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4411 /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4412 /// target-list SRFs follow — see round 67). The array-returning family keeps
4413 /// its own lowering; this channel carries the ones that have no array form
4414 /// (`generate_series`, a user `RETURNS SETOF` function).
4415 pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4416 /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4417 /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4418 /// tables (implicit LATERAL, like every SRF channel). Executed by
4419 /// walking the row path over the parsed doc, then each column's
4420 /// path per row-item; NESTED expands as a per-parent outer join.
4421 pub json_table: Option<Box<JsonTable>>,
4422}
4423
4424/// What a FROM item IS.
4425///
4426/// v7.40.10 — a boolean cannot make a consumer handle a new kind; an
4427/// enum can. Every consumer that must know the difference writes a
4428/// `match` with no wildcard arm, so a variant added here is a compile
4429/// error at each of them rather than a defect at whichever one the new
4430/// shape reaches first.
4431///
4432/// The engine asked "is this item synthesised?" in fifty-six places and
4433/// exactly one of them listed every field. The rest were missing
4434/// between one and five, and the gaps were reachable:
4435/// `SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)` answered
4436/// `relation "jsonb_each_text" does not exist` over the extended
4437/// protocol because one such list named four of seven.
4438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4439pub enum FromItemKind {
4440 /// A table, view or CTE named in the catalog.
4441 Relation,
4442 /// `unnest(...)` and the set-returning functions the parser lowers
4443 /// onto the same slot (`string_to_table`, `jsonb_object_keys`, …).
4444 Unnest,
4445 /// `generate_series(...)`.
4446 GenerateSeries,
4447 /// A derived table or LATERAL subquery.
4448 Subquery,
4449 /// `jsonb_each(...)` / `jsonb_each_text(...)`.
4450 JsonbEach,
4451 /// A table function call the parser kept as a name plus arguments.
4452 TableFn,
4453 /// `ROWS FROM (...)`.
4454 RowsFrom,
4455 /// `JSON_TABLE(...)`.
4456 JsonTable,
4457 /// A scalar function in FROM position, which yields one row.
4458 ScalarFn,
4459}
4460
4461/// One walkable slot of a FROM item: an expression, or a nested SELECT.
4462///
4463/// v7.40.10 — see [`TableRef::try_for_each_slot_mut`].
4464#[derive(Debug)]
4465pub enum FromSlot<'a> {
4466 Expr(&'a mut Expr),
4467 Select(&'a mut SelectStatement),
4468}
4469
4470impl TableRef {
4471 /// Whether this FROM item NAMES A RELATION — a table, view or CTE —
4472 /// rather than producing its own rows.
4473 ///
4474 /// **A total destructure, no `..`.** A field added to `TableRef` is a
4475 /// compile error here.
4476 ///
4477 /// v7.40.10, on evidence. This question is asked in 56 places across
4478 /// the engine and every one of them wrote its own list of fields.
4479 /// Exactly one was complete. The others were missing between one and
4480 /// five slots each, and each gap is a defect waiting for the shape
4481 /// that reaches it:
4482 ///
4483 /// ```text
4484 /// try_stream_single_table's guard named four of seven, so
4485 /// SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)
4486 /// ERROR: relation "jsonb_each_text" does not exist
4487 /// over the extended protocol, while count(*) over the same item
4488 /// answered and the simple query protocol answered.
4489 /// ```
4490 ///
4491 /// `scalar_fn_item` counts as not-a-relation for the same reason the
4492 /// rest do: the row comes from the item, not from the catalog.
4493 #[must_use]
4494 pub fn names_a_relation(&self) -> bool {
4495 self.kind() == FromItemKind::Relation
4496 }
4497
4498 /// What this FROM item is.
4499 ///
4500 /// **A total destructure, no `..`.** A field added to `TableRef` is
4501 /// a compile error here — which is the point, because every
4502 /// consumer matches exhaustively on the result.
4503 ///
4504 /// The slots are mutually exclusive by construction: the parser
4505 /// fills exactly one of them, or none for a plain relation.
4506 #[must_use]
4507 pub fn kind(&self) -> FromItemKind {
4508 let Self {
4509 name: _,
4510 alias: _,
4511 only: _,
4512 as_of_segment: _,
4513 unnest_column_aliases: _,
4514 with_ordinality: _,
4515 scalar_fn_item,
4516 unnest_expr,
4517 generate_series_args,
4518 lateral_subquery,
4519 jsonb_each_text_arg,
4520 table_fn_call,
4521 rows_from,
4522 json_table,
4523 } = self;
4524 if unnest_expr.is_some() {
4525 FromItemKind::Unnest
4526 } else if generate_series_args.is_some() {
4527 FromItemKind::GenerateSeries
4528 } else if lateral_subquery.is_some() {
4529 FromItemKind::Subquery
4530 } else if jsonb_each_text_arg.is_some() {
4531 FromItemKind::JsonbEach
4532 } else if table_fn_call.is_some() {
4533 FromItemKind::TableFn
4534 } else if rows_from.is_some() {
4535 FromItemKind::RowsFrom
4536 } else if json_table.is_some() {
4537 FromItemKind::JsonTable
4538 } else if *scalar_fn_item {
4539 FromItemKind::ScalarFn
4540 } else {
4541 FromItemKind::Relation
4542 }
4543 }
4544
4545 /// Every expression this FROM item carries, and the SELECT nested in
4546 /// it — in one place, for every pass that needs them.
4547 ///
4548 /// **Written as a TOTAL destructure, with no `..`.** A field added
4549 /// to `TableRef` is a compile error here, rather than a defect in
4550 /// each pass that enumerated the slots for itself. That is the whole
4551 /// point of the function existing.
4552 ///
4553 /// v7.40.10, on evidence. `TableRef` carries seven expression slots
4554 /// and three separate passes each knew a different subset of them.
4555 /// In one day: the parameter-substitution walk knew only
4556 /// `lateral_subquery`, so `unnest($1)` reached execution still
4557 /// holding a placeholder (a customer's live 500); `describe` knew
4558 /// only `unnest_expr`, so `generate_series(…)` described no columns
4559 /// and a driver got a protocol error; and the LIMIT/OFFSET
4560 /// resolution knew CTEs and UNION peers but not a FROM subquery, so
4561 /// `LIMIT $n` inside a derived table returned every row.
4562 ///
4563 /// Fixing those three one at a time left four slots unvisited.
4564 /// Measured after the third fix shipped, all on the same message:
4565 ///
4566 /// ```text
4567 /// jsonb_each_text($1) parameter $1 referenced but only 0 bound
4568 /// ROWS FROM (…$1…) parameter $1 referenced but only 0 bound
4569 /// json_table($1, …) parameter $1 referenced but only 0 bound
4570 /// ```
4571 ///
4572 /// Those were the next three reports. This is what stops the fourth.
4573 ///
4574 /// # Errors
4575 /// Whatever the callbacks return; the walk stops at the first.
4576 pub fn try_for_each_slot_mut<E>(
4577 &mut self,
4578 visit: &mut dyn FnMut(FromSlot<'_>) -> Result<(), E>,
4579 ) -> Result<(), E> {
4580 // One callback rather than two, so a caller that needs the same
4581 // state for both — every caller so far — does not have to lend
4582 // it twice.
4583 let Self {
4584 // Not expressions — named so the destructure stays total.
4585 name: _,
4586 alias: _,
4587 only: _,
4588 as_of_segment: _,
4589 unnest_column_aliases: _,
4590 with_ordinality: _,
4591 scalar_fn_item: _,
4592 // The seven that carry something to walk.
4593 unnest_expr,
4594 generate_series_args,
4595 lateral_subquery,
4596 jsonb_each_text_arg,
4597 table_fn_call,
4598 rows_from,
4599 json_table,
4600 } = self;
4601 if let Some(e) = unnest_expr {
4602 visit(FromSlot::Expr(e))?;
4603 }
4604 if let Some(args) = generate_series_args {
4605 for a in args.iter_mut() {
4606 visit(FromSlot::Expr(a))?;
4607 }
4608 }
4609 if let Some(sub) = lateral_subquery {
4610 visit(FromSlot::Select(sub))?;
4611 }
4612 if let Some((_, e)) = jsonb_each_text_arg {
4613 visit(FromSlot::Expr(e))?;
4614 }
4615 if let Some(call) = table_fn_call {
4616 for a in call.1.iter_mut() {
4617 visit(FromSlot::Expr(a))?;
4618 }
4619 }
4620 if let Some(items) = rows_from {
4621 for (_, args) in items.iter_mut() {
4622 for a in args.iter_mut() {
4623 visit(FromSlot::Expr(a))?;
4624 }
4625 }
4626 }
4627 if let Some(jt) = json_table {
4628 let JsonTable {
4629 doc,
4630 row_path: _,
4631 columns: _,
4632 passing,
4633 } = jt.as_mut();
4634 visit(FromSlot::Expr(doc))?;
4635 for (_, e) in passing.iter_mut() {
4636 visit(FromSlot::Expr(e))?;
4637 }
4638 }
4639 Ok(())
4640 }
4641}
4642
4643/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4644#[derive(Debug, Clone, PartialEq)]
4645pub struct JsonTable {
4646 /// The document expression (jsonb/json/text). May reference outer
4647 /// columns → implicit LATERAL.
4648 pub doc: Box<Expr>,
4649 /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4650 /// match is one row's context item.
4651 pub row_path: String,
4652 /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4653 pub columns: Vec<JsonTableColumn>,
4654 /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4655 pub passing: Vec<(String, Expr)>,
4656}
4657
4658/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4659#[derive(Debug, Clone, PartialEq)]
4660pub enum JsonTableColumn {
4661 /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4662 Ordinality { name: String },
4663 /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4664 /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4665 /// `<name> <type> EXISTS [PATH '<p>']`.
4666 Regular {
4667 name: String,
4668 ty: ColumnTypeName,
4669 /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4670 path: String,
4671 /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4672 exists: bool,
4673 /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4674 format_json: bool,
4675 /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4676 wrapper: bool,
4677 /// Behaviour when the path matches nothing (default NULL).
4678 on_empty: JsonTableOnBehavior,
4679 /// Behaviour when coercion fails (default NULL).
4680 on_error: JsonTableOnBehavior,
4681 },
4682 /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4683 /// row like a LEFT JOIN (a parent with no nested match still emits one
4684 /// row, nested cols NULL).
4685 Nested {
4686 path: String,
4687 columns: Vec<JsonTableColumn>,
4688 },
4689}
4690
4691/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4692#[derive(Debug, Clone, PartialEq)]
4693pub enum JsonTableOnBehavior {
4694 /// Default: the column value is NULL.
4695 Null,
4696 /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4697 Error,
4698 /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4699 Default(Box<Expr>),
4700}
4701
4702/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4703/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4704/// joins evaluate left-associatively in nested-loop order.
4705#[derive(Debug, Clone, PartialEq)]
4706pub struct FromClause {
4707 pub primary: TableRef,
4708 pub joins: Vec<FromJoin>,
4709}
4710
4711#[derive(Debug, Clone, PartialEq)]
4712pub struct FromJoin {
4713 pub kind: JoinKind,
4714 pub table: TableRef,
4715 /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4716 pub on: Option<Expr>,
4717 /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4718 /// USING column list so the executor can perform PG's column-merge
4719 /// (the join columns collapse to a single unqualified output column,
4720 /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4721 /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4722 /// USING into an equivalent `on` predicate so the join filter/count
4723 /// path works unchanged; `using_cols` drives only the output-shape
4724 /// rewrite. Empty/`None` for `ON` and CROSS joins.
4725 pub using_cols: Option<Vec<String>>,
4726 /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4727 /// column names are not known until the table schemas are available
4728 /// (parse time is schema-less), so the parser only sets this flag and
4729 /// leaves `on`/`using_cols` empty; the engine resolves the common
4730 /// columns at execution time, synthesises the `on` predicate + the
4731 /// USING column-merge, and clears the flag. If there are no common
4732 /// columns PG treats it as a CROSS join.
4733 pub natural: bool,
4734}
4735
4736#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4737pub enum JoinKind {
4738 Inner,
4739 Left,
4740 Cross,
4741 /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4742 /// NULL-filling the left (drive) columns on unmatched right rows.
4743 /// The executor runs the LEFT algorithm's mirror: it tracks which
4744 /// peer rows matched and emits the unmatched ones with a NULL-left
4745 /// tuple after the probe loop. Output column order is unchanged
4746 /// (left-table cols then right-table cols).
4747 Right,
4748 /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4749 /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4750 FullOuter,
4751 /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4752 /// once, paired with the first peer row that satisfies the ON. Not
4753 /// reachable from SQL — the EXISTS pull-up emits it, which is what
4754 /// frees positive EXISTS from the round-721 uniqueness gate (an
4755 /// INNER join would multiply the outer rows; a semi join cannot).
4756 Semi,
4757}
4758
4759#[derive(Debug, Clone, PartialEq)]
4760pub enum Expr {
4761 Literal(Literal),
4762 /// v7.39.2 — `<expr> COLLATE <name>`: the collation this expression
4763 /// compares under, whatever the column or the database says.
4764 ///
4765 /// The parser used to refuse the locale names in this position and
4766 /// SILENTLY ABSORB the byte-order ones, so `'a' COLLATE "C" < 'B'`
4767 /// answered `t` where PostgreSQL 18.6 answers `f` — the one family
4768 /// it let through is the one where dropping it changes the answer.
4769 ///
4770 /// Whether dropping is safe depends on the DATABASE's own collation,
4771 /// which the parser cannot see: under `SPG_LC_COLLATE=C` absorbing
4772 /// `COLLATE "C"` is exactly right. So the name rides along and the
4773 /// engine, which knows, decides.
4774 Collate {
4775 expr: Box<Expr>,
4776 collation: String,
4777 },
4778 Column(ColumnName),
4779 /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4780 /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4781 /// callee's declared parameter names, and a user function's live in the
4782 /// catalog — which the parser cannot see. So the name rides along in the
4783 /// tree and the evaluator, which has the catalog, does the reordering.
4784 /// Appears only inside a `FunctionCall`'s argument list.
4785 NamedArg {
4786 name: String,
4787 expr: Box<Expr>,
4788 },
4789 /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
4790 /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
4791 /// expression evaluates to an array whose elements the evaluator splices
4792 /// into the call as individual trailing arguments. Appears only inside a
4793 /// `FunctionCall`'s argument list.
4794 Variadic(Box<Expr>),
4795 /// v6.1.1 — `$N` parameter placeholder for the extended query
4796 /// protocol. The number is 1-based per PostgreSQL convention.
4797 /// Evaluation looks up `params[N-1]` from the prepared-statement
4798 /// bind buffer; out-of-range indices raise a runtime error
4799 /// (same shape as a column-not-found miss).
4800 Placeholder(u16),
4801 Binary {
4802 lhs: Box<Expr>,
4803 op: BinOp,
4804 rhs: Box<Expr>,
4805 },
4806 Unary {
4807 op: UnOp,
4808 expr: Box<Expr>,
4809 },
4810 /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
4811 /// TEXT, BOOL targets; engine coerces at evaluation time.
4812 Cast {
4813 expr: Box<Expr>,
4814 target: CastTarget,
4815 },
4816 /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
4817 /// evaluates to a composite/record value (an explicit `ROW(...)`, a
4818 /// whole-row reference, or a composite-returning function); `field` names
4819 /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
4820 /// column names for a whole-row). Only the parenthesised form reaches
4821 /// here — a bare `a.b` is parsed as a qualified column reference.
4822 FieldAccess {
4823 base: Box<Expr>,
4824 field: String,
4825 },
4826 /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
4827 IsNull {
4828 expr: Box<Expr>,
4829 negated: bool,
4830 },
4831 /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
4832 /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
4833 /// `Some(false)` for FALSE and `None` for UNKNOWN.
4834 ///
4835 /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
4836 /// The semantics were right, but the AST then had no way to say what
4837 /// the user wrote, so every renderer printed the lowering:
4838 /// `CHECK ((a > 1) IS TRUE)` came back as
4839 /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
4840 /// dumped view lost the form too.
4841 BoolTest {
4842 expr: Box<Expr>,
4843 value: Option<bool>,
4844 negated: bool,
4845 },
4846 /// Function call `name(args...)`. v1.4 supports a small built-in set
4847 /// (length, upper, lower, abs, coalesce); unknown names error at eval
4848 /// time so the parser stays open for v1.5 aggregates.
4849 FunctionCall {
4850 name: String,
4851 args: Vec<Expr>,
4852 },
4853 /// v7.24 (mailrs round-16 A) — an aggregate call with an
4854 /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
4855 /// Wraps the plain [`Expr::FunctionCall`] so every existing
4856 /// FunctionCall consumer stays untouched; only the aggregate
4857 /// executor (and the expression walkers) know the wrapper.
4858 /// Non-aggregate evaluation contexts reject it at eval time.
4859 AggregateOrdered {
4860 call: Box<Expr>,
4861 order_by: Vec<OrderBy>,
4862 /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
4863 /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
4864 /// aggregate modifier so plain FunctionCall stays untouched.
4865 distinct: bool,
4866 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
4867 /// Only the rows where `cond` is true contribute to this
4868 /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
4869 /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
4870 /// END)`, which is faithful for NULL-ignoring aggregates but
4871 /// WRONG for `array_agg` (it would collect a NULL per excluded
4872 /// row). The executor instead skips excluded rows before
4873 /// accumulation, which is correct for every aggregate.
4874 filter: Option<Box<Expr>>,
4875 },
4876 /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
4877 /// wildcards are `%` (any run) and `_` (one char), backslash escapes
4878 /// the next char (so `\%` matches a literal `%`).
4879 Like {
4880 expr: Box<Expr>,
4881 pattern: Box<Expr>,
4882 negated: bool,
4883 /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
4884 /// match. PG folds both operands.
4885 case_insensitive: bool,
4886 },
4887 /// v4.12 window function call: `name(args) OVER (PARTITION BY
4888 /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
4889 /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
4890 /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
4891 /// unordered windows and "from start of partition through
4892 /// current row" for ordered windows — no explicit ROWS /
4893 /// RANGE clause in v4.12 MVP.
4894 WindowFunction {
4895 name: String,
4896 args: Vec<Expr>,
4897 partition_by: Vec<Expr>,
4898 /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
4899 /// (None = PG default, same contract as [`OrderBy`]).
4900 order_by: Vec<(
4901 Expr,
4902 bool, /* desc */
4903 Option<bool>, /* nulls_first */
4904 )>,
4905 /// v4.20 explicit frame. `None` means "use the default":
4906 /// whole-partition when unordered, running aggregate from
4907 /// partition start through current row when ordered.
4908 frame: Option<WindowFrame>,
4909 /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
4910 /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
4911 /// `Respect` (PG / ANSI default — NULLs participate). Other
4912 /// window functions ignore this flag.
4913 null_treatment: NullTreatment,
4914 /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
4915 /// = no FILTER. Only aggregate window functions honor it; the
4916 /// predicate restricts which peer rows contribute within the frame.
4917 filter: Option<Box<Expr>>,
4918 },
4919 /// v4.10 scalar subquery — `(SELECT ...)` used in expression
4920 /// position. Must return exactly one row × one column at eval
4921 /// time; the engine errors out otherwise. Uncorrelated only —
4922 /// the inner SELECT cannot reference outer columns.
4923 ScalarSubquery(Box<SelectStatement>),
4924 /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
4925 /// projection is ignored; only row-count matters.
4926 Exists {
4927 subquery: Box<SelectStatement>,
4928 negated: bool,
4929 },
4930 /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
4931 /// project exactly one column; membership is tested by Eq
4932 /// against each row's value (NULL handling follows ANSI:
4933 /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
4934 InSubquery {
4935 expr: Box<Expr>,
4936 subquery: Box<SelectStatement>,
4937 negated: bool,
4938 },
4939 /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
4940 /// against a multi-column subquery. Row comparisons against a *list*
4941 /// decompose to OR-of-AND at parse time, but the subquery form can't
4942 /// (its rows are only known at runtime), so this survives as its own
4943 /// node evaluated with PG's row-comparison three-valued logic.
4944 RowInSubquery {
4945 row: Vec<Expr>,
4946 subquery: Box<SelectStatement>,
4947 negated: bool,
4948 },
4949 /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
4950 /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
4951 /// RowInSubquery, the literal-RHS form decomposes at parse time but the
4952 /// subquery form can't, so it survives as its own node. The subquery
4953 /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
4954 RowCmpSubquery {
4955 row: Vec<Expr>,
4956 op: BinOp,
4957 subquery: Box<SelectStatement>,
4958 },
4959 /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
4960 /// list. Both the parser's literal-list path and the engine's
4961 /// IN-subquery materialisation used to desugar into a left-deep
4962 /// OR-Eq chain, so expression depth scaled with the element count
4963 /// — a 24k-row subquery result overflowed the 2 MiB worker stack
4964 /// (recursive eval AND recursive Box drop) and aborted embedding
4965 /// host processes. The flat node keeps depth constant: eval is an
4966 /// iterative scan with PG three-valued logic, drop is a Vec drop.
4967 InList {
4968 expr: Box<Expr>,
4969 list: Vec<Expr>,
4970 negated: bool,
4971 },
4972 /// `EXTRACT(<field> FROM <source>)` — pull an integer component
4973 /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
4974 /// because the `FROM` keyword is what separates the two halves,
4975 /// not a comma.
4976 Extract {
4977 field: ExtractField,
4978 source: Box<Expr>,
4979 },
4980 /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
4981 /// element is evaluated independently; NULLs are allowed.
4982 /// v7.10 supports only single-dimension TEXT[] semantically;
4983 /// non-text elements coerce at engine evaluation time when
4984 /// the surrounding context (column type / cast) makes the
4985 /// target clear.
4986 Array(Vec<Expr>),
4987 /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
4988 /// engine returns NULL for out-of-range indices.
4989 ArraySubscript {
4990 target: Box<Expr>,
4991 index: Box<Expr>,
4992 },
4993 /// Array slice `arr[lo:hi]` — PG 1-based, both ends
4994 /// inclusive; a missing bound extends to that end of the
4995 /// array and out-of-range bounds clamp. Returns an array of
4996 /// the same element type.
4997 ArraySlice {
4998 target: Box<Expr>,
4999 lo: Option<Box<Expr>>,
5000 hi: Option<Box<Expr>>,
5001 },
5002 /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
5003 /// operator is the comparison binary op (Eq / Ne / Lt / …);
5004 /// the engine desugars: `ANY` returns true if any element
5005 /// satisfies; `ALL` returns true only if every element does.
5006 /// NULL handling follows PG's three-valued logic.
5007 AnyAll {
5008 expr: Box<Expr>,
5009 op: BinOp,
5010 array: Box<Expr>,
5011 /// `true` = ANY, `false` = ALL.
5012 is_any: bool,
5013 },
5014 /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
5015 /// (searched form, `operand` is None) and
5016 /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
5017 /// `operand` is the lead expression compared against each
5018 /// branch's match). Each `(when_expr, then_expr)` branch
5019 /// stays as written; engine short-circuits on the first match.
5020 /// `else_branch` is `None` when no ELSE; evaluates to NULL.
5021 /// mailrs round-5 G9.
5022 Case {
5023 operand: Option<Box<Expr>>,
5024 branches: Vec<(Expr, Expr)>,
5025 else_branch: Option<Box<Expr>>,
5026 },
5027}
5028
5029/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
5030/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
5031/// in the offset walk. `Ignore` causes the function to skip NULL
5032/// values in the argument expression, returning the next non-NULL.
5033#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5034pub enum NullTreatment {
5035 #[default]
5036 Respect,
5037 Ignore,
5038}
5039
5040/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
5041/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
5042/// where end implicitly = CURRENT ROW.
5043#[derive(Debug, Clone, PartialEq, Eq)]
5044pub struct WindowFrame {
5045 pub kind: FrameKind,
5046 pub start: FrameBound,
5047 pub end: Option<FrameBound>,
5048 /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
5049 /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
5050 /// no-op; CURRENT ROW drops the current row from the frame.
5051 pub exclude: FrameExclusion,
5052}
5053
5054#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5055pub enum FrameExclusion {
5056 /// Default — exclude nothing.
5057 #[default]
5058 NoOthers,
5059 /// Drop the current row from the frame.
5060 CurrentRow,
5061 /// Drop the current row's whole peer group.
5062 Group,
5063 /// Drop the current row's peers but keep the current row.
5064 Ties,
5065}
5066
5067#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5068pub enum FrameKind {
5069 Rows,
5070 Range,
5071 /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
5072 /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
5073 /// bounds (no explicit integer offsets) GROUPS behaves identically
5074 /// to RANGE — both consult the peer-group of the current row.
5075 /// Integer offsets are not yet supported; the executor rejects
5076 /// them at run time.
5077 Groups,
5078}
5079
5080#[derive(Debug, Clone, PartialEq, Eq)]
5081pub enum FrameBound {
5082 UnboundedPreceding,
5083 OffsetPreceding(u64),
5084 CurrentRow,
5085 OffsetFollowing(u64),
5086 UnboundedFollowing,
5087 /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
5088 /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
5089 /// interval is folded to its (months, days, micros) components at
5090 /// parse time.
5091 IntervalPreceding {
5092 months: i32,
5093 days: i32,
5094 micros: i64,
5095 },
5096 IntervalFollowing {
5097 months: i32,
5098 days: i32,
5099 micros: i64,
5100 },
5101}
5102
5103impl fmt::Display for FrameBound {
5104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5105 match self {
5106 Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
5107 Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
5108 Self::CurrentRow => f.write_str("CURRENT ROW"),
5109 Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
5110 Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
5111 Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
5112 Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
5113 }
5114 }
5115}
5116
5117#[derive(Debug, Clone, PartialEq, Eq)]
5118pub enum ExtractField {
5119 Year,
5120 Month,
5121 Day,
5122 Hour,
5123 Minute,
5124 Second,
5125 Microsecond,
5126 /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
5127 /// SPG keeps the integer convention — truncated seconds).
5128 Epoch,
5129 /// Day of week, 0 = Sunday … 6 = Saturday.
5130 Dow,
5131 /// ISO day of week, 1 = Monday … 7 = Sunday.
5132 Isodow,
5133 /// Day of year, 1-366.
5134 Doy,
5135 /// ISO 8601 week number, 1-53.
5136 Week,
5137 /// ISO 8601 week-numbering year (pairs with `Week`).
5138 Isoyear,
5139 /// Quarter, 1-4.
5140 Quarter,
5141 /// Year divided by 10 (floor).
5142 Decade,
5143 /// Century — 2001-2100 is century 21.
5144 Century,
5145 /// Millennium — 2001-3000 is millennium 3.
5146 Millennium,
5147 /// Julian day number (truncated for timestamps).
5148 Julian,
5149 /// Seconds and fraction in milliseconds (ss·1000 + frac).
5150 Millisecond,
5151 /// UTC offset in seconds — SPG sessions run UTC, so 0.
5152 Timezone,
5153 /// Hour component of the UTC offset — 0.
5154 TimezoneHour,
5155 /// Minute component of the UTC offset — 0.
5156 TimezoneMinute,
5157 /// v7.39 (round 253) — a field name the parser does not know. PG
5158 /// resolves EXTRACT fields at RUNTIME and reports them with the
5159 /// source type (`unit "nosuch" not recognized for type timestamp
5160 /// without time zone`, 22023), so the parser carries the raw name
5161 /// instead of rejecting.
5162 Other(String),
5163}
5164
5165impl fmt::Display for ExtractField {
5166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5167 f.write_str(match self {
5168 Self::Year => "YEAR",
5169 Self::Month => "MONTH",
5170 Self::Day => "DAY",
5171 Self::Hour => "HOUR",
5172 Self::Minute => "MINUTE",
5173 Self::Second => "SECOND",
5174 Self::Microsecond => "MICROSECOND",
5175 Self::Epoch => "EPOCH",
5176 Self::Dow => "DOW",
5177 Self::Isodow => "ISODOW",
5178 Self::Doy => "DOY",
5179 Self::Week => "WEEK",
5180 Self::Isoyear => "ISOYEAR",
5181 Self::Quarter => "QUARTER",
5182 Self::Decade => "DECADE",
5183 Self::Century => "CENTURY",
5184 Self::Millennium => "MILLENNIUM",
5185 Self::Julian => "JULIAN",
5186 Self::Millisecond => "MILLISECOND",
5187 Self::Timezone => "TIMEZONE",
5188 Self::TimezoneHour => "TIMEZONE_HOUR",
5189 Self::TimezoneMinute => "TIMEZONE_MINUTE",
5190 Self::Other(name) => return f.write_str(name),
5191 })
5192 }
5193}
5194
5195#[derive(Debug, Clone, PartialEq, Eq)]
5196pub enum CastTarget {
5197 Int,
5198 BigInt,
5199 Float,
5200 Text,
5201 Bool,
5202 Vector,
5203 Date,
5204 Timestamp,
5205 /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
5206 /// H3a. Engine reuses the existing runtime-interval / timestamp
5207 /// paths (parse the text input, return the matching Value).
5208 Interval,
5209 Timestamptz,
5210 /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
5211 /// types (v7.9.0); the cast just routes Text→Json with the
5212 /// requested OID for the wire layer.
5213 Json,
5214 Jsonb,
5215 /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
5216 /// compatibility; engine surfaces as Unsupported with a
5217 /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
5218 RegType,
5219 RegClass,
5220 /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
5221 /// the PG external array form `{a,b,NULL}`.
5222 TextArray,
5223 /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
5224 /// `{1,2,3}` or widens a `TextArray` whose elements are
5225 /// integer-shaped.
5226 IntArray,
5227 BigIntArray,
5228 /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
5229 /// external form text representation. Used by pg_dump output
5230 /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
5231 TsVector,
5232 TsQuery,
5233 /// v7.17.0 — `::uuid`. Decodes the LHS Text via
5234 /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
5235 /// unhyphenated, uppercase, and brace-wrapped forms); malformed
5236 /// input is a SQL error.
5237 Uuid,
5238 /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
5239 /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
5240 /// inputs pass through unchanged. Closes the mailrs D-pre #3
5241 /// reverse-acceptance gap — anywhere a PG schema writes
5242 /// `expr::bytea`, SPG now matches.
5243 Bytea,
5244 /// v7.37.5 ship triage — generic cast target for the long tail
5245 /// of PG type names the parser meets in `expr::TYPE` shapes that
5246 /// don't deserve their own enum variant. The engine routes these
5247 /// through `column_type_to_data_type` + the existing typed
5248 /// `coerce_value` dispatch, so adding a new PG type to SPG
5249 /// implicitly adds its cast-target form too — no parser change
5250 /// per type. The string carries the lowercase PG type ident
5251 /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
5252 /// a clear message when the type isn't known.
5253 Named(String),
5254}
5255
5256impl fmt::Display for CastTarget {
5257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5258 f.write_str(match self {
5259 Self::Int => "int",
5260 Self::BigInt => "bigint",
5261 Self::Float => "float",
5262 Self::Text => "text",
5263 Self::Bool => "bool",
5264 Self::Vector => "vector",
5265 Self::Interval => "interval",
5266 Self::Timestamptz => "timestamptz",
5267 Self::Json => "json",
5268 Self::Jsonb => "jsonb",
5269 Self::RegType => "regtype",
5270 Self::RegClass => "regclass",
5271 Self::Date => "date",
5272 Self::Timestamp => "timestamp",
5273 Self::TextArray => "TEXT[]",
5274 Self::IntArray => "INT[]",
5275 Self::BigIntArray => "BIGINT[]",
5276 Self::TsVector => "tsvector",
5277 Self::TsQuery => "tsquery",
5278 Self::Uuid => "uuid",
5279 Self::Bytea => "bytea",
5280 // v7.37.5 — `Self::Named` carries its own canonical name.
5281 Self::Named(name) => return f.write_str(name),
5282 })
5283 }
5284}
5285
5286#[derive(Debug, Clone, PartialEq)]
5287pub enum Literal {
5288 Integer(i64),
5289 Float(f64),
5290 /// Exact decimal literal — a bare `12.34`-style token, kept as
5291 /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
5292 /// before it becomes a `Value::Numeric`. PG parses such literals as
5293 /// `numeric`, not `double precision`. (Scientific/huge literals stay
5294 /// `Float`.)
5295 Numeric {
5296 unscaled: i128,
5297 /// v7.39 (round 271) — widened to u16. At u8 a literal with more
5298 /// than 255 decimal places could not be represented, and the
5299 /// conversion's `.expect("lexer-validated decimal")` aborted the
5300 /// query with an internal error on SQL PG accepts.
5301 scale: u16,
5302 },
5303 /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
5304 /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
5305 /// `Value::NumericBig` at eval; previously such literals fell back to double.
5306 NumericBig(String),
5307 String(String),
5308 /// v7.38.8 — a temporal constant that has already been decoded.
5309 ///
5310 /// Without these the only way to carry one through the AST was as
5311 /// text, and a predicate comparing a `timestamp` column against a
5312 /// literal then coerced that text back into a timestamp ONCE PER
5313 /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
5314 /// profile. `constfold` produced text for the same reason: its exit
5315 /// had nothing else to hand back.
5316 ///
5317 /// `text` keeps the spelling so `Display` round-trips byte for byte,
5318 /// the way `Interval` already does and for the same reason: this
5319 /// node is printed in EXPLAIN, in dumps and in error messages, and
5320 /// none of those should change because the value stopped being
5321 /// carried as a string. The enum already holds a `String` and an
5322 /// `i128`, so neither variant widens it.
5323 Timestamp {
5324 micros: i64,
5325 text: String,
5326 },
5327 /// Days since the epoch `Value::Date` counts from. See
5328 /// [`Literal::Timestamp`].
5329 Date {
5330 days: i32,
5331 text: String,
5332 },
5333 Bool(bool),
5334 Null,
5335 /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
5336 Vector(Vec<f32>),
5337 /// TEXT[] value carried through the prepared-bind path
5338 /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
5339 /// text form, so the array rides the AST natively).
5340 TextArray(Vec<Option<String>>),
5341 /// INT[] value carried through the prepared-bind path.
5342 IntArray(Vec<Option<i32>>),
5343 /// BIGINT[] value carried through the prepared-bind path.
5344 BigIntArray(Vec<Option<i64>>),
5345 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
5346 /// Three independent dimensions: `months` (variable-length;
5347 /// year/month), `days` (fixed 86400 seconds at non-DST, but
5348 /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
5349 /// stays distinguishable), and `micros` (sub-day; can carry).
5350 /// `text` keeps the original spelling so Display round-trips
5351 /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
5352 Interval {
5353 months: i32,
5354 days: i32,
5355 micros: i64,
5356 text: String,
5357 },
5358}
5359
5360#[derive(Debug, Clone, PartialEq, Eq)]
5361pub struct ColumnName {
5362 pub qualifier: Option<String>,
5363 pub name: String,
5364}
5365
5366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5367pub enum BinOp {
5368 Or,
5369 And,
5370 Eq,
5371 NotEq,
5372 /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
5373 /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
5374 /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
5375 /// non-NULL behaviour matches `<>` / `=` exactly. Common in
5376 /// PG-style JOIN ON predicates and pg_dump output.
5377 IsDistinctFrom,
5378 IsNotDistinctFrom,
5379 /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
5380 /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
5381 /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
5382 /// is a real division (round 351).
5383 IntDiv,
5384 Lt,
5385 LtEq,
5386 Gt,
5387 GtEq,
5388 Add,
5389 Sub,
5390 Mul,
5391 Div,
5392 /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
5393 /// precedence as Mul/Div; result type follows left operand.
5394 Mod,
5395 /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
5396 /// operands of equal dimension; engine returns `Value::Float(d)`.
5397 L2Distance,
5398 /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
5399 GeomParallel,
5400 /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
5401 OverLeft,
5402 OverRight,
5403 /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
5404 GeomPerp,
5405 /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
5406 GeomSameAs,
5407 /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
5408 /// object to the left-hand one.
5409 ClosestPoint,
5410 /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
5411 GeomHoriz,
5412 /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
5413 /// more similar" remains true (matches pgvector's published convention).
5414 InnerProduct,
5415 /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
5416 CosineDistance,
5417 /// SQL string concatenation `||`. NULL propagates.
5418 Concat,
5419 /// Bitwise OR `|` on integers.
5420 BitOr,
5421 /// Bitwise AND `&` on integers.
5422 BitAnd,
5423 /// Bitwise XOR `#` on integers and equal-length bit strings.
5424 BitXor,
5425 /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
5426 /// sides as truth values and returns their exclusive-or (`1 XOR 0`
5427 /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
5428 /// MySQL dialect produces it; PG has no logical XOR. Its precedence
5429 /// sits between OR (loosest) and AND.
5430 LogicalXor,
5431 /// v4.14 `json -> key` — element access by string key (object)
5432 /// or integer index (array). Returns a JSON value.
5433 JsonGet,
5434 /// v4.14 `json ->> key` — same access, returns the result as
5435 /// TEXT (unwraps a top-level JSON string; renders other scalars
5436 /// as their canonical text).
5437 JsonGetText,
5438 /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
5439 /// text array literal like `'{a,0,b}'`. Returns JSON.
5440 JsonGetPath,
5441 /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
5442 JsonGetPathText,
5443 /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
5444 /// when every key/value in `sub_json` is structurally present in
5445 /// the left side. Matches PG semantics (top-level + recursive).
5446 JsonContains,
5447 /// `@?` — jsonb path existence (jsonb_path_exists).
5448 JsonPathExists,
5449 /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
5450 /// `a <@ b` is defined as `b @> a` (same semantics, swapped
5451 /// sides). Eval dispatch reuses `JsonContains` with swapped args.
5452 JsonContainedBy,
5453 /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
5454 /// returns BOOL. For an object, true if `key` is an existing
5455 /// member name; for an array, true if any element is the string
5456 /// `key` (PG semantics).
5457 JsonKeyExists,
5458 /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
5459 /// returns BOOL.
5460 JsonKeysAny,
5461 /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
5462 /// returns BOOL.
5463 JsonKeysAll,
5464 /// `jsonb #- path_text[]` — delete the value at a nested path.
5465 /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
5466 JsonDeletePath,
5467 /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
5468 /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
5469 /// tsvector` and engine eval normalises either ordering.
5470 TsMatch,
5471 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
5472 /// `<<`. LHS network is strictly inside RHS network (no equality).
5473 InetContainedBy,
5474 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
5475 /// `<<=`. LHS network ⊆ RHS network.
5476 InetContainedByEq,
5477 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
5478 /// LHS network strictly contains RHS network.
5479 InetContains,
5480 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
5481 /// LHS network ⊇ RHS network.
5482 InetContainsEq,
5483 /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
5484 /// True iff either network contains any address of the other.
5485 InetOverlap,
5486 /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
5487 /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
5488 Intersects,
5489 /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
5490 /// (point, box).
5491 IsBelow,
5492 IsAbove,
5493 /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
5494 /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
5495 /// where `'A' < 'a'` is false under a non-C collation, which is the
5496 /// whole reason the operator family exists — it is what makes a LIKE
5497 /// prefix index-usable. pg_dump writes these into index definitions.
5498 PatternLt,
5499 PatternLtEq,
5500 PatternGt,
5501 PatternGtEq,
5502}
5503
5504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5505pub enum UnOp {
5506 Not,
5507 Neg,
5508 /// Bitwise NOT `~` on integers.
5509 BitNot,
5510 /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
5511 /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
5512 /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
5513 /// while PG18 and MariaDB accept every one of them.
5514 ///
5515 /// It is not a no-op to drop at parse time — PG refuses it on
5516 /// non-numeric operands ("operator does not exist: + boolean"), so the
5517 /// operand's type has to be seen at eval.
5518 Plus,
5519}
5520
5521// --- Display impls (round-trip-safe) --------------------------------------
5522
5523impl Statement {
5524 /// v7.18 — classify whether the statement is read-only at
5525 /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
5526 /// route SELECT-shaped traffic through the fan-out
5527 /// `AsyncReadHandle` (no writer-lock contention) while
5528 /// keeping DML / DDL / TX-control on the single-writer path.
5529 ///
5530 /// The classification matches what
5531 /// `Engine::execute_readonly_with_cancel` accepts: anything
5532 /// that does NOT mutate catalog, statistics, session state,
5533 /// or transaction state. WaitForWalPosition is included
5534 /// (engine returns `Unsupported`, but the classification is
5535 /// semantically read-only — no mutation). Empty is excluded
5536 /// out of an abundance of caution — the no-op routes
5537 /// through the writer so any future side effect lands
5538 /// uniformly.
5539 ///
5540 /// **Not connection-state aware**. `SET LOCAL` / `RESET`
5541 /// affect session parameters and must run on the writer
5542 /// engine that owns the session state; they classify as
5543 /// writer-path here. Same for `BEGIN` / `COMMIT` /
5544 /// `ROLLBACK` / `SAVEPOINT` — transaction control is
5545 /// always writer-path.
5546 /// v7.39 (round 435) — does this statement implicitly COMMIT an open
5547 /// transaction under MySQL?
5548 ///
5549 /// PG runs DDL inside the transaction; MySQL commits before (and after)
5550 /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
5551 /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
5552 /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
5553 /// nested START TRANSACTION; and measured NOT to fire for `CREATE
5554 /// TEMPORARY TABLE`, `SET`, or a SELECT.
5555 ///
5556 /// A positive list, not "everything that is not DML": a statement
5557 /// wrongly listed here commits a client's data early, which is as bad as
5558 /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
5559 /// COMPACT) are left out — a MySQL session never sends them.
5560 #[must_use]
5561 pub fn mysql_implicit_commit(&self) -> bool {
5562 match self {
5563 // MySQL's documented exception, measured on MariaDB 11: a
5564 // TEMPORARY table is not DDL for this purpose and does not
5565 // commit. (Round 435 got this for free because the parser then
5566 // lowered that spelling to `Statement::Empty`; round 436 made it
5567 // a real CREATE TABLE, and the round-435 pin caught it.)
5568 Self::CreateTable(c) => !c.temporary,
5569 // MySQL commits the open transaction and opens a fresh one.
5570 Self::Begin { .. }
5571 | Self::DropTable { .. }
5572 | Self::DropIndex { .. }
5573 | Self::CreateIndex(_)
5574 | Self::AlterIndex { .. }
5575 | Self::AlterTable(_)
5576 | Self::Truncate { .. }
5577 | Self::Analyze { .. }
5578 | Self::CreateStatistics { .. }
5579 | Self::DropStatistics { .. }
5580 | Self::CreateView { .. }
5581 | Self::DropView { .. }
5582 | Self::CreateMaterializedView { .. }
5583 | Self::RefreshMaterializedView { .. }
5584 | Self::DropMaterializedView { .. }
5585 | Self::CreateSequence(_)
5586 | Self::AlterSequence { .. }
5587 | Self::DropSequence { .. }
5588 | Self::CreateFunction(_)
5589 | Self::DropFunction { .. }
5590 | Self::CreateTrigger(_)
5591 | Self::DropTrigger { .. }
5592 | Self::CreateRule(_)
5593 | Self::DropRule { .. }
5594 | Self::CreateType(_)
5595 | Self::DropType { .. }
5596 | Self::AlterTypeAddValue { .. }
5597 | Self::AlterTypeRenameValue { .. }
5598 | Self::CreateDomain(_)
5599 | Self::AlterDomain { .. }
5600 | Self::DropDomain { .. }
5601 | Self::CreateSchema { .. }
5602 | Self::DropSchema { .. }
5603 | Self::CreateUser { .. }
5604 | Self::DropUser { .. }
5605 | Self::Grant { .. }
5606 | Self::Revoke { .. }
5607 | Self::CreatePolicy(_)
5608 | Self::AlterPolicy(_)
5609 | Self::DropPolicy { .. }
5610 | Self::CommentOn { .. }
5611 | Self::CreateExtension { .. } => true,
5612 _ => false,
5613 }
5614 }
5615
5616 #[must_use]
5617 pub fn is_readonly(&self) -> bool {
5618 match self {
5619 Statement::RenameTables(_) => false,
5620 // v7.39 (round 288) — SET CONSTRAINTS changes transaction
5621 // state, and IMMEDIATE can run the deferred checks there and
5622 // then; writer-path.
5623 Statement::SetConstraints { .. } => false,
5624 // v7.39 (round 695) — it writes nothing (SPG has no
5625 // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
5626 // writer and a read-only session refuses it there too.
5627 Statement::AlterSystem { .. } => false,
5628 // Same shape: a no-op here, a writer to PG, so a read-only
5629 // session refuses it as PG's would.
5630 Statement::NoOpPreventedInTransaction { .. } => false,
5631 Statement::DropDatabase { .. } => false,
5632 // v7.39 (round 696) — they perform nothing, so nothing is
5633 // written; PG classes LOCK and the OWNED BY pair as writers and
5634 // a read-only session refuses them there.
5635 Statement::ValidateOnly { .. } => false,
5636 // v7.39 (round 750) — a credential rotation persists.
5637 Statement::AlterRolePassword { .. } => true,
5638 Statement::DropAggregate { .. } => false,
5639 // v7.39 (round 547) — records a GUC default in the catalog.
5640 Statement::SetDbRoleSetting(_) => false,
5641 // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5642 // but they name a relation and PG refuses one that is not
5643 // there, so they are not read-only in the sense this asks.
5644 Statement::Maintain { .. } => false,
5645 // v7.39 (round 277) — the prepared-statement surface is
5646 // session state, like SET; writer-path so it lands on the
5647 // engine that owns the session. EXECUTE may also run a
5648 // write, and its body is only known at execution time.
5649 Statement::Prepare { .. }
5650 | Statement::Execute { .. }
5651 | Statement::Deallocate(_)
5652 | Statement::Call(_)
5653 | Statement::PrepareTransaction(_)
5654 | Statement::CreateStatistics { .. }
5655 | Statement::DropStatistics { .. }
5656 // v7.39 (round 318, V51) — KILL signals another connection;
5657 // it must run on the writer path that owns the registry hook.
5658 | Statement::Kill { .. }
5659 // v7.39 (round 320, V53) — DISCARD throws session state away;
5660 // writer path, like SET / RESET.
5661 | Statement::Discard(_)
5662 // v7.39.2 — `USE <db>` writes session state, the same way
5663 // SET does, and takes the same path.
5664 | Statement::UseDatabase(_) => false,
5665 // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5666 // locks MUTATES the lock table, so it is not a read. Left as
5667 // a read it went to the read-only executor and the locking
5668 // pre-pass never ran at all — the clause was honoured only
5669 // inside an explicit transaction, and silently ignored in
5670 // autocommit, which is where a queue worker runs it.
5671 Statement::Select(s) if s.locking.is_some() => false,
5672 Statement::Select(_)
5673 | Statement::CopyTo { .. }
5674 | Statement::CopyToFile { .. }
5675 | Statement::Explain(_)
5676 | Statement::ShowTables
5677 | Statement::ShowDatabases
5678 | Statement::ShowCreateTable(_)
5679 | Statement::ShowIndexes(_)
5680 | Statement::ShowStatus
5681 | Statement::ShowVariables
5682 | Statement::ShowVariablesLike(_)
5683 | Statement::ShowProcesslist
5684 | Statement::ShowColumns(_)
5685 | Statement::ShowUsers
5686 | Statement::ShowPublications
5687 | Statement::ShowSubscriptions
5688 | Statement::WaitForWalPosition { .. } => true,
5689 // Everything else mutates catalog, statistics,
5690 // session state, or transaction state — writer path.
5691 // Listed explicitly so a new Statement variant fails
5692 // the match exhaustiveness check and forces a
5693 // classification decision at add-site.
5694 Statement::Empty
5695 // v7.39 (round 169) — VACUUM mutates storage (reclaims
5696 // tombstoned versions): writer path.
5697 | Statement::Vacuum { .. }
5698 | Statement::DropTable { .. }
5699 | Statement::DropIndex { .. }
5700 | Statement::CreateTable(_)
5701 | Statement::CreateExtension(_)
5702 | Statement::DoBlock(_)
5703 | Statement::CreateIndex(_)
5704 | Statement::Insert(_)
5705 | Statement::Update(_)
5706 | Statement::Delete(_)
5707 | Statement::Merge(_)
5708 | Statement::Begin(_)
5709 | Statement::Commit
5710 | Statement::Rollback
5711 | Statement::Savepoint(_)
5712 | Statement::RollbackToSavepoint(_)
5713 | Statement::ReleaseSavepoint(_)
5714 | Statement::CreateUser(_)
5715 | Statement::DropUser { .. }
5716 | Statement::SetRole(_)
5717 | Statement::Grant(_)
5718 | Statement::Revoke(_)
5719 | Statement::CreatePolicy(_)
5720 | Statement::AlterPolicy(_)
5721 | Statement::DropPolicy(_)
5722 | Statement::AlterIndex(_)
5723 | Statement::AlterTable(_)
5724 | Statement::CreatePublication(_)
5725 | Statement::DropPublication { .. }
5726 | Statement::CreateSubscription(_)
5727 | Statement::DropSubscription { .. }
5728 | Statement::Analyze(_)
5729 | Statement::Truncate { .. }
5730 | Statement::CompactColdSegments
5731 | Statement::SetParameter { .. }
5732 | Statement::SetParameterList(_)
5733 | Statement::SetUserVars(..)
5734 | Statement::SetTransaction { .. }
5735 | Statement::ShowParameter(_)
5736 | Statement::ResetParameter(_)
5737 | Statement::CreateFunction(_)
5738 | Statement::CreateTrigger(_)
5739 | Statement::DropTrigger { .. }
5740 | Statement::CreateRule(_)
5741 | Statement::DropRule { .. }
5742 | Statement::DropFunction { .. }
5743 | Statement::CreateSequence(_)
5744 | Statement::AlterSequence(_)
5745 | Statement::DropSequence { .. }
5746 | Statement::CreateView(_)
5747 | Statement::DropView { .. }
5748 | Statement::CreateMaterializedView(_)
5749 | Statement::RefreshMaterializedView { .. }
5750 | Statement::DropMaterializedView { .. }
5751 | Statement::CreateType(_)
5752 | Statement::AlterTypeAddValue { .. }
5753 | Statement::AlterTypeRenameValue { .. }
5754 | Statement::CommentOn { .. }
5755 | Statement::DropType { .. }
5756 | Statement::CreateDomain(_)
5757 | Statement::DropDomain { .. }
5758 | Statement::CreateSchema { .. }
5759 | Statement::DropSchema { .. }
5760 // v7.39 (round 218) — cursors mutate per-session cursor state
5761 // (open/position/close) on the writer engine: writer path.
5762 | Statement::DeclareCursor { .. }
5763 | Statement::FetchCursor { .. }
5764 | Statement::MoveCursor { .. }
5765 | Statement::CloseCursor { .. }
5766 // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5767 // state / the notification queue: writer path.
5768 | Statement::Listen(_)
5769 | Statement::Notify { .. }
5770 | Statement::Unlisten(_)
5771 | Statement::CopyFromFile { .. }
5772 | Statement::AlterDomain { .. } => false,
5773 }
5774 }
5775}
5776
5777/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5778/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5779#[derive(Debug, Clone, PartialEq, Eq)]
5780pub struct GrantStatement {
5781 /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5782 /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5783 /// is why they keep the case the user typed.
5784 pub privileges: Vec<GrantPriv>,
5785 /// What the privileges are on.
5786 pub object: GrantObject,
5787 /// The roles granted to / revoked from. An empty string entry = PUBLIC.
5788 pub grantees: Vec<String>,
5789 /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
5790 /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
5791 /// privilege itself).
5792 pub grant_option: bool,
5793}
5794
5795/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
5796/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
5797/// An empty column list means the privilege is table-wide.
5798#[derive(Debug, Clone, PartialEq, Eq)]
5799pub struct GrantPriv {
5800 pub word: String,
5801 pub columns: Vec<String>,
5802}
5803
5804/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
5805/// privileges; every other object class parses and is accepted as a no-op, so
5806/// a pg_dump that grants on schemas / sequences / functions still restores.
5807#[derive(Debug, Clone, PartialEq, Eq)]
5808pub enum GrantObject {
5809 /// `ON [TABLE] a, b` — the enforced case.
5810 Tables(Vec<String>),
5811 /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
5812 /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
5813 /// granted roles; the grantees are the members.
5814 Roles(Vec<String>),
5815 /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
5816 /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
5817 Sequences(Vec<String>),
5818 /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
5819 Schemas(Vec<String>),
5820 /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
5821 Databases(Vec<String>),
5822 /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
5823 /// (SPG keys functions by name); the argument list parses and is dropped.
5824 Functions(Vec<(String, Option<Vec<String>>)>),
5825 /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
5826 /// every table at GRANT time, exactly like PG.
5827 AllTablesInSchema,
5828 /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
5829 /// message.
5830 Other(String),
5831}
5832
5833impl GrantStatement {
5834 /// Round-trip text. `grant = false` renders the REVOKE form.
5835 fn render(&self, grant: bool) -> alloc::string::String {
5836 use core::fmt::Write as _;
5837 let mut s = alloc::string::String::new();
5838 let privs = if self.privileges.is_empty() {
5839 alloc::string::String::from("ALL")
5840 } else {
5841 let parts: Vec<_> = self
5842 .privileges
5843 .iter()
5844 .map(|p| {
5845 if p.columns.is_empty() {
5846 p.word.clone()
5847 } else {
5848 let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
5849 alloc::format!("{} ({})", p.word, cols.join(", "))
5850 }
5851 })
5852 .collect();
5853 parts.join(", ")
5854 };
5855 let obj = match &self.object {
5856 GrantObject::Tables(t) => {
5857 let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
5858 alloc::format!("TABLE {}", names.join(", "))
5859 }
5860 GrantObject::Roles(r) => {
5861 let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
5862 names.join(", ")
5863 }
5864 GrantObject::Sequences(n) => {
5865 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5866 alloc::format!("SEQUENCE {}", names.join(", "))
5867 }
5868 GrantObject::Schemas(n) => {
5869 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5870 alloc::format!("SCHEMA {}", names.join(", "))
5871 }
5872 GrantObject::Databases(n) => {
5873 let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5874 alloc::format!("DATABASE {}", names.join(", "))
5875 }
5876 GrantObject::Functions(n) => {
5877 let names: Vec<_> = n
5878 .iter()
5879 .map(|(name, args)| match args {
5880 Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
5881 None => quote_ident(name),
5882 })
5883 .collect();
5884 alloc::format!("FUNCTION {}", names.join(", "))
5885 }
5886 GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
5887 GrantObject::Other(k) => k.clone(),
5888 };
5889 let who: Vec<_> = self
5890 .grantees
5891 .iter()
5892 .map(|g| {
5893 if g.is_empty() {
5894 "PUBLIC".into()
5895 } else {
5896 quote_ident(g)
5897 }
5898 })
5899 .collect();
5900 if let GrantObject::Roles(_) = &self.object {
5901 let _ = if grant {
5902 write!(s, "GRANT {obj} TO {}", who.join(", "))
5903 } else {
5904 write!(s, "REVOKE {obj} FROM {}", who.join(", "))
5905 };
5906 return s;
5907 }
5908 if grant {
5909 let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
5910 if self.grant_option {
5911 s.push_str(" WITH GRANT OPTION");
5912 }
5913 } else {
5914 s.push_str("REVOKE ");
5915 if self.grant_option {
5916 s.push_str("GRANT OPTION FOR ");
5917 }
5918 let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
5919 }
5920 s
5921 }
5922}
5923
5924impl fmt::Display for Statement {
5925 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5926 match self {
5927 Self::Empty => Ok(()),
5928 // v7.39 (round 695) — deparsed the way PG writes it.
5929 // v7.39 (round 696) — never deparsed into a dump (nothing is
5930 // stored), so the shortest faithful spelling of what it was.
5931 Self::DropAggregate { if_exists, items } => {
5932 f.write_str("DROP AGGREGATE ")?;
5933 if *if_exists {
5934 f.write_str("IF EXISTS ")?;
5935 }
5936 for (i, (name, args)) in items.iter().enumerate() {
5937 if i > 0 {
5938 f.write_str(", ")?;
5939 }
5940 match args {
5941 Some(a) => write!(f, "{name}({})", a.join(", "))?,
5942 None => write!(f, "{name}(*)")?,
5943 }
5944 }
5945 Ok(())
5946 }
5947 Self::AlterRolePassword { name, password } => {
5948 write!(f, "ALTER ROLE {}", quote_ident(name))?;
5949 match password {
5950 Some(_) => f.write_str(" PASSWORD '<redacted>'"),
5951 None => f.write_str(" PASSWORD NULL"),
5952 }
5953 }
5954 Self::ValidateOnly { kind, names } => match kind {
5955 ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
5956 ValidateOnlyKind::RoleName => {
5957 write!(f, "DROP OWNED BY {}", names.join(", "))
5958 }
5959 ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
5960 ValidateOnlyKind::ExtensionAvailable => {
5961 write!(f, "CREATE EXTENSION {}", names.join(", "))
5962 }
5963 ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
5964 ValidateOnlyKind::CollationName => {
5965 write!(f, "DROP COLLATION {}", names.join(", "))
5966 }
5967 ValidateOnlyKind::TsConfigName => {
5968 write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
5969 }
5970 ValidateOnlyKind::EventTriggerName => {
5971 write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
5972 }
5973 ValidateOnlyKind::TablespaceName => {
5974 write!(f, "DROP TABLESPACE {}", names.join(", "))
5975 }
5976 ValidateOnlyKind::LargeObjectOid => {
5977 write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
5978 }
5979 ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
5980 ValidateOnlyKind::AggregateName => {
5981 write!(f, "ALTER AGGREGATE {}", names.join(", "))
5982 }
5983 ValidateOnlyKind::ConversionName => {
5984 write!(f, "DROP CONVERSION {}", names.join(", "))
5985 }
5986 ValidateOnlyKind::LanguageName => {
5987 write!(f, "DROP LANGUAGE {}", names.join(", "))
5988 }
5989 ValidateOnlyKind::ExtensionInstalled => {
5990 write!(f, "DROP EXTENSION {}", names.join(", "))
5991 }
5992 },
5993 Self::AlterSystem { parameter } => match parameter {
5994 Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
5995 None => f.write_str("ALTER SYSTEM RESET ALL"),
5996 },
5997 // v7.39 (round 547) — round-trips as PG writes it.
5998 Self::SetDbRoleSetting(st) => {
5999 match (&st.database, &st.role) {
6000 (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
6001 (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
6002 (None, None) => f.write_str("ALTER ROLE ALL")?,
6003 }
6004 if let (Some(d), Some(_)) = (&st.database, &st.role) {
6005 write!(f, " IN DATABASE {d}")?;
6006 }
6007 match (&st.param, &st.value) {
6008 (None, _) => f.write_str(" RESET ALL"),
6009 (Some(p), None) => write!(f, " RESET {p}"),
6010 (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
6011 }
6012 }
6013 Self::Maintain {
6014 kind,
6015 concurrently,
6016 target,
6017 } => {
6018 f.write_str(match kind {
6019 crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
6020 _ => "REINDEX ",
6021 })?;
6022 if *concurrently {
6023 f.write_str("CONCURRENTLY ")?;
6024 }
6025 if let Some(t) = target {
6026 f.write_str(t)?;
6027 }
6028 Ok(())
6029 }
6030 Self::DropDatabase { name, if_exists } => {
6031 f.write_str("DROP DATABASE ")?;
6032 if *if_exists {
6033 f.write_str("IF EXISTS ")?;
6034 }
6035 f.write_str(name)
6036 }
6037 Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
6038 Self::SetConstraints { names, deferred } => {
6039 f.write_str("SET CONSTRAINTS ")?;
6040 if names.is_empty() {
6041 f.write_str("ALL")?;
6042 } else {
6043 for (i, n) in names.iter().enumerate() {
6044 if i > 0 {
6045 f.write_str(", ")?;
6046 }
6047 f.write_str(n)?;
6048 }
6049 }
6050 f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
6051 }
6052 // v7.39 (round 277) — the source text is kept verbatim so
6053 // `pg_prepared_statements.statement` can report it the way
6054 // PG does (the whole PREPARE statement, not just the body).
6055 Self::Prepare { source, .. } => f.write_str(source),
6056 Self::Execute { name, args } => {
6057 write!(f, "EXECUTE {}", quote_ident(name))?;
6058 if !args.is_empty() {
6059 f.write_str("(")?;
6060 for (i, a) in args.iter().enumerate() {
6061 if i > 0 {
6062 f.write_str(", ")?;
6063 }
6064 write!(f, "{a}")?;
6065 }
6066 f.write_str(")")?;
6067 }
6068 Ok(())
6069 }
6070 Self::CreateStatistics {
6071 name,
6072 if_not_exists,
6073 kinds,
6074 columns,
6075 table,
6076 } => {
6077 f.write_str("CREATE STATISTICS ")?;
6078 if *if_not_exists {
6079 f.write_str("IF NOT EXISTS ")?;
6080 }
6081 write!(f, "{}", quote_ident(name))?;
6082 if !kinds.is_empty() {
6083 write!(f, " ({})", kinds.join(", "))?;
6084 }
6085 write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
6086 }
6087 Self::DropStatistics { name, if_exists } => {
6088 f.write_str("DROP STATISTICS ")?;
6089 if *if_exists {
6090 f.write_str("IF EXISTS ")?;
6091 }
6092 write!(f, "{}", quote_ident(name))
6093 }
6094 Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
6095 Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
6096 Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
6097 Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
6098 Self::DeclareCursor {
6099 name,
6100 scroll,
6101 hold,
6102 query,
6103 } => {
6104 write!(f, "DECLARE {} ", quote_ident(name))?;
6105 match scroll {
6106 Some(true) => f.write_str("SCROLL ")?,
6107 Some(false) => f.write_str("NO SCROLL ")?,
6108 None => {}
6109 }
6110 f.write_str("CURSOR ")?;
6111 if *hold {
6112 f.write_str("WITH HOLD ")?;
6113 }
6114 write!(f, "FOR {query}")
6115 }
6116 Self::FetchCursor { name, direction } => {
6117 write!(f, "FETCH {direction} FROM {}", quote_ident(name))
6118 }
6119 Self::MoveCursor { name, direction } => {
6120 write!(f, "MOVE {direction} FROM {}", quote_ident(name))
6121 }
6122 Self::CloseCursor { name } => match name {
6123 Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
6124 None => f.write_str("CLOSE ALL"),
6125 },
6126 Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
6127 Self::Notify { channel, payload } => {
6128 write!(f, "NOTIFY {}", quote_ident(channel))?;
6129 if let Some(p) = payload {
6130 write!(f, ", '{}'", p.replace('\'', "''"))?;
6131 }
6132 Ok(())
6133 }
6134 Self::Unlisten(ch) => match ch {
6135 Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
6136 None => f.write_str("UNLISTEN *"),
6137 },
6138 Self::CopyTo {
6139 table,
6140 columns,
6141 query,
6142 options,
6143 } => {
6144 if let Some(q) = query {
6145 write!(f, "COPY ({q})")?;
6146 } else {
6147 write!(f, "COPY {table}")?;
6148 if let Some(cols) = columns {
6149 write!(f, " ({})", cols.join(", "))?;
6150 }
6151 }
6152 write!(f, " TO STDOUT")?;
6153 let mut parts: Vec<String> = Vec::new();
6154 if options.format == CopyFormat::Csv {
6155 parts.push("FORMAT csv".to_string());
6156 }
6157 if options.header {
6158 parts.push("HEADER true".to_string());
6159 }
6160 if let Some(d) = options.delimiter {
6161 parts.push(alloc::format!("DELIMITER '{d}'"));
6162 }
6163 if let Some(n) = &options.null_str {
6164 parts.push(alloc::format!("NULL '{n}'"));
6165 }
6166 if let Some(q) = options.quote {
6167 parts.push(alloc::format!("QUOTE '{q}'"));
6168 }
6169 if !parts.is_empty() {
6170 write!(f, " WITH ({})", parts.join(", "))?;
6171 }
6172 Ok(())
6173 }
6174 Self::CopyFromFile {
6175 table,
6176 columns,
6177 path,
6178 options,
6179 } => {
6180 write!(f, "COPY {table}")?;
6181 if let Some(cols) = columns {
6182 write!(f, " ({})", cols.join(", "))?;
6183 }
6184 write!(f, " FROM '{path}'")?;
6185 let mut parts: Vec<String> = Vec::new();
6186 if options.format == CopyFormat::Csv {
6187 parts.push("FORMAT csv".to_string());
6188 }
6189 if options.header {
6190 parts.push("HEADER true".to_string());
6191 }
6192 if let Some(d) = options.delimiter {
6193 parts.push(alloc::format!("DELIMITER '{d}'"));
6194 }
6195 if let Some(n) = &options.null_str {
6196 parts.push(alloc::format!("NULL '{n}'"));
6197 }
6198 if let Some(q) = options.quote {
6199 parts.push(alloc::format!("QUOTE '{q}'"));
6200 }
6201 if !parts.is_empty() {
6202 write!(f, " WITH ({})", parts.join(", "))?;
6203 }
6204 Ok(())
6205 }
6206 Self::CopyToFile {
6207 table,
6208 columns,
6209 query,
6210 path,
6211 options,
6212 } => {
6213 if let Some(q) = query {
6214 write!(f, "COPY ({q})")?;
6215 } else {
6216 write!(f, "COPY {table}")?;
6217 if let Some(cols) = columns {
6218 write!(f, " ({})", cols.join(", "))?;
6219 }
6220 }
6221 write!(f, " TO '{path}'")?;
6222 let mut parts: Vec<String> = Vec::new();
6223 if options.format == CopyFormat::Csv {
6224 parts.push("FORMAT csv".to_string());
6225 }
6226 if options.header {
6227 parts.push("HEADER true".to_string());
6228 }
6229 if let Some(d) = options.delimiter {
6230 parts.push(alloc::format!("DELIMITER '{d}'"));
6231 }
6232 if let Some(n) = &options.null_str {
6233 parts.push(alloc::format!("NULL '{n}'"));
6234 }
6235 if let Some(q) = options.quote {
6236 parts.push(alloc::format!("QUOTE '{q}'"));
6237 }
6238 if !parts.is_empty() {
6239 write!(f, " WITH ({})", parts.join(", "))?;
6240 }
6241 Ok(())
6242 }
6243 Self::AlterDomain { name, action } => {
6244 write!(f, "ALTER DOMAIN {name} ")?;
6245 match action {
6246 AlterDomainAction::AddConstraint { name: cn, check } => match cn {
6247 Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
6248 None => write!(f, "ADD CHECK ({check})"),
6249 },
6250 AlterDomainAction::DropConstraint {
6251 name: cn,
6252 if_exists,
6253 } => {
6254 if *if_exists {
6255 write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
6256 } else {
6257 write!(f, "DROP CONSTRAINT {cn}")
6258 }
6259 }
6260 AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
6261 AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
6262 AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
6263 AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
6264 AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
6265 }
6266 }
6267 Self::Truncate {
6268 tables,
6269 restart_identity,
6270 cascade,
6271 only,
6272 } => {
6273 f.write_str("TRUNCATE TABLE ")?;
6274 if *only {
6275 f.write_str("ONLY ")?;
6276 }
6277 for (i, t) in tables.iter().enumerate() {
6278 if i > 0 {
6279 f.write_str(", ")?;
6280 }
6281 f.write_str(t)?;
6282 }
6283 if *restart_identity {
6284 f.write_str(" RESTART IDENTITY")?;
6285 }
6286 if *cascade {
6287 f.write_str(" CASCADE")?;
6288 }
6289 Ok(())
6290 }
6291 Self::DropTable { names, if_exists } => {
6292 f.write_str("DROP TABLE ")?;
6293 if *if_exists {
6294 f.write_str("IF EXISTS ")?;
6295 }
6296 for (i, n) in names.iter().enumerate() {
6297 if i > 0 {
6298 f.write_str(", ")?;
6299 }
6300 write!(f, "{}", quote_ident(n))?;
6301 }
6302 Ok(())
6303 }
6304 Self::DropIndex {
6305 name,
6306 if_exists,
6307 table,
6308 } => {
6309 f.write_str("DROP INDEX ")?;
6310 if *if_exists {
6311 f.write_str("IF EXISTS ")?;
6312 }
6313 write!(f, "{}", quote_ident(name))?;
6314 if let Some(t) = table {
6315 write!(f, " ON {}", quote_ident(t))?;
6316 }
6317 Ok(())
6318 }
6319 Self::Select(s) => s.fmt(f),
6320 Self::CreateTable(s) => s.fmt(f),
6321 Self::CreateIndex(s) => s.fmt(f),
6322 Self::Insert(s) => s.fmt(f),
6323 Self::Update(s) => s.fmt(f),
6324 Self::Delete(s) => s.fmt(f),
6325 Self::Merge(s) => s.fmt(f),
6326 Self::Vacuum { table, analyze } => {
6327 f.write_str("VACUUM")?;
6328 if *analyze {
6329 f.write_str(" ANALYZE")?;
6330 }
6331 if let Some(t) = table {
6332 write!(f, " {}", quote_ident(t))?;
6333 }
6334 Ok(())
6335 }
6336 Self::Begin(modes) => {
6337 f.write_str("BEGIN")?;
6338 if let Some(level) = modes.isolation {
6339 write!(f, " ISOLATION LEVEL {level}")?;
6340 }
6341 match modes.read_only {
6342 Some(true) => f.write_str(" READ ONLY")?,
6343 Some(false) => f.write_str(" READ WRITE")?,
6344 None => {}
6345 }
6346 Ok(())
6347 }
6348 Self::Commit => f.write_str("COMMIT"),
6349 Self::Rollback => f.write_str("ROLLBACK"),
6350 Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
6351 Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
6352 Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
6353 Self::ShowTables => f.write_str("SHOW TABLES"),
6354 Self::ShowDatabases => f.write_str("SHOW DATABASES"),
6355 Self::UseDatabase(n) => write!(f, "USE {}", quote_ident(n)),
6356 Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
6357 Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
6358 Self::ShowStatus => f.write_str("SHOW STATUS"),
6359 Self::ShowVariables => f.write_str("SHOW VARIABLES"),
6360 Self::ShowVariablesLike(p) => {
6361 write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
6362 }
6363 Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
6364 Self::Discard(t) => write!(f, "DISCARD {t}"),
6365 Self::Kill { query_only, id } => {
6366 if *query_only {
6367 write!(f, "KILL QUERY {id}")
6368 } else {
6369 write!(f, "KILL CONNECTION {id}")
6370 }
6371 }
6372 Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
6373 Self::CreateUser(s) => write!(
6374 f,
6375 "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
6376 quote_ident(&s.name),
6377 s.role
6378 ),
6379 Self::DropUser { name, if_exists } => {
6380 let ie = if *if_exists { "IF EXISTS " } else { "" };
6381 write!(f, "DROP USER {ie}{}", quote_ident(name))
6382 }
6383 Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
6384 Self::SetRole(None) => f.write_str("RESET ROLE"),
6385 Self::Grant(g) => write!(f, "{}", g.render(true)),
6386 Self::Revoke(g) => write!(f, "{}", g.render(false)),
6387 Self::CreatePolicy(s) => {
6388 write!(
6389 f,
6390 "CREATE POLICY {} ON {}",
6391 quote_ident(&s.name),
6392 quote_ident(&s.table)
6393 )?;
6394 if !s.permissive {
6395 f.write_str(" AS RESTRICTIVE")?;
6396 }
6397 if !matches!(s.cmd, PolicyCmd::All) {
6398 let w = match s.cmd {
6399 PolicyCmd::Select => "SELECT",
6400 PolicyCmd::Insert => "INSERT",
6401 PolicyCmd::Update => "UPDATE",
6402 PolicyCmd::Delete => "DELETE",
6403 PolicyCmd::All => unreachable!(),
6404 };
6405 write!(f, " FOR {w}")?;
6406 }
6407 if !s.roles.is_empty() {
6408 write!(f, " TO {}", s.roles.join(", "))?;
6409 }
6410 if let Some(u) = &s.using {
6411 write!(f, " USING ({u})")?;
6412 }
6413 if let Some(c) = &s.with_check {
6414 write!(f, " WITH CHECK ({c})")?;
6415 }
6416 Ok(())
6417 }
6418 Self::AlterPolicy(s) => {
6419 write!(
6420 f,
6421 "ALTER POLICY {} ON {}",
6422 quote_ident(&s.name),
6423 quote_ident(&s.table)
6424 )?;
6425 if let Some(nn) = &s.rename_to {
6426 return write!(f, " RENAME TO {}", quote_ident(nn));
6427 }
6428 if let Some(roles) = &s.roles {
6429 write!(f, " TO {}", roles.join(", "))?;
6430 }
6431 if let Some(u) = &s.using {
6432 write!(f, " USING ({u})")?;
6433 }
6434 if let Some(c) = &s.with_check {
6435 write!(f, " WITH CHECK ({c})")?;
6436 }
6437 Ok(())
6438 }
6439 Self::DropPolicy(s) => {
6440 f.write_str("DROP POLICY ")?;
6441 if s.if_exists {
6442 f.write_str("IF EXISTS ")?;
6443 }
6444 write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
6445 }
6446 Self::ShowUsers => f.write_str("SHOW USERS"),
6447 Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
6448 Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
6449 Self::CreateSubscription(s) => {
6450 write!(
6451 f,
6452 "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
6453 quote_ident(&s.name),
6454 s.conn_str.replace('\'', "''")
6455 )?;
6456 for (i, p) in s.publications.iter().enumerate() {
6457 if i > 0 {
6458 f.write_str(", ")?;
6459 }
6460 write!(f, "{}", quote_ident(p))?;
6461 }
6462 Ok(())
6463 }
6464 Self::DropSubscription { name, if_exists } => {
6465 let opt = if *if_exists { "IF EXISTS " } else { "" };
6466 write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
6467 }
6468 Self::WaitForWalPosition { pos, timeout_ms } => {
6469 write!(f, "WAIT FOR WAL POSITION {pos}")?;
6470 if let Some(ms) = timeout_ms {
6471 write!(f, " WITH TIMEOUT {ms}")?;
6472 }
6473 Ok(())
6474 }
6475 Self::RenameTables(pairs) => {
6476 f.write_str("RENAME TABLE ")?;
6477 for (i, (from, to)) in pairs.iter().enumerate() {
6478 if i > 0 {
6479 f.write_str(", ")?;
6480 }
6481 write!(f, "{} TO {}", quote_ident(from), quote_ident(to))?;
6482 }
6483 Ok(())
6484 }
6485 Self::Analyze(None) => f.write_str("ANALYZE"),
6486 Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
6487 Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
6488 Self::Explain(e) => {
6489 if e.suggest {
6490 write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
6491 } else if e.analyze {
6492 write!(f, "EXPLAIN ANALYZE {}", e.inner)
6493 } else {
6494 write!(f, "EXPLAIN {}", e.inner)
6495 }
6496 }
6497 Self::AlterIndex(a) => {
6498 write!(f, "ALTER INDEX ")?;
6499 match &a.target {
6500 // Parameters are consumed, not stored; the shortest
6501 // faithful spelling.
6502 AlterIndexTarget::StorageParams => {
6503 write!(f, "{} SET ()", quote_ident(&a.name))
6504 }
6505 AlterIndexTarget::Rebuild { encoding } => {
6506 write!(f, "{} REBUILD", quote_ident(&a.name))?;
6507 if let Some(enc) = encoding {
6508 write!(f, " WITH (encoding = {enc})")?;
6509 }
6510 Ok(())
6511 }
6512 AlterIndexTarget::Rename { new, if_exists } => {
6513 if *if_exists {
6514 f.write_str("IF EXISTS ")?;
6515 }
6516 write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
6517 }
6518 }
6519 }
6520 Self::AlterTable(a) => {
6521 write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
6522 for (i, t) in a.targets.iter().enumerate() {
6523 if i > 0 {
6524 f.write_str(", ")?;
6525 }
6526 fmt_alter_target(f, t)?;
6527 }
6528 Ok(())
6529 }
6530 Self::CreatePublication(p) => {
6531 write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
6532 match &p.scope {
6533 PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
6534 PublicationScope::ForTables(ts) => {
6535 f.write_str(" FOR TABLE ")?;
6536 for (i, t) in ts.iter().enumerate() {
6537 if i > 0 {
6538 f.write_str(", ")?;
6539 }
6540 write!(f, "{}", quote_ident(t))?;
6541 }
6542 Ok(())
6543 }
6544 PublicationScope::TablesInSchema(schema) => {
6545 write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
6546 Ok(())
6547 }
6548 PublicationScope::AllTablesExcept(ts) => {
6549 f.write_str(" FOR ALL TABLES EXCEPT ")?;
6550 for (i, t) in ts.iter().enumerate() {
6551 if i > 0 {
6552 f.write_str(", ")?;
6553 }
6554 write!(f, "{}", quote_ident(t))?;
6555 }
6556 Ok(())
6557 }
6558 }
6559 }
6560 Self::CreateExtension(name) => {
6561 write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
6562 }
6563 Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
6564 Self::DropPublication { name, if_exists } => {
6565 let opt = if *if_exists { "IF EXISTS " } else { "" };
6566 write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
6567 }
6568 Self::SetParameter { name, value, local } => {
6569 write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
6570 match value {
6571 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
6572 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
6573 SetValue::Default => f.write_str("DEFAULT"),
6574 }
6575 }
6576 Self::SetTransaction { modes } => {
6577 f.write_str("SET TRANSACTION")?;
6578 if let Some(isolation) = modes.isolation {
6579 let name = match isolation {
6580 IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
6581 IsolationLevel::ReadCommitted => "READ COMMITTED",
6582 IsolationLevel::RepeatableRead => "REPEATABLE READ",
6583 IsolationLevel::Serializable => "SERIALIZABLE",
6584 };
6585 write!(f, " ISOLATION LEVEL {name}")?;
6586 }
6587 match modes.read_only {
6588 Some(true) => f.write_str(" READ ONLY")?,
6589 Some(false) => f.write_str(" READ WRITE")?,
6590 None => {}
6591 }
6592 Ok(())
6593 }
6594 Self::ShowParameter(name) => write!(f, "SHOW {name}"),
6595 Self::SetUserVars(assigns, _) => {
6596 f.write_str("SET ")?;
6597 for (i, (name, value)) in assigns.iter().enumerate() {
6598 if i > 0 {
6599 f.write_str(", ")?;
6600 }
6601 write!(f, "@{name} = {value}")?;
6602 }
6603 Ok(())
6604 }
6605 Self::SetParameterList(pairs) => {
6606 f.write_str("SET ")?;
6607 for (i, (name, value)) in pairs.iter().enumerate() {
6608 if i > 0 {
6609 f.write_str(", ")?;
6610 }
6611 write!(f, "{name} = ")?;
6612 match value {
6613 SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
6614 SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
6615 SetValue::Default => f.write_str("DEFAULT")?,
6616 }
6617 }
6618 Ok(())
6619 }
6620 Self::ResetParameter(None) => f.write_str("RESET ALL"),
6621 Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
6622 Self::CreateFunction(s) => s.fmt(f),
6623 Self::CreateTrigger(s) => s.fmt(f),
6624 Self::DropTrigger {
6625 name,
6626 table,
6627 if_exists,
6628 } => {
6629 f.write_str("DROP TRIGGER ")?;
6630 if *if_exists {
6631 f.write_str("IF EXISTS ")?;
6632 }
6633 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6634 }
6635 Self::DropFunction {
6636 name,
6637 args,
6638 if_exists,
6639 } => {
6640 f.write_str("DROP FUNCTION ")?;
6641 if *if_exists {
6642 f.write_str("IF EXISTS ")?;
6643 }
6644 write!(f, "{}", quote_ident(name))?;
6645 if let Some(a) = args {
6646 write!(f, "({})", a.join(", "))?;
6647 }
6648 Ok(())
6649 }
6650 Self::CreateSequence(s) => s.fmt(f),
6651 Self::AlterSequence(s) => s.fmt(f),
6652 Self::DropSequence { names, if_exists } => {
6653 f.write_str("DROP SEQUENCE ")?;
6654 if *if_exists {
6655 f.write_str("IF EXISTS ")?;
6656 }
6657 for (i, n) in names.iter().enumerate() {
6658 if i > 0 {
6659 f.write_str(", ")?;
6660 }
6661 write!(f, "{}", quote_ident(n))?;
6662 }
6663 Ok(())
6664 }
6665 Self::CreateView(v) => v.fmt(f),
6666 Self::DropView { names, if_exists } => {
6667 f.write_str("DROP VIEW ")?;
6668 if *if_exists {
6669 f.write_str("IF EXISTS ")?;
6670 }
6671 for (i, n) in names.iter().enumerate() {
6672 if i > 0 {
6673 f.write_str(", ")?;
6674 }
6675 write!(f, "{}", quote_ident(n))?;
6676 }
6677 Ok(())
6678 }
6679 Self::CreateMaterializedView(v) => v.fmt(f),
6680 Self::RefreshMaterializedView { name, with_data } => {
6681 write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6682 if !*with_data {
6683 f.write_str(" WITH NO DATA")?;
6684 }
6685 Ok(())
6686 }
6687 Self::DropMaterializedView { names, if_exists } => {
6688 f.write_str("DROP MATERIALIZED VIEW ")?;
6689 if *if_exists {
6690 f.write_str("IF EXISTS ")?;
6691 }
6692 for (i, n) in names.iter().enumerate() {
6693 if i > 0 {
6694 f.write_str(", ")?;
6695 }
6696 write!(f, "{}", quote_ident(n))?;
6697 }
6698 Ok(())
6699 }
6700 Self::CreateType(t) => t.fmt(f),
6701 Self::CommentOn {
6702 kind,
6703 name,
6704 comment,
6705 } => {
6706 let body = match comment {
6707 Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6708 None => "NULL".into(),
6709 };
6710 write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6711 }
6712 Self::AlterTypeRenameValue {
6713 type_name,
6714 old,
6715 new,
6716 } => write!(
6717 f,
6718 "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6719 quote_ident(type_name),
6720 old.replace('\'', "''"),
6721 new.replace('\'', "''")
6722 ),
6723 Self::AlterTypeAddValue {
6724 type_name,
6725 label,
6726 if_not_exists,
6727 position,
6728 } => {
6729 write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6730 if *if_not_exists {
6731 write!(f, "IF NOT EXISTS ")?;
6732 }
6733 write!(f, "'{label}'")?;
6734 if let Some((is_before, anchor)) = position {
6735 write!(
6736 f,
6737 " {} '{anchor}'",
6738 if *is_before { "BEFORE" } else { "AFTER" }
6739 )?;
6740 }
6741 Ok(())
6742 }
6743 Self::DropType { names, if_exists } => {
6744 f.write_str("DROP TYPE ")?;
6745 if *if_exists {
6746 f.write_str("IF EXISTS ")?;
6747 }
6748 for (i, n) in names.iter().enumerate() {
6749 if i > 0 {
6750 f.write_str(", ")?;
6751 }
6752 write!(f, "{}", quote_ident(n))?;
6753 }
6754 Ok(())
6755 }
6756 Self::CreateDomain(d) => d.fmt(f),
6757 Self::DropDomain { names, if_exists } => {
6758 f.write_str("DROP DOMAIN ")?;
6759 if *if_exists {
6760 f.write_str("IF EXISTS ")?;
6761 }
6762 for (i, n) in names.iter().enumerate() {
6763 if i > 0 {
6764 f.write_str(", ")?;
6765 }
6766 write!(f, "{}", quote_ident(n))?;
6767 }
6768 Ok(())
6769 }
6770 Self::CreateSchema {
6771 name,
6772 if_not_exists,
6773 } => {
6774 f.write_str("CREATE SCHEMA ")?;
6775 if *if_not_exists {
6776 f.write_str("IF NOT EXISTS ")?;
6777 }
6778 write!(f, "{}", quote_ident(name))
6779 }
6780 Self::DropSchema { names, if_exists } => {
6781 f.write_str("DROP SCHEMA ")?;
6782 if *if_exists {
6783 f.write_str("IF EXISTS ")?;
6784 }
6785 for (i, n) in names.iter().enumerate() {
6786 if i > 0 {
6787 f.write_str(", ")?;
6788 }
6789 write!(f, "{}", quote_ident(n))?;
6790 }
6791 Ok(())
6792 }
6793 Self::CreateRule(r) => {
6794 f.write_str("CREATE ")?;
6795 if r.or_replace {
6796 f.write_str("OR REPLACE ")?;
6797 }
6798 write!(
6799 f,
6800 "RULE {} AS ON {} TO {}",
6801 quote_ident(&r.name),
6802 r.event,
6803 quote_ident(&r.table)
6804 )?;
6805 if let Some(w) = &r.when_condition {
6806 write!(f, " WHERE {w}")?;
6807 }
6808 f.write_str(if r.instead {
6809 " DO INSTEAD "
6810 } else {
6811 " DO ALSO "
6812 })?;
6813 if r.commands.is_empty() {
6814 f.write_str("NOTHING")?;
6815 } else if r.commands.len() == 1 {
6816 write!(f, "{}", r.commands[0])?;
6817 } else {
6818 f.write_str("(")?;
6819 for (i, c) in r.commands.iter().enumerate() {
6820 if i > 0 {
6821 f.write_str("; ")?;
6822 }
6823 write!(f, "{c}")?;
6824 }
6825 f.write_str(")")?;
6826 }
6827 Ok(())
6828 }
6829 Self::DropRule {
6830 name,
6831 table,
6832 if_exists,
6833 } => {
6834 f.write_str("DROP RULE ")?;
6835 if *if_exists {
6836 f.write_str("IF EXISTS ")?;
6837 }
6838 write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6839 }
6840 }
6841 }
6842}
6843
6844impl fmt::Display for CreateDomainStatement {
6845 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6846 write!(
6847 f,
6848 "CREATE DOMAIN {} AS {}",
6849 quote_ident(&self.name),
6850 self.base_type
6851 )?;
6852 if let Some(d) = &self.default {
6853 write!(f, " DEFAULT {d}")?;
6854 }
6855 if self.not_null {
6856 f.write_str(" NOT NULL")?;
6857 }
6858 for c in &self.checks {
6859 write!(f, " CHECK ({c})")?;
6860 }
6861 Ok(())
6862 }
6863}
6864
6865impl fmt::Display for CreateTypeStatement {
6866 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6867 write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
6868 match &self.kind {
6869 TypeKind::Enum { labels } => {
6870 f.write_str("ENUM (")?;
6871 for (i, l) in labels.iter().enumerate() {
6872 if i > 0 {
6873 f.write_str(", ")?;
6874 }
6875 write!(f, "'{}'", l.replace('\'', "''"))?;
6876 }
6877 f.write_str(")")
6878 }
6879 TypeKind::Composite { fields, .. } => {
6880 f.write_str("(")?;
6881 for (i, (n, t)) in fields.iter().enumerate() {
6882 if i > 0 {
6883 f.write_str(", ")?;
6884 }
6885 write!(f, "{} {}", quote_ident(n), t)?;
6886 }
6887 f.write_str(")")
6888 }
6889 }
6890 }
6891}
6892
6893impl fmt::Display for CreateMaterializedViewStatement {
6894 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6895 f.write_str("CREATE MATERIALIZED VIEW ")?;
6896 if self.if_not_exists {
6897 f.write_str("IF NOT EXISTS ")?;
6898 }
6899 write!(f, "{}", quote_ident(&self.name))?;
6900 if !self.columns.is_empty() {
6901 f.write_str(" (")?;
6902 for (i, c) in self.columns.iter().enumerate() {
6903 if i > 0 {
6904 f.write_str(", ")?;
6905 }
6906 write!(f, "{}", quote_ident(c))?;
6907 }
6908 f.write_str(")")?;
6909 }
6910 write!(f, " AS {}", self.body)?;
6911 if !self.with_data {
6912 f.write_str(" WITH NO DATA")?;
6913 }
6914 Ok(())
6915 }
6916}
6917
6918impl fmt::Display for CreateViewStatement {
6919 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6920 f.write_str("CREATE ")?;
6921 if self.or_replace {
6922 f.write_str("OR REPLACE ")?;
6923 }
6924 if self.temporary {
6925 f.write_str("TEMPORARY ")?;
6926 }
6927 f.write_str("VIEW ")?;
6928 if self.if_not_exists {
6929 f.write_str("IF NOT EXISTS ")?;
6930 }
6931 write!(f, "{}", quote_ident(&self.name))?;
6932 if !self.columns.is_empty() {
6933 f.write_str(" (")?;
6934 for (i, c) in self.columns.iter().enumerate() {
6935 if i > 0 {
6936 f.write_str(", ")?;
6937 }
6938 write!(f, "{}", quote_ident(c))?;
6939 }
6940 f.write_str(")")?;
6941 }
6942 write!(f, " AS {}", self.body)?;
6943 match self.check_option {
6944 Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
6945 Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
6946 None => Ok(()),
6947 }
6948 }
6949}
6950
6951impl fmt::Display for CreateSequenceStatement {
6952 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6953 f.write_str("CREATE ")?;
6954 if self.temporary {
6955 f.write_str("TEMPORARY ")?;
6956 }
6957 f.write_str("SEQUENCE ")?;
6958 if self.if_not_exists {
6959 f.write_str("IF NOT EXISTS ")?;
6960 }
6961 write!(f, "{}", quote_ident(&self.name))?;
6962 if let Some(dt) = self.data_type {
6963 write!(f, " AS {dt}")?;
6964 }
6965 write_sequence_options(f, &self.options)
6966 }
6967}
6968
6969impl fmt::Display for AlterSequenceStatement {
6970 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6971 f.write_str("ALTER SEQUENCE ")?;
6972 if self.if_exists {
6973 f.write_str("IF EXISTS ")?;
6974 }
6975 write!(f, "{}", quote_ident(&self.name))?;
6976 write_sequence_options(f, &self.options)
6977 }
6978}
6979
6980impl fmt::Display for SequenceDataType {
6981 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6982 f.write_str(match self {
6983 Self::SmallInt => "smallint",
6984 Self::Int => "integer",
6985 Self::BigInt => "bigint",
6986 })
6987 }
6988}
6989
6990fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
6991 if let Some(n) = o.increment {
6992 write!(f, " INCREMENT BY {n}")?;
6993 }
6994 match o.min_value {
6995 Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
6996 Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
6997 None => {}
6998 }
6999 match o.max_value {
7000 Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
7001 Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
7002 None => {}
7003 }
7004 if let Some(n) = o.start {
7005 write!(f, " START WITH {n}")?;
7006 }
7007 match o.restart {
7008 Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
7009 Some(None) => f.write_str(" RESTART")?,
7010 None => {}
7011 }
7012 if let Some(n) = o.cache {
7013 write!(f, " CACHE {n}")?;
7014 }
7015 match o.cycle {
7016 Some(true) => f.write_str(" CYCLE")?,
7017 Some(false) => f.write_str(" NO CYCLE")?,
7018 None => {}
7019 }
7020 if let Some(ob) = &o.owned_by {
7021 match ob {
7022 SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
7023 SequenceOwnedBy::Column { table, column } => {
7024 write!(
7025 f,
7026 " OWNED BY {}.{}",
7027 quote_ident(table),
7028 quote_ident(column)
7029 )?;
7030 }
7031 }
7032 }
7033 Ok(())
7034}
7035
7036impl fmt::Display for CreateFunctionStatement {
7037 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7038 f.write_str("CREATE ")?;
7039 if self.or_replace {
7040 f.write_str("OR REPLACE ")?;
7041 }
7042 write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
7043 for (i, arg) in self.args.iter().enumerate() {
7044 if i > 0 {
7045 f.write_str(", ")?;
7046 }
7047 match arg.mode {
7048 FunctionArgMode::In => {}
7049 FunctionArgMode::Out => f.write_str("OUT ")?,
7050 FunctionArgMode::InOut => f.write_str("INOUT ")?,
7051 }
7052 if let Some(name) = &arg.name {
7053 write!(f, "{} ", quote_ident(name))?;
7054 }
7055 match &arg.ty {
7056 FunctionArgType::Typed(t) => write!(f, "{t}")?,
7057 FunctionArgType::Raw(s) => f.write_str(s)?,
7058 }
7059 }
7060 f.write_str(") RETURNS ")?;
7061 match &self.returns {
7062 FunctionReturn::Trigger => f.write_str("TRIGGER")?,
7063 FunctionReturn::Void => f.write_str("VOID")?,
7064 FunctionReturn::Type(t) => write!(f, "{t}")?,
7065 FunctionReturn::Other(s) => f.write_str(s)?,
7066 }
7067 write!(f, " LANGUAGE {} AS $$", self.language)?;
7068 match &self.body {
7069 FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
7070 FunctionBody::Raw(s) => f.write_str(s)?,
7071 }
7072 f.write_str("$$")
7073 }
7074}
7075
7076impl fmt::Display for PlPgSqlBlock {
7077 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7078 if !self.declarations.is_empty() {
7079 f.write_str("DECLARE\n")?;
7080 for d in &self.declarations {
7081 write!(f, " {} ", quote_ident(&d.name))?;
7082 match &d.ty {
7083 FunctionArgType::Typed(t) => write!(f, "{t}")?,
7084 FunctionArgType::Raw(s) => f.write_str(s)?,
7085 }
7086 if let Some(e) = &d.default {
7087 write!(f, " := {e}")?;
7088 }
7089 f.write_str(";\n")?;
7090 }
7091 }
7092 f.write_str("BEGIN\n")?;
7093 for stmt in &self.statements {
7094 writeln!(f, " {stmt};")?;
7095 }
7096 // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
7097 // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
7098 // parsed block through it — so every exception handler a function
7099 // declared was thrown away AT STORE TIME. The block executed fine while
7100 // it was still an AST (a DO block never round-trips through text), which
7101 // is why only functions and triggers lost theirs.
7102 if !self.exception_handlers.is_empty() {
7103 f.write_str("EXCEPTION\n")?;
7104 for h in &self.exception_handlers {
7105 writeln!(f, " WHEN {} THEN", h.conditions.join(" OR "))?;
7106 for stmt in &h.body {
7107 writeln!(f, " {stmt};")?;
7108 }
7109 }
7110 }
7111 f.write_str("END")
7112 }
7113}
7114
7115impl fmt::Display for PlPgSqlStmt {
7116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7117 match self {
7118 Self::Assign { target, value } => write!(f, "{target} := {value}"),
7119 Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
7120 Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
7121 Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
7122 Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
7123 Self::Return(t) => match t {
7124 ReturnTarget::New => f.write_str("RETURN NEW"),
7125 ReturnTarget::Old => f.write_str("RETURN OLD"),
7126 ReturnTarget::Null => f.write_str("RETURN NULL"),
7127 ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
7128 },
7129 Self::If {
7130 branches,
7131 else_branch,
7132 } => {
7133 for (i, (cond, body)) in branches.iter().enumerate() {
7134 if i == 0 {
7135 write!(f, "IF {cond} THEN ")?;
7136 } else {
7137 write!(f, " ELSIF {cond} THEN ")?;
7138 }
7139 for (j, s) in body.iter().enumerate() {
7140 if j > 0 {
7141 f.write_str("; ")?;
7142 }
7143 write!(f, "{s}")?;
7144 }
7145 }
7146 if !else_branch.is_empty() {
7147 f.write_str(" ELSE ")?;
7148 for (j, s) in else_branch.iter().enumerate() {
7149 if j > 0 {
7150 f.write_str("; ")?;
7151 }
7152 write!(f, "{s}")?;
7153 }
7154 }
7155 f.write_str(" END IF")
7156 }
7157 Self::Raise {
7158 level,
7159 message,
7160 args,
7161 } => {
7162 let lvl = match level {
7163 RaiseLevel::Notice => "NOTICE",
7164 RaiseLevel::Warning => "WARNING",
7165 RaiseLevel::Info => "INFO",
7166 RaiseLevel::Log => "LOG",
7167 RaiseLevel::Debug => "DEBUG",
7168 RaiseLevel::Exception => "EXCEPTION",
7169 };
7170 write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
7171 for a in args {
7172 write!(f, ", {a}")?;
7173 }
7174 Ok(())
7175 }
7176 Self::EmbeddedSql(s) => write!(f, "{s}"),
7177 Self::Assert { condition, message } => {
7178 write!(f, "ASSERT {condition}")?;
7179 if let Some(m) = message {
7180 write!(f, ", {m}")?;
7181 }
7182 Ok(())
7183 }
7184 Self::While { condition, body } => {
7185 writeln!(f, "WHILE {condition} LOOP")?;
7186 for s in body {
7187 writeln!(f, " {s};")?;
7188 }
7189 f.write_str("END LOOP")
7190 }
7191 Self::ForRange {
7192 var,
7193 start,
7194 end,
7195 reverse,
7196 body,
7197 } => {
7198 write!(f, "FOR {var} IN ")?;
7199 if *reverse {
7200 f.write_str("REVERSE ")?;
7201 }
7202 writeln!(f, "{start}..{end} LOOP")?;
7203 for s in body {
7204 writeln!(f, " {s};")?;
7205 }
7206 f.write_str("END LOOP")
7207 }
7208 Self::Loop { body } => {
7209 writeln!(f, "LOOP")?;
7210 for s in body {
7211 writeln!(f, " {s};")?;
7212 }
7213 f.write_str("END LOOP")
7214 }
7215 Self::Exit { when } => {
7216 f.write_str("EXIT")?;
7217 if let Some(c) = when {
7218 write!(f, " WHEN {c}")?;
7219 }
7220 Ok(())
7221 }
7222 Self::Continue { when } => {
7223 f.write_str("CONTINUE")?;
7224 if let Some(c) = when {
7225 write!(f, " WHEN {c}")?;
7226 }
7227 Ok(())
7228 }
7229 Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
7230 Self::ForQuery { var, query, body } => {
7231 writeln!(f, "FOR {var} IN ({query}) LOOP")?;
7232 for s in body {
7233 writeln!(f, " {s};")?;
7234 }
7235 f.write_str("END LOOP")
7236 }
7237 Self::ForExecute {
7238 var,
7239 sql_expr,
7240 body,
7241 } => {
7242 writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
7243 for s in body {
7244 writeln!(f, " {s};")?;
7245 }
7246 f.write_str("END LOOP")
7247 }
7248 }
7249 }
7250}
7251
7252impl fmt::Display for AssignTarget {
7253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7254 match self {
7255 Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
7256 Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
7257 Self::Local(n) => f.write_str(n),
7258 }
7259 }
7260}
7261
7262impl fmt::Display for CreateTriggerStatement {
7263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7264 f.write_str("CREATE ")?;
7265 if self.or_replace {
7266 f.write_str("OR REPLACE ")?;
7267 }
7268 write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
7269 match self.timing {
7270 TriggerTiming::Before => f.write_str("BEFORE")?,
7271 TriggerTiming::After => f.write_str("AFTER")?,
7272 TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
7273 }
7274 for (i, e) in self.events.iter().enumerate() {
7275 if i == 0 {
7276 f.write_str(" ")?;
7277 } else {
7278 f.write_str(" OR ")?;
7279 }
7280 match e {
7281 TriggerEvent::Insert => f.write_str("INSERT")?,
7282 TriggerEvent::Update => {
7283 f.write_str("UPDATE")?;
7284 if !self.update_columns.is_empty() {
7285 f.write_str(" OF ")?;
7286 for (j, col) in self.update_columns.iter().enumerate() {
7287 if j > 0 {
7288 f.write_str(", ")?;
7289 }
7290 f.write_str("e_ident(col))?;
7291 }
7292 }
7293 }
7294 TriggerEvent::Delete => f.write_str("DELETE")?,
7295 TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
7296 }
7297 }
7298 write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
7299 match self.for_each {
7300 TriggerForEach::Row => f.write_str("ROW")?,
7301 TriggerForEach::Statement => f.write_str("STATEMENT")?,
7302 }
7303 write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
7304 }
7305}
7306
7307impl fmt::Display for CreateIndexStatement {
7308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7309 if self.is_unique {
7310 f.write_str("CREATE UNIQUE INDEX ")?;
7311 } else {
7312 f.write_str("CREATE INDEX ")?;
7313 }
7314 if self.if_not_exists {
7315 f.write_str("IF NOT EXISTS ")?;
7316 }
7317 write!(
7318 f,
7319 "{} ON {} ",
7320 quote_ident(&self.name),
7321 quote_ident(&self.table)
7322 )?;
7323 match self.method {
7324 IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
7325 IndexMethod::Brin => f.write_str("USING brin ")?,
7326 IndexMethod::Gin => f.write_str("USING gin ")?,
7327 IndexMethod::BTree => {}
7328 }
7329 if let Some(expr) = &self.expression {
7330 write!(f, "({})", expr)?;
7331 } else if self.extra_columns.is_empty() {
7332 // v7.15.0 — preserve operator class on round-trip
7333 // (`(col opclass)`) so WAL replay reconstructs the
7334 // engine-routing intent (e.g. `gin_trgm_ops` →
7335 // trigram-GIN build path).
7336 if let Some(op) = &self.opclass {
7337 write!(f, "({} {})", quote_ident(&self.column), op)?;
7338 } else {
7339 write!(f, "({})", quote_ident(&self.column))?;
7340 }
7341 } else {
7342 // v7.9.14 — multi-column key. Emit each column quoted
7343 // so the round-tripped form re-parses to identical AST.
7344 f.write_str("(")?;
7345 write!(f, "{}", quote_ident(&self.column))?;
7346 for c in &self.extra_columns {
7347 write!(f, ", {}", quote_ident(c))?;
7348 }
7349 f.write_str(")")?;
7350 }
7351 if !self.included_columns.is_empty() {
7352 f.write_str(" INCLUDE (")?;
7353 for (i, c) in self.included_columns.iter().enumerate() {
7354 if i > 0 {
7355 f.write_str(", ")?;
7356 }
7357 write!(f, "{}", quote_ident(c))?;
7358 }
7359 f.write_str(")")?;
7360 }
7361 if let Some(pred) = &self.partial_predicate {
7362 write!(f, " WHERE {}", pred)?;
7363 }
7364 Ok(())
7365 }
7366}
7367
7368impl fmt::Display for CreateTableStatement {
7369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7370 f.write_str("CREATE TABLE ")?;
7371 if self.if_not_exists {
7372 f.write_str("IF NOT EXISTS ")?;
7373 }
7374 write!(f, "{}", quote_ident(&self.name))?;
7375 // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
7376 // no column list and no constraints; the table inherits its
7377 // columns from the parent at engine-DDL time.
7378 if let Some(spec) = &self.partition_of {
7379 write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
7380 return match &spec.bounds {
7381 PartitionOfBoundsAst::Range { lower, upper } => {
7382 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7383 }
7384 PartitionOfBoundsAst::List { values } => {
7385 f.write_str("FOR VALUES IN (")?;
7386 for (i, v) in values.iter().enumerate() {
7387 if i > 0 {
7388 f.write_str(", ")?;
7389 }
7390 write!(f, "{}", v)?;
7391 }
7392 f.write_str(")")
7393 }
7394 PartitionOfBoundsAst::Hash { modulus, remainder } => {
7395 write!(
7396 f,
7397 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7398 modulus, remainder
7399 )
7400 }
7401 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7402 };
7403 }
7404 f.write_str(" (")?;
7405 for (i, col) in self.columns.iter().enumerate() {
7406 if i > 0 {
7407 f.write_str(", ")?;
7408 }
7409 write!(f, "{col}")?;
7410 }
7411 // v7.6.0 — render FK constraints in table-level form, after
7412 // the column list. WAL replay round-trips through Display, so
7413 // every FK must serialise here for replay to reconstruct the
7414 // schema bit-for-bit.
7415 for fk in &self.foreign_keys {
7416 f.write_str(", ")?;
7417 write!(f, "{fk}")?;
7418 }
7419 // v7.13.0 — render table-level constraints (PRIMARY KEY /
7420 // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
7421 // column-level UNIQUE / CHECK get lifted to this list at
7422 // parse time, so emitting only here avoids double-counting.
7423 for tc in &self.table_constraints {
7424 f.write_str(", ")?;
7425 write!(f, "{tc}")?;
7426 }
7427 f.write_str(")")?;
7428 // v7.37.6-B — partition-parent suffix renders after the
7429 // closing column-list paren, before the optional MySQL
7430 // table-options tail (which Display doesn't currently emit).
7431 if let Some(spec) = &self.partition_by {
7432 f.write_str(" PARTITION BY ")?;
7433 match spec.kind {
7434 PartitionKindAst::Range => f.write_str("RANGE ")?,
7435 PartitionKindAst::List => f.write_str("LIST ")?,
7436 PartitionKindAst::Hash => f.write_str("HASH ")?,
7437 }
7438 f.write_str("(")?;
7439 for (i, col) in spec.key_columns.iter().enumerate() {
7440 if i > 0 {
7441 f.write_str(", ")?;
7442 }
7443 f.write_str("e_ident(col))?;
7444 }
7445 f.write_str(")")?;
7446 }
7447 Ok(())
7448 }
7449}
7450
7451fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
7452 match t {
7453 AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
7454 AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
7455 write!(f, "REPLICA IDENTITY USING INDEX {index}")
7456 }
7457 AlterTableTarget::Inherit { parent, detach } => {
7458 if *detach {
7459 write!(f, "NO INHERIT {parent}")
7460 } else {
7461 write!(f, "INHERIT {parent}")
7462 }
7463 }
7464 AlterTableTarget::SetHotTierBytes(n) => {
7465 write!(f, "SET hot_tier_bytes = {n}")
7466 }
7467 AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
7468 AlterTableTarget::DropForeignKey { name, if_exists } => {
7469 f.write_str("DROP CONSTRAINT ")?;
7470 if *if_exists {
7471 f.write_str("IF EXISTS ")?;
7472 }
7473 write!(f, "{}", quote_ident(name))
7474 }
7475 AlterTableTarget::DropIndex { name, if_exists } => {
7476 f.write_str("DROP INDEX ")?;
7477 if *if_exists {
7478 f.write_str("IF EXISTS ")?;
7479 }
7480 write!(f, "{}", quote_ident(name))
7481 }
7482 AlterTableTarget::ModifyColumn {
7483 column,
7484 rename_to,
7485 definition,
7486 position,
7487 } => {
7488 if let Some(new) = rename_to {
7489 write!(
7490 f,
7491 "CHANGE COLUMN {} {} {}",
7492 quote_ident(column),
7493 quote_ident(new),
7494 definition.ty
7495 )?;
7496 } else {
7497 write!(f, "MODIFY COLUMN {} {}", quote_ident(column), definition.ty)?;
7498 }
7499 if !definition.nullable {
7500 f.write_str(" NOT NULL")?;
7501 }
7502 write_column_position(f, position.as_ref())
7503 }
7504 AlterTableTarget::RenameIndex { old, new } => {
7505 write!(
7506 f,
7507 "RENAME INDEX {} TO {}",
7508 quote_ident(old),
7509 quote_ident(new)
7510 )
7511 }
7512 AlterTableTarget::SetTableAutoIncrement(n) => write!(f, "AUTO_INCREMENT = {n}"),
7513 AlterTableTarget::SetEngine(name) => write!(f, "ENGINE = {name}"),
7514 AlterTableTarget::ConvertToCharacterSet { charset, collate } => {
7515 write!(f, "CONVERT TO CHARACTER SET {charset}")?;
7516 if let Some(c) = collate {
7517 write!(f, " COLLATE {c}")?;
7518 }
7519 Ok(())
7520 }
7521 AlterTableTarget::AddColumn {
7522 column,
7523 if_not_exists,
7524 position,
7525 } => {
7526 f.write_str("ADD COLUMN ")?;
7527 if *if_not_exists {
7528 f.write_str("IF NOT EXISTS ")?;
7529 }
7530 write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
7531 if !column.nullable {
7532 f.write_str(" NOT NULL")?;
7533 }
7534 if let Some(d) = &column.default {
7535 write!(f, " DEFAULT {d}")?;
7536 }
7537 if column.auto_increment {
7538 f.write_str(" AUTO_INCREMENT")?;
7539 }
7540 if column.is_primary_key {
7541 f.write_str(" PRIMARY KEY")?;
7542 }
7543 Ok(())
7544 }
7545 AlterTableTarget::AlterColumnType {
7546 column,
7547 new_type,
7548 using,
7549 collation,
7550 } => {
7551 write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
7552 if let Some((_, name)) = collation {
7553 write!(f, " COLLATE {}", quote_ident(name))?;
7554 }
7555 if let Some(u) = using {
7556 write!(f, " USING {u}")?;
7557 }
7558 Ok(())
7559 }
7560 AlterTableTarget::DropColumn {
7561 column,
7562 if_exists,
7563 cascade,
7564 } => {
7565 f.write_str("DROP COLUMN ")?;
7566 if *if_exists {
7567 f.write_str("IF EXISTS ")?;
7568 }
7569 write!(f, "{}", quote_ident(column))?;
7570 if *cascade {
7571 f.write_str(" CASCADE")?;
7572 }
7573 Ok(())
7574 }
7575 AlterTableTarget::AddTableConstraint(tc) => {
7576 write!(f, "ADD {tc}")
7577 }
7578 AlterTableTarget::ValidateConstraint { name } => {
7579 write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
7580 }
7581 AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
7582 AlterTableTarget::ClusterOn { index } => match index {
7583 Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
7584 None => f.write_str("SET WITHOUT CLUSTER"),
7585 },
7586 AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
7587 // Round-trip-safe spelling: re-parsing this form lowers
7588 // back to SetColumnAutoIncrement (the nextval default is
7589 // how pg_dump says "serial").
7590 let seq = seq_name
7591 .clone()
7592 .unwrap_or_else(|| alloc::format!("{column}_seq"));
7593 write!(
7594 f,
7595 "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
7596 quote_ident(column)
7597 )
7598 }
7599 AlterTableTarget::RenameColumn { old, new } => {
7600 write!(
7601 f,
7602 "RENAME COLUMN {} TO {}",
7603 quote_ident(old),
7604 quote_ident(new)
7605 )
7606 }
7607 AlterTableTarget::RenameConstraint { old, new } => {
7608 write!(
7609 f,
7610 "RENAME CONSTRAINT {} TO {}",
7611 quote_ident(old),
7612 quote_ident(new)
7613 )
7614 }
7615 AlterTableTarget::RenameTable { new } => {
7616 write!(f, "RENAME TO {}", quote_ident(new))
7617 }
7618 AlterTableTarget::SetTriggerEnabled { which, enabled } => {
7619 f.write_str(if *enabled {
7620 "ENABLE TRIGGER "
7621 } else {
7622 "DISABLE TRIGGER "
7623 })?;
7624 match which {
7625 TriggerSelector::All => f.write_str("ALL"),
7626 TriggerSelector::Named(n) => f.write_str("e_ident(n)),
7627 }
7628 }
7629 AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
7630 (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
7631 (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
7632 (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
7633 (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
7634 (None, None) => Ok(()),
7635 },
7636 AlterTableTarget::AttachPartition { child, bounds } => {
7637 write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
7638 match bounds {
7639 PartitionOfBoundsAst::Range { lower, upper } => {
7640 write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7641 }
7642 PartitionOfBoundsAst::List { values } => {
7643 f.write_str("FOR VALUES IN (")?;
7644 for (i, v) in values.iter().enumerate() {
7645 if i > 0 {
7646 f.write_str(", ")?;
7647 }
7648 write!(f, "{}", v)?;
7649 }
7650 f.write_str(")")
7651 }
7652 PartitionOfBoundsAst::Hash { modulus, remainder } => {
7653 write!(
7654 f,
7655 "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7656 modulus, remainder
7657 )
7658 }
7659 PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7660 }
7661 }
7662 AlterTableTarget::DetachPartition {
7663 child,
7664 concurrently,
7665 finalize,
7666 } => {
7667 write!(f, "DETACH PARTITION {}", quote_ident(child))?;
7668 if *concurrently {
7669 f.write_str(" CONCURRENTLY")?;
7670 }
7671 if *finalize {
7672 f.write_str(" FINALIZE")?;
7673 }
7674 Ok(())
7675 }
7676 AlterTableTarget::AlterColumnSetDefault {
7677 column,
7678 default_expr,
7679 } => write!(
7680 f,
7681 "ALTER COLUMN {} SET DEFAULT {}",
7682 quote_ident(column),
7683 default_expr
7684 ),
7685 AlterTableTarget::AlterColumnDropDefault { column } => {
7686 write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
7687 }
7688 AlterTableTarget::AlterColumnSetNotNull { column } => {
7689 write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
7690 }
7691 AlterTableTarget::AlterColumnDropNotNull { column } => {
7692 write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
7693 }
7694 AlterTableTarget::AlterColumnRestart { column, with } => {
7695 write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
7696 if let Some(n) = with {
7697 write!(f, " WITH {n}")?;
7698 }
7699 Ok(())
7700 }
7701 AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
7702 write!(
7703 f,
7704 "ALTER COLUMN {} DROP EXPRESSION{}",
7705 quote_ident(column),
7706 if *if_exists { " IF EXISTS" } else { "" }
7707 )
7708 }
7709 AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7710 write!(
7711 f,
7712 "ALTER COLUMN {} DROP IDENTITY{}",
7713 quote_ident(column),
7714 if *if_exists { " IF EXISTS" } else { "" }
7715 )
7716 }
7717 AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7718 write!(
7719 f,
7720 "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7721 quote_ident(column)
7722 )
7723 }
7724 }
7725}
7726
7727impl fmt::Display for TableConstraint {
7728 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7729 match self {
7730 Self::PrimaryKey { name, columns, .. } => {
7731 if let Some(n) = name {
7732 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7733 }
7734 f.write_str("PRIMARY KEY (")?;
7735 for (i, c) in columns.iter().enumerate() {
7736 if i > 0 {
7737 f.write_str(", ")?;
7738 }
7739 f.write_str("e_ident(c))?;
7740 }
7741 f.write_str(")")
7742 }
7743 Self::Unique {
7744 name,
7745 columns,
7746 nulls_not_distinct,
7747 ..
7748 } => {
7749 if let Some(n) = name {
7750 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7751 }
7752 f.write_str("UNIQUE ")?;
7753 if *nulls_not_distinct {
7754 f.write_str("NULLS NOT DISTINCT ")?;
7755 }
7756 f.write_str("(")?;
7757 for (i, c) in columns.iter().enumerate() {
7758 if i > 0 {
7759 f.write_str(", ")?;
7760 }
7761 f.write_str("e_ident(c))?;
7762 }
7763 f.write_str(")")
7764 }
7765 Self::Check {
7766 name,
7767 expr,
7768 not_valid,
7769 } => {
7770 if let Some(n) = name {
7771 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7772 }
7773 write!(f, "CHECK ({expr})")?;
7774 if *not_valid {
7775 write!(f, " NOT VALID")?;
7776 }
7777 Ok(())
7778 }
7779 Self::Index {
7780 name,
7781 columns,
7782 prefix_lengths,
7783 } => {
7784 f.write_str("KEY ")?;
7785 if let Some(n) = name {
7786 write!(f, "{} ", quote_ident(n))?;
7787 }
7788 f.write_str("(")?;
7789 for (i, c) in columns.iter().enumerate() {
7790 if i > 0 {
7791 f.write_str(", ")?;
7792 }
7793 f.write_str("e_ident(c))?;
7794 // v7.40.0 — the declared prefix rounds back with it.
7795 if let Some(Some(p)) = prefix_lengths.get(i) {
7796 write!(f, "({p})")?;
7797 }
7798 }
7799 f.write_str(")")
7800 }
7801 Self::FulltextIndex { name, columns } => {
7802 // Mysqldump emits `FULLTEXT KEY name (cols)` —
7803 // Display rounds back to that shape so dump
7804 // replay reproduces the input verbatim.
7805 f.write_str("FULLTEXT KEY ")?;
7806 if let Some(n) = name {
7807 write!(f, "{} ", quote_ident(n))?;
7808 }
7809 f.write_str("(")?;
7810 for (i, c) in columns.iter().enumerate() {
7811 if i > 0 {
7812 f.write_str(", ")?;
7813 }
7814 f.write_str("e_ident(c))?;
7815 }
7816 f.write_str(")")
7817 }
7818 Self::Exclude {
7819 name,
7820 method,
7821 elements,
7822 } => {
7823 if let Some(n) = name {
7824 write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7825 }
7826 f.write_str("EXCLUDE ")?;
7827 if let Some(m) = method {
7828 write!(f, "USING {m} ")?;
7829 }
7830 f.write_str("(")?;
7831 for (i, (col, op)) in elements.iter().enumerate() {
7832 if i > 0 {
7833 f.write_str(", ")?;
7834 }
7835 write!(f, "{} WITH {op}", quote_ident(col))?;
7836 }
7837 f.write_str(")")
7838 }
7839 }
7840 }
7841}
7842
7843impl fmt::Display for ForeignKeyConstraint {
7844 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7845 if let Some(name) = &self.name {
7846 write!(f, "CONSTRAINT {} ", quote_ident(name))?;
7847 }
7848 f.write_str("FOREIGN KEY (")?;
7849 for (i, c) in self.columns.iter().enumerate() {
7850 if i > 0 {
7851 f.write_str(", ")?;
7852 }
7853 f.write_str("e_ident(c))?;
7854 }
7855 write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
7856 if !self.parent_columns.is_empty() {
7857 f.write_str(" (")?;
7858 for (i, c) in self.parent_columns.iter().enumerate() {
7859 if i > 0 {
7860 f.write_str(", ")?;
7861 }
7862 f.write_str("e_ident(c))?;
7863 }
7864 f.write_str(")")?;
7865 }
7866 // Only render non-default actions to keep Display output
7867 // close to user input. SPG's default is RESTRICT (matches
7868 // SQL spec).
7869 if self.on_delete != FkAction::Restrict {
7870 write!(f, " ON DELETE {}", self.on_delete)?;
7871 }
7872 if self.on_update != FkAction::Restrict {
7873 write!(f, " ON UPDATE {}", self.on_update)?;
7874 }
7875 Ok(())
7876 }
7877}
7878
7879impl fmt::Display for FkAction {
7880 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7881 match self {
7882 Self::Restrict => f.write_str("RESTRICT"),
7883 Self::Cascade => f.write_str("CASCADE"),
7884 Self::SetNull => f.write_str("SET NULL"),
7885 Self::SetDefault => f.write_str("SET DEFAULT"),
7886 Self::NoAction => f.write_str("NO ACTION"),
7887 }
7888 }
7889}
7890
7891impl fmt::Display for ColumnDef {
7892 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7893 // v7.30.1 (mailrs round-24 class audit) — the type position
7894 // must re-parse to the same ColumnDef: a user-defined type
7895 // reference and the MySQL inline ENUM / SET value lists all
7896 // lower `ty` to Text, so rendering `ty` lost them.
7897 write!(f, "{}", quote_ident(&self.name))?;
7898 if let Some(ut) = &self.user_type_ref {
7899 write!(f, " {}", quote_ident(ut))?;
7900 } else if let Some(variants) = &self.inline_enum_variants {
7901 write_variant_list(f, "ENUM", variants)?;
7902 } else if let Some(variants) = &self.inline_set_variants {
7903 write_variant_list(f, "SET", variants)?;
7904 } else {
7905 write!(f, " {}", self.ty)?;
7906 }
7907 if self.is_unsigned {
7908 f.write_str(" UNSIGNED")?;
7909 }
7910 // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
7911 // DDL. Only emits when non-default so the typical output
7912 // stays unchanged.
7913 match self.collation {
7914 Collation::Binary => {}
7915 Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
7916 }
7917 if let Some(d) = &self.default {
7918 write!(f, " DEFAULT {d}")?;
7919 }
7920 if self.auto_increment {
7921 f.write_str(" AUTO_INCREMENT")?;
7922 }
7923 if !self.nullable {
7924 f.write_str(" NOT NULL")?;
7925 }
7926 // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
7927 // is NOT lifted to a table-level constraint at parse time
7928 // (unlike UNIQUE / CHECK), so the WAL round trip of a
7929 // prepared CREATE TABLE silently dropped the primary key.
7930 if self.is_primary_key {
7931 f.write_str(" PRIMARY KEY")?;
7932 }
7933 // The parser accepts only CURRENT_TIMESTAMP here (stored as
7934 // now()), so that spelling is the lossless round trip.
7935 if self.on_update_runtime.is_some() {
7936 f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
7937 }
7938 // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
7939 // replay reconstructs the computed-column declaration. The
7940 // expression sits inside a single set of parens; STORED is
7941 // the only variant the parser accepts.
7942 if let Some(gen_expr) = &self.generated_stored_expr {
7943 write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
7944 }
7945 Ok(())
7946 }
7947}
7948
7949/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
7950/// types (MySQL flavour; `ty` is Text underneath).
7951fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
7952 write!(f, " {kw}(")?;
7953 for (i, v) in variants.iter().enumerate() {
7954 if i > 0 {
7955 f.write_str(", ")?;
7956 }
7957 write!(f, "'{}'", v.replace('\'', "''"))?;
7958 }
7959 f.write_str(")")
7960}
7961
7962impl fmt::Display for InsertStatement {
7963 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7964 write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
7965 if let Some(cols) = &self.columns {
7966 f.write_str(" (")?;
7967 for (i, c) in cols.iter().enumerate() {
7968 if i > 0 {
7969 f.write_str(", ")?;
7970 }
7971 f.write_str("e_ident(c))?;
7972 }
7973 f.write_str(")")?;
7974 }
7975 // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
7976 // skipping the VALUES list (mailrs round-5 G4).
7977 if let Some(sel) = &self.select_source {
7978 write!(f, " {sel}")?;
7979 } else {
7980 f.write_str(" VALUES ")?;
7981 for (ri, row) in self.rows.iter().enumerate() {
7982 if ri > 0 {
7983 f.write_str(", ")?;
7984 }
7985 f.write_str("(")?;
7986 for (i, v) in row.iter().enumerate() {
7987 if i > 0 {
7988 f.write_str(", ")?;
7989 }
7990 write!(f, "{v}")?;
7991 }
7992 f.write_str(")")?;
7993 }
7994 }
7995 // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
7996 // Display round trip: WAL persistence renders the bind-final
7997 // AST through this impl, and a replayed bare INSERT turns a
7998 // legal upsert no-op into a UNIQUE violation that refuses to
7999 // open the catalog.
8000 if let Some(oc) = &self.on_conflict {
8001 write!(f, " {oc}")?;
8002 }
8003 write_returning(self.returning.as_deref(), f)?;
8004 Ok(())
8005 }
8006}
8007
8008/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
8009/// parser produced, so the AST→SQL round trip preserves upsert
8010/// semantics (WAL replay depends on it).
8011impl fmt::Display for OnConflictClause {
8012 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8013 f.write_str("ON CONFLICT")?;
8014 if let Some(name) = &self.constraint_name {
8015 write!(f, " ON CONSTRAINT {name}")?;
8016 }
8017 if !self.target_columns.is_empty() {
8018 f.write_str(" (")?;
8019 for (i, c) in self.target_columns.iter().enumerate() {
8020 if i > 0 {
8021 f.write_str(", ")?;
8022 }
8023 f.write_str("e_ident(c))?;
8024 }
8025 f.write_str(")")?;
8026 }
8027 if let Some(w) = &self.index_where {
8028 write!(f, " WHERE {w}")?;
8029 }
8030 match &self.action {
8031 OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
8032 OnConflictAction::Update {
8033 assignments,
8034 where_,
8035 } => {
8036 f.write_str(" DO UPDATE SET ")?;
8037 for (i, (col, expr)) in assignments.iter().enumerate() {
8038 if i > 0 {
8039 f.write_str(", ")?;
8040 }
8041 write!(f, "{} = {expr}", quote_ident(col))?;
8042 }
8043 if let Some(w) = where_ {
8044 write!(f, " WHERE {w}")?;
8045 }
8046 Ok(())
8047 }
8048 }
8049 }
8050}
8051
8052/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
8053/// tail for the three DML Display impls.
8054fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8055 let Some(items) = ret else {
8056 return Ok(());
8057 };
8058 f.write_str(" RETURNING ")?;
8059 for (i, item) in items.iter().enumerate() {
8060 if i > 0 {
8061 f.write_str(", ")?;
8062 }
8063 write!(f, "{item}")?;
8064 }
8065 Ok(())
8066}
8067
8068impl fmt::Display for UpdateStatement {
8069 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8070 write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
8071 for (i, (col, expr)) in self.assignments.iter().enumerate() {
8072 if i > 0 {
8073 f.write_str(", ")?;
8074 }
8075 write!(f, "{} = {expr}", quote_ident(col))?;
8076 }
8077 if let Some(w) = &self.where_ {
8078 write!(f, " WHERE {w}")?;
8079 }
8080 // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
8081 if let Some(ol) = self.order_limit.as_deref() {
8082 if !ol.order_by.is_empty() {
8083 f.write_str(" ORDER BY ")?;
8084 for (i, o) in ol.order_by.iter().enumerate() {
8085 if i > 0 {
8086 f.write_str(", ")?;
8087 }
8088 write!(f, "{}", o.expr)?;
8089 if o.desc {
8090 f.write_str(" DESC")?;
8091 }
8092 match o.nulls_first {
8093 Some(true) => f.write_str(" NULLS FIRST")?,
8094 Some(false) => f.write_str(" NULLS LAST")?,
8095 None => {}
8096 }
8097 }
8098 }
8099 if let Some(n) = ol.limit {
8100 write!(f, " LIMIT {n}")?;
8101 }
8102 }
8103 write_returning(self.returning.as_deref(), f)?;
8104 Ok(())
8105 }
8106}
8107
8108impl fmt::Display for DeleteStatement {
8109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8110 write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
8111 if let Some(w) = &self.where_ {
8112 write!(f, " WHERE {w}")?;
8113 }
8114 write_returning(self.returning.as_deref(), f)?;
8115 Ok(())
8116 }
8117}
8118
8119impl fmt::Display for CteBody {
8120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8121 match self {
8122 Self::Select(s) => write!(f, "{s}"),
8123 Self::Insert(s) => write!(f, "{s}"),
8124 Self::Update(s) => write!(f, "{s}"),
8125 Self::Delete(s) => write!(f, "{s}"),
8126 Self::Merge(s) => write!(f, "{s}"),
8127 }
8128 }
8129}
8130
8131impl fmt::Display for MergeStatement {
8132 // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
8133 // (it round-trips for the cases tests cover, not for
8134 // round-tripping every edge of the surface).
8135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8136 fmt_with_clause(&self.ctes, f)?;
8137 f.write_str("MERGE INTO ")?;
8138 write!(f, "{}", quote_ident(&self.target))?;
8139 if let Some(a) = &self.target_alias {
8140 write!(f, " {}", quote_ident(a))?;
8141 }
8142 f.write_str(" USING ")?;
8143 if let Some(sub) = &self.source_select {
8144 write!(f, "({sub})")?;
8145 } else {
8146 write!(f, "{}", quote_ident(&self.source))?;
8147 }
8148 if let Some(a) = &self.source_alias {
8149 write!(f, " {}", quote_ident(a))?;
8150 }
8151 if !self.source_column_aliases.is_empty() {
8152 f.write_str("(")?;
8153 for (i, c) in self.source_column_aliases.iter().enumerate() {
8154 if i > 0 {
8155 f.write_str(", ")?;
8156 }
8157 write!(f, "{}", quote_ident(c))?;
8158 }
8159 f.write_str(")")?;
8160 }
8161 write!(f, " ON {}", self.on)?;
8162 for clause in &self.clauses {
8163 f.write_str(" WHEN ")?;
8164 f.write_str(match clause.matched {
8165 MergeMatched::Matched => "MATCHED",
8166 MergeMatched::NotMatched => "NOT MATCHED",
8167 MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
8168 })?;
8169 if let Some(c) = &clause.condition {
8170 write!(f, " AND {c}")?;
8171 }
8172 f.write_str(" THEN ")?;
8173 match &clause.action {
8174 MergeAction::Insert { columns, values } => {
8175 f.write_str("INSERT ")?;
8176 // A column list is optional (round 146): the bare
8177 // `INSERT VALUES (…)` form maps positionally.
8178 if !columns.is_empty() {
8179 f.write_str("(")?;
8180 for (i, c) in columns.iter().enumerate() {
8181 if i > 0 {
8182 f.write_str(", ")?;
8183 }
8184 write!(f, "{}", quote_ident(c))?;
8185 }
8186 f.write_str(") ")?;
8187 }
8188 f.write_str("VALUES (")?;
8189 for (i, v) in values.iter().enumerate() {
8190 if i > 0 {
8191 f.write_str(", ")?;
8192 }
8193 write!(f, "{v}")?;
8194 }
8195 f.write_str(")")?;
8196 }
8197 MergeAction::Update { assignments } => {
8198 f.write_str("UPDATE SET ")?;
8199 for (i, (c, e)) in assignments.iter().enumerate() {
8200 if i > 0 {
8201 f.write_str(", ")?;
8202 }
8203 write!(f, "{} = {e}", quote_ident(c))?;
8204 }
8205 }
8206 MergeAction::Delete => f.write_str("DELETE")?,
8207 MergeAction::DoNothing => f.write_str("DO NOTHING")?,
8208 }
8209 }
8210 if let Some(items) = &self.returning {
8211 f.write_str(" RETURNING ")?;
8212 for (i, it) in items.iter().enumerate() {
8213 if i > 0 {
8214 f.write_str(", ")?;
8215 }
8216 write!(f, "{it}")?;
8217 }
8218 }
8219 Ok(())
8220 }
8221}
8222
8223/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
8224/// carry a CTE list and must round-trip it identically.
8225fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
8226 if ctes.is_empty() {
8227 return Ok(());
8228 }
8229 f.write_str("WITH ")?;
8230 if ctes.iter().any(|c| c.recursive) {
8231 f.write_str("RECURSIVE ")?;
8232 }
8233 for (i, cte) in ctes.iter().enumerate() {
8234 if i > 0 {
8235 f.write_str(", ")?;
8236 }
8237 f.write_str("e_ident(&cte.name))?;
8238 if !cte.column_overrides.is_empty() {
8239 f.write_str(" (")?;
8240 for (ci, c) in cte.column_overrides.iter().enumerate() {
8241 if ci > 0 {
8242 f.write_str(", ")?;
8243 }
8244 f.write_str("e_ident(c))?;
8245 }
8246 f.write_str(")")?;
8247 }
8248 write!(f, " AS ({})", cte.body)?;
8249 }
8250 f.write_str(" ")
8251}
8252
8253impl fmt::Display for SelectStatement {
8254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8255 // v7.30.1 (mailrs round-24 class audit) — the WITH clause
8256 // must survive the round trip; a CTE-using statement
8257 // re-parsed without it references undefined tables.
8258 fmt_with_clause(&self.ctes, f)?;
8259 write_bare_select(self, f)?;
8260 for (kind, peer) in &self.unions {
8261 f.write_str(match kind {
8262 UnionKind::Distinct => " UNION ",
8263 UnionKind::All => " UNION ALL ",
8264 UnionKind::Intersect => " INTERSECT ",
8265 UnionKind::IntersectAll => " INTERSECT ALL ",
8266 UnionKind::Except => " EXCEPT ",
8267 UnionKind::ExceptAll => " EXCEPT ALL ",
8268 })?;
8269 write_bare_select(peer, f)?;
8270 }
8271 if !self.order_by.is_empty() {
8272 f.write_str(" ORDER BY ")?;
8273 for (i, o) in self.order_by.iter().enumerate() {
8274 if i > 0 {
8275 f.write_str(", ")?;
8276 }
8277 write!(f, "{}", o.expr)?;
8278 if o.desc {
8279 f.write_str(" DESC")?;
8280 }
8281 match o.nulls_first {
8282 Some(true) => f.write_str(" NULLS FIRST")?,
8283 Some(false) => f.write_str(" NULLS LAST")?,
8284 None => {}
8285 }
8286 }
8287 }
8288 // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
8289 // exists in the FETCH FIRST spelling; rendering it as LIMIT
8290 // dropped the tie-extension semantics on replay. The parser
8291 // accepts OFFSET before FETCH, so keep that order here.
8292 if self.limit_with_ties {
8293 if let Some(o) = &self.offset {
8294 write!(f, " OFFSET {o}")?;
8295 }
8296 if let Some(n) = &self.limit {
8297 write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
8298 }
8299 } else {
8300 if let Some(n) = &self.limit {
8301 write!(f, " LIMIT {n}")?;
8302 }
8303 if let Some(o) = &self.offset {
8304 write!(f, " OFFSET {o}")?;
8305 }
8306 }
8307 Ok(())
8308 }
8309}
8310
8311fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8312 f.write_str("SELECT ")?;
8313 if s.distinct {
8314 f.write_str("DISTINCT ")?;
8315 }
8316 write_bare_select_body(s, f)
8317}
8318
8319fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8320 for (i, item) in s.items.iter().enumerate() {
8321 if i > 0 {
8322 f.write_str(", ")?;
8323 }
8324 write!(f, "{item}")?;
8325 }
8326 if let Some(t) = &s.from {
8327 write!(f, " FROM {t}")?;
8328 }
8329 if let Some(e) = &s.where_ {
8330 write!(f, " WHERE {e}")?;
8331 }
8332 if let Some(gs) = &s.group_by {
8333 f.write_str(" GROUP BY ")?;
8334 for (i, g) in gs.iter().enumerate() {
8335 if i > 0 {
8336 f.write_str(", ")?;
8337 }
8338 write!(f, "{g}")?;
8339 }
8340 } else if s.group_by_all {
8341 // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
8342 // shortcut parses to group_by: None + this flag; dropping
8343 // it turned an aggregate query into a bare projection on
8344 // re-parse.
8345 f.write_str(" GROUP BY ALL")?;
8346 }
8347 if let Some(h) = &s.having {
8348 write!(f, " HAVING {h}")?;
8349 }
8350 Ok(())
8351}
8352
8353impl fmt::Display for SelectItem {
8354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8355 match self {
8356 Self::Wildcard => f.write_str("*"),
8357 Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
8358 Self::Expr { expr, alias } => {
8359 write!(f, "{expr}")?;
8360 if let Some(a) = alias {
8361 write!(f, " AS {}", quote_ident(a))?;
8362 }
8363 Ok(())
8364 }
8365 }
8366 }
8367}
8368
8369impl fmt::Display for FromClause {
8370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8371 write!(f, "{}", self.primary)?;
8372 for j in &self.joins {
8373 match j.kind {
8374 JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
8375 JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
8376 JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
8377 JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
8378 JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
8379 JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
8380 }
8381 if let Some(on) = &j.on {
8382 write!(f, " ON {on}")?;
8383 }
8384 }
8385 Ok(())
8386 }
8387}
8388
8389/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
8390/// for NESTED). Kept close to the parser's grammar so it re-parses.
8391fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
8392 for (i, c) in cols.iter().enumerate() {
8393 if i > 0 {
8394 f.write_str(", ")?;
8395 }
8396 match c {
8397 JsonTableColumn::Ordinality { name } => {
8398 write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
8399 }
8400 JsonTableColumn::Nested { path, columns } => {
8401 write!(f, "NESTED PATH '{path}' COLUMNS (")?;
8402 fmt_json_table_columns(f, columns)?;
8403 f.write_str(")")?;
8404 }
8405 JsonTableColumn::Regular {
8406 name,
8407 ty,
8408 path,
8409 exists,
8410 format_json,
8411 wrapper,
8412 on_empty,
8413 on_error,
8414 } => {
8415 write!(f, "{} {ty}", quote_ident(name))?;
8416 if *format_json {
8417 f.write_str(" FORMAT JSON")?;
8418 }
8419 if *exists {
8420 write!(f, " EXISTS PATH '{path}'")?;
8421 } else {
8422 write!(f, " PATH '{path}'")?;
8423 }
8424 if *wrapper {
8425 f.write_str(" WITH WRAPPER")?;
8426 }
8427 if let JsonTableOnBehavior::Error = on_empty {
8428 f.write_str(" ERROR ON EMPTY")?;
8429 } else if let JsonTableOnBehavior::Default(e) = on_empty {
8430 write!(f, " DEFAULT {e} ON EMPTY")?;
8431 }
8432 if let JsonTableOnBehavior::Error = on_error {
8433 f.write_str(" ERROR ON ERROR")?;
8434 } else if let JsonTableOnBehavior::Default(e) = on_error {
8435 write!(f, " DEFAULT {e} ON ERROR")?;
8436 }
8437 }
8438 }
8439 }
8440 Ok(())
8441}
8442
8443impl fmt::Display for TableRef {
8444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8445 // v7.30.1 (mailrs round-24 class audit) — the dynamic
8446 // table-ref shapes must round-trip: rendering only the
8447 // (synthetic) name turned LATERAL / unnest() /
8448 // generate_series() into references to nonexistent tables
8449 // on re-parse.
8450 // v7.39 (round 205) — JSON_TABLE round-trips through Display
8451 // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
8452 if let Some(jt) = &self.json_table {
8453 write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
8454 if !jt.passing.is_empty() {
8455 f.write_str(" PASSING ")?;
8456 for (i, (n, e)) in jt.passing.iter().enumerate() {
8457 if i > 0 {
8458 f.write_str(", ")?;
8459 }
8460 write!(f, "{e} AS {}", quote_ident(n))?;
8461 }
8462 }
8463 f.write_str(" COLUMNS (")?;
8464 fmt_json_table_columns(f, &jt.columns)?;
8465 f.write_str(")")?;
8466 if let Some(a) = &self.alias {
8467 write!(f, " AS {}", quote_ident(a))?;
8468 }
8469 return Ok(());
8470 }
8471 if let Some(inner) = &self.lateral_subquery {
8472 write!(f, "LATERAL ({inner})")?;
8473 if let Some(a) = &self.alias {
8474 write!(f, " AS {}", quote_ident(a))?;
8475 // v7.37 D.28 — a derived table on the lateral_subquery channel
8476 // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
8477 // lowers here). Rendering the alias without the column list lost
8478 // the column names on re-parse (a view body round-trips through
8479 // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
8480 if !self.unnest_column_aliases.is_empty() {
8481 f.write_str(" (")?;
8482 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8483 if i > 0 {
8484 f.write_str(", ")?;
8485 }
8486 f.write_str("e_ident(c))?;
8487 }
8488 f.write_str(")")?;
8489 }
8490 }
8491 return Ok(());
8492 }
8493 if let Some(expr) = &self.unnest_expr {
8494 write!(f, "UNNEST({expr})")?;
8495 if let Some(a) = &self.alias {
8496 write!(f, " AS {}", quote_ident(a))?;
8497 if !self.unnest_column_aliases.is_empty() {
8498 f.write_str(" (")?;
8499 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8500 if i > 0 {
8501 f.write_str(", ")?;
8502 }
8503 f.write_str("e_ident(c))?;
8504 }
8505 f.write_str(")")?;
8506 }
8507 }
8508 return Ok(());
8509 }
8510 // 7.38.1 S5.1 — a FROM-position table function must re-render
8511 // as the CALL, not its bare name: ARRAY(subquery) desugars by
8512 // re-parsing the subquery's canonical text, and a dropped
8513 // argument list turned `pg_options_to_table(x)` into a
8514 // relation lookup that does not exist.
8515 if let Some(call) = &self.table_fn_call {
8516 let (fn_name, args) = call.as_ref();
8517 write!(f, "{fn_name}(")?;
8518 for (i, a) in args.iter().enumerate() {
8519 if i > 0 {
8520 f.write_str(", ")?;
8521 }
8522 write!(f, "{a}")?;
8523 }
8524 f.write_str(")")?;
8525 if let Some(a) = &self.alias {
8526 write!(f, " AS {}", quote_ident(a))?;
8527 if !self.unnest_column_aliases.is_empty() {
8528 f.write_str("(")?;
8529 for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8530 if i > 0 {
8531 f.write_str(", ")?;
8532 }
8533 write!(f, "{}", quote_ident(c))?;
8534 }
8535 f.write_str(")")?;
8536 }
8537 }
8538 return Ok(());
8539 }
8540 if let Some(args) = &self.generate_series_args {
8541 f.write_str("generate_series(")?;
8542 for (i, a) in args.iter().enumerate() {
8543 if i > 0 {
8544 f.write_str(", ")?;
8545 }
8546 write!(f, "{a}")?;
8547 }
8548 f.write_str(")")?;
8549 if let Some(a) = &self.alias {
8550 write!(f, " AS {}", quote_ident(a))?;
8551 }
8552 return Ok(());
8553 }
8554 write!(f, "{}", quote_ident(&self.name))?;
8555 if let Some(seg) = self.as_of_segment {
8556 write!(f, " AS OF SEGMENT {seg}")?;
8557 }
8558 if let Some(a) = &self.alias {
8559 write!(f, " AS {}", quote_ident(a))?;
8560 }
8561 Ok(())
8562 }
8563}
8564
8565impl fmt::Display for ColumnName {
8566 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8567 if let Some(q) = &self.qualifier {
8568 write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
8569 } else {
8570 write!(f, "{}", quote_ident(&self.name))
8571 }
8572 }
8573}
8574
8575/// v7.39 (round 311) — render the left spine of an AND / OR chain
8576/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
8577/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
8578/// SAME operator flattens; anything else is an ordinary operand.
8579fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
8580 if let Expr::Binary {
8581 lhs,
8582 op: inner,
8583 rhs,
8584 } = e
8585 && *inner == op
8586 {
8587 write_bool_chain(f, lhs, op)?;
8588 return write!(f, " {op} {rhs}");
8589 }
8590 write!(f, "{e}")
8591}
8592
8593/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
8594/// form `pg_get_constraintdef(oid, true)` and friends return.
8595///
8596/// The default [`fmt::Display`] parenthesises every operator node, which
8597/// is what PG's non-pretty deparse does and what makes the text
8598/// round-trip. Pretty drops the pairs the grammar can put back, and the
8599/// rule is NOT plain precedence minimisation — measured against PG 18.4
8600/// across 37 shapes:
8601///
8602/// * the boolean layer follows precedence (NOT > AND > OR): an OR
8603/// under an AND keeps its parens, an AND under an OR does not, and a
8604/// comparison under any of them does not (`NOT a > 1`);
8605/// * an associative chain flattens completely, even where the source
8606/// nested it to the right (`a AND (b AND c)` prints as one chain);
8607/// * but an operand of a comparison or arithmetic operator keeps its
8608/// parens whenever it is itself an operator expression — so
8609/// `(a + b) > 10` and `(- a) + b`, even though precedence alone
8610/// would not require either. A cast, function call, column or
8611/// literal in that position does not (`a::text = t`,
8612/// `length(code) > 2`); a cast counts as compound exactly when the
8613/// thing it casts is (`((a + b)::text) = t`).
8614///
8615/// Anything outside that layer defers to `Display`, which is never
8616/// wrong — only more parenthesised than PG would print.
8617#[must_use]
8618pub fn pretty_expr(e: &Expr) -> String {
8619 let mut out = String::new();
8620 write_pretty(&mut out, e, PrettyParent::None, false, false);
8621 out
8622}
8623
8624/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
8625/// writes it.
8626///
8627/// MariaDB names the offending expression in its out-of-range message
8628/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
8629/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
8630/// MySQL client, for a cast the client had just written the other way.
8631#[must_use]
8632pub fn pretty_expr_mysql(e: &Expr) -> String {
8633 let mut out = String::new();
8634 write_pretty(&mut out, e, PrettyParent::None, false, true);
8635 out
8636}
8637
8638/// v7.39 (round 505) — how strongly an expression suggests its own column
8639/// name. A cast keeps its argument's name only when that name is STRONG;
8640/// otherwise the cast reports the type it casts to.
8641///
8642/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
8643/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
8644/// itself `text` — so `case` and a function name cannot be the same kind of
8645/// answer, even though a bare `CASE …` does report `case`.
8646#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8647enum NameStrength {
8648 /// Nothing to go on — PG reports `?column?`.
8649 None,
8650 /// A name, but one a cast overrides: `case`, or a type name.
8651 Weak,
8652 /// A name a cast keeps: a column, or the function that produced it.
8653 Strong,
8654}
8655
8656/// v7.39 (round 505) — the column name PG18 gives a projected expression
8657/// that carries no `AS` alias. `None` means `?column?`.
8658///
8659/// SPG used to print the parsed expression back out, which matched neither
8660/// oracle and made name-keyed row access miss on both wires:
8661///
8662/// | query | PG18 | SPG (before) |
8663/// |--------------|------------|--------------|
8664/// | `upper(s)` | `upper` | `upper(s)` |
8665/// | `a+b` | `?column?` | `(a + b)` |
8666/// | `'lit'` | `?column?` | `'lit'` |
8667/// | `CASE …` | `case` | `CASE WHEN (a = 1) THEN …` |
8668///
8669/// Every rule below is one of those measurements, taken with `\gdesc`
8670/// against PG18: a call is named for its function, a cast recurses into its
8671/// argument and falls back to the type, a scalar subquery takes the name of
8672/// the column it selects, and operators have no name at all.
8673#[must_use]
8674pub fn figure_column_name(expr: &Expr) -> Option<String> {
8675 let (name, _) = figure_name_inner(expr);
8676 name
8677}
8678
8679/// The name a function reports, which is not always the name SPG parsed it
8680/// under: `count(*)` is held as `count_star` so the star arity survives the
8681/// AST, and that internal spelling must not reach a client. PG18 reports
8682/// `count`.
8683/// v7.39.13 — public, because Describe was naming the same call from a
8684/// second map that did not have this entry.
8685///
8686/// `count(*)` is held as `count_star` so the star arity survives the
8687/// AST. The projection mapped it back and the extended protocol's
8688/// Describe did not, so `SELECT count(*) OVER ()` answered `count` in
8689/// the row stream and `count_star` to `\gdesc` — an ORM-visible column
8690/// name, and two answers to one question. Reported by sentori against
8691/// 7.39.12.
8692#[must_use]
8693pub fn canonical_function_name(name: &str) -> String {
8694 match name {
8695 "count_star" => "count".to_string(),
8696 other => other.to_ascii_lowercase(),
8697 }
8698}
8699
8700/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
8701/// reports when its operand has none of its own. Only the spellings that
8702/// differ from what the user writes need an entry; everything else is
8703/// already its own typname.
8704fn cast_target_typname(target: &CastTarget) -> String {
8705 let written = target.to_string().to_ascii_lowercase();
8706 let base = written.strip_suffix("[]").unwrap_or(&written);
8707 let mapped = match base {
8708 "bigint" => "int8",
8709 "integer" | "int" => "int4",
8710 "smallint" => "int2",
8711 "boolean" => "bool",
8712 "double precision" => "float8",
8713 "real" => "float4",
8714 "character varying" => "varchar",
8715 "character" => "bpchar",
8716 "timestamp with time zone" => "timestamptz",
8717 "timestamp without time zone" => "timestamp",
8718 "time without time zone" => "time",
8719 "decimal" => "numeric",
8720 other => other,
8721 };
8722 if written.ends_with("[]") {
8723 alloc::format!("_{mapped}")
8724 } else {
8725 String::from(mapped)
8726 }
8727}
8728
8729fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8730 let strong = |n: String| (Some(n), NameStrength::Strong);
8731 match expr {
8732 // A column keeps its own name, qualifier and all discarded:
8733 // `lbl.a` reports `a`.
8734 Expr::Column(c) => strong(c.name.clone()),
8735 // Calls are named for the function. This covers the shapes that
8736 // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8737 // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8738 // because PG resolves them to functions before naming them.
8739 Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8740 strong(canonical_function_name(name))
8741 }
8742 Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8743 Expr::Extract { .. } => strong("extract".to_string()),
8744 Expr::Exists { .. } => strong("exists".to_string()),
8745 Expr::Array(_) => strong("array".to_string()),
8746 // `(expr).field` is named for the field, as a column would be.
8747 Expr::FieldAccess { field, .. } => strong(field.clone()),
8748 // v7.39.12 — PostgreSQL names a subscript after its operand, so
8749 // `arr[1]` is `arr`. There was no arm, so it fell through to
8750 // `?column?`. Reported by sentori against 7.39.11 — the same
8751 // naming defect v7.38.20 closed, reached through a different
8752 // expression. Weak, like the field access above it: an outer
8753 // cast or function still names the column.
8754 Expr::ArraySubscript { target, .. } => (figure_name_inner(target).0, NameStrength::Weak),
8755 // A cast prefers its argument's name and settles for the type:
8756 // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8757 Expr::Cast {
8758 expr: inner,
8759 target,
8760 } => match figure_name_inner(inner) {
8761 (Some(n), NameStrength::Strong) => strong(n),
8762 // v7.38.7 — the fallback is the target type's INTERNAL name,
8763 // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8764 // the `bigint` the user typed. Measured on PG18 alongside
8765 // `CAST(7 AS bigint)`, which answers `int8` too.
8766 _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8767 },
8768 // A scalar subquery reports whatever its single output column
8769 // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8770 Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8771 // `CASE …` names itself, but weakly — a cast around it wins.
8772 Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8773 // A literal that carries its own type names itself for that type:
8774 // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8775 // reports nothing. Weak, like any other type name.
8776 Expr::Literal(Literal::Interval { .. }) => {
8777 (Some("interval".to_string()), NameStrength::Weak)
8778 }
8779 // A wrapper that adds no name of its own.
8780 Expr::Variadic(inner) => figure_name_inner(inner),
8781 Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
8782 // Everything else — operators, comparisons, IS NULL, LIKE, IN,
8783 // literals, placeholders — reports `?column?`.
8784 _ => (None, NameStrength::None),
8785 }
8786}
8787
8788/// The name a scalar subquery's single projected column reports.
8789fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
8790 match sel.items.as_slice() {
8791 [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
8792 [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
8793 _ => (None, NameStrength::None),
8794 }
8795}
8796
8797/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
8798fn pretty_prec(e: &Expr) -> u8 {
8799 match e {
8800 Expr::Binary { op, .. } => match op {
8801 // v7.39 (round 407) — this deparse ladder mirrors the parser's:
8802 // OR < XOR < AND < NOT < comparison < additive < multiplicative.
8803 // XOR (MySQL-only) sits between OR and AND, so AND and everything
8804 // above shifted +1 to open rung 2 for it.
8805 BinOp::Or => 1,
8806 BinOp::LogicalXor => 2,
8807 BinOp::And => 3,
8808 BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
8809 BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
8810 // Everything else in this enum is a comparison-shaped
8811 // operator; they share one level, as in the grammar.
8812 _ => 5,
8813 },
8814 Expr::Unary { op, .. } => match op {
8815 UnOp::Not => 4,
8816 UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
8817 },
8818 _ => u8::MAX,
8819 }
8820}
8821
8822/// Is this node an operator expression — the thing an arithmetic or
8823/// comparison parent keeps parentheses around? A cast inherits the
8824/// answer from what it casts.
8825fn pretty_is_compound(e: &Expr) -> bool {
8826 match e {
8827 Expr::Binary { .. } | Expr::Unary { .. } => true,
8828 Expr::Cast { expr, .. } => pretty_is_compound(expr),
8829 _ => false,
8830 }
8831}
8832
8833/// `parent` describes the enclosing operator: its binding power, and
8834/// whether it is a comparison (which keeps parens around any operator
8835/// operand) or a NOT (which keeps them at equal power too).
8836#[derive(Clone, Copy, PartialEq)]
8837enum PrettyParent {
8838 /// Nothing encloses this node.
8839 None,
8840 /// A comparison-shaped operator: an operator operand always keeps
8841 /// its parens, whatever precedence would allow.
8842 Comparison,
8843 /// Arithmetic / concatenation: precedence decides.
8844 Arith(u8),
8845 /// A boolean connective: precedence decides.
8846 Bool(u8),
8847 /// `NOT`: precedence decides, but equal power still needs parens so
8848 /// `NOT (NOT a > 1)` does not collapse.
8849 Not,
8850}
8851
8852fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
8853 let prec = pretty_prec(e);
8854 let is_unary_sign = matches!(
8855 e,
8856 Expr::Unary {
8857 op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
8858 ..
8859 }
8860 );
8861 let needs = match parent {
8862 PrettyParent::None => false,
8863 PrettyParent::Comparison => pretty_is_compound(e),
8864 // A sign always keeps its parens under an operator — PG writes
8865 // `(- a) + b` even though precedence would not require it.
8866 PrettyParent::Arith(p) => {
8867 is_unary_sign
8868 || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
8869 && (prec < p || (prec == p && is_rhs)))
8870 }
8871 PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
8872 PrettyParent::Not => {
8873 matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
8874 }
8875 };
8876 if needs {
8877 out.push('(');
8878 }
8879 match e {
8880 Expr::Binary { lhs, op, rhs } => {
8881 let child = match op {
8882 BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
8883 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
8884 PrettyParent::Arith(prec)
8885 }
8886 _ => PrettyParent::Comparison,
8887 };
8888 write_pretty(out, lhs, child, false, mysql);
8889 out.push(' ');
8890 out.push_str(&alloc::format!("{op}"));
8891 out.push(' ');
8892 // AND / OR are associative, so an explicitly right-nested
8893 // chain still prints as one chain.
8894 let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
8895 write_pretty(out, rhs, child, rhs_is_rhs, mysql);
8896 }
8897 Expr::Unary { op, expr } => match op {
8898 UnOp::Not => {
8899 out.push_str("NOT ");
8900 write_pretty(out, expr, PrettyParent::Not, false, mysql);
8901 }
8902 UnOp::Neg => {
8903 out.push_str("- ");
8904 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8905 }
8906 UnOp::Plus => {
8907 out.push_str("+ ");
8908 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8909 }
8910 UnOp::BitNot => {
8911 out.push('~');
8912 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8913 }
8914 },
8915 Expr::Cast { expr, target } => {
8916 if mysql {
8917 // MySQL's own spelling, which is what its error messages
8918 // quote back.
8919 out.push_str("cast(");
8920 write_pretty(out, expr, PrettyParent::None, false, mysql);
8921 out.push_str(&alloc::format!(
8922 " as {})",
8923 target.to_string().to_lowercase()
8924 ));
8925 } else {
8926 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8927 out.push_str(&alloc::format!("::{target}"));
8928 }
8929 }
8930 Expr::IsNull { expr, negated } => {
8931 write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8932 out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
8933 }
8934 other => out.push_str(&alloc::format!("{other}")),
8935 }
8936 if needs {
8937 out.push(')');
8938 }
8939}
8940
8941const fn pretty_prec_not() -> u8 {
8942 // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
8943 // when the XOR insertion shifted the deparse ladder up by one).
8944 4
8945}
8946
8947impl fmt::Display for Expr {
8948 #[allow(clippy::too_many_lines)]
8949 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8950 match self {
8951 Self::Literal(l) => write!(f, "{l}"),
8952 Self::Column(c) => write!(f, "{c}"),
8953 Self::Placeholder(n) => write!(f, "${n}"),
8954 // Round-trips as the spelling PG's docs lead with.
8955 Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
8956 Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
8957 // Round-trips with the name quoted, which is how PG spells a
8958 // collation everywhere: `"en_US.utf8"`, `"C"`.
8959 Self::Collate { expr, collation } => {
8960 write!(f, "{expr} COLLATE {}", quote_ident(collation))
8961 }
8962 // v7.39 (round 311) — an AND / OR chain that nests to the
8963 // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
8964 // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
8965 // its parentheses, because that is a different grouping as
8966 // written. Both halves measured against PG 18.4's deparse,
8967 // which flattens a same-operator left chain at parse time and
8968 // leaves `a AND (b AND c)` alone.
8969 Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
8970 f.write_str("(")?;
8971 write_bool_chain(f, lhs, *op)?;
8972 write!(f, " {op} {rhs}")?;
8973 f.write_str(")")
8974 }
8975 Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
8976 Self::Unary { op, expr } => match op {
8977 UnOp::Not => write!(f, "(NOT {expr})"),
8978 // A space after the sign, as PG's deparse writes it.
8979 UnOp::Neg => write!(f, "(- {expr})"),
8980 UnOp::Plus => write!(f, "(+ {expr})"),
8981 UnOp::BitNot => write!(f, "(~{expr})"),
8982 },
8983 // The OPERAND carries the parentheses, not the cast:
8984 // `(a)::text`, `((a + b))::text`. PG words it this way, and
8985 // it is what keeps `a::text = t` from reading as a cast of
8986 // the comparison.
8987 Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
8988 Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
8989 Self::AggregateOrdered {
8990 call,
8991 order_by,
8992 distinct,
8993 filter,
8994 } => {
8995 let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8996 for (i, o) in order_by.iter().enumerate() {
8997 if i > 0 {
8998 f.write_str(", ")?;
8999 }
9000 write!(f, "{}", o.expr)?;
9001 if o.desc {
9002 f.write_str(" DESC")?;
9003 }
9004 match o.nulls_first {
9005 Some(true) => f.write_str(" NULLS FIRST")?,
9006 Some(false) => f.write_str(" NULLS LAST")?,
9007 None => {}
9008 }
9009 }
9010 Ok(())
9011 };
9012 // Ordered-set aggregates (`percentile_cont(f) WITHIN
9013 // GROUP (ORDER BY x)`) render the in-parens args as the
9014 // direct argument and the sort spec under WITHIN GROUP —
9015 // not as an in-argument ORDER BY.
9016 let ordered_set = matches!(
9017 call.as_ref(),
9018 Expr::FunctionCall { name, .. }
9019 if matches!(
9020 name.to_ascii_lowercase().as_str(),
9021 "percentile_cont" | "percentile_disc" | "mode"
9022 )
9023 );
9024 if ordered_set {
9025 write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
9026 fmt_order_by(f)?;
9027 f.write_str(")")?;
9028 } else {
9029 // `name([DISTINCT ]args [ORDER BY …])` — peel the
9030 // inner call's parens to splice modifiers.
9031 let inner = alloc::format!("{call}");
9032 let body = inner.strip_suffix(')').unwrap_or(&inner);
9033 let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
9034 write!(f, "{head}(")?;
9035 if *distinct {
9036 f.write_str("DISTINCT ")?;
9037 }
9038 write!(f, "{args_part}")?;
9039 if !order_by.is_empty() {
9040 f.write_str(" ORDER BY ")?;
9041 fmt_order_by(f)?;
9042 }
9043 f.write_str(")")?;
9044 }
9045 if let Some(cond) = filter {
9046 write!(f, " FILTER (WHERE {cond})")?;
9047 }
9048 Ok(())
9049 }
9050 Self::IsNull { expr, negated } => {
9051 if *negated {
9052 write!(f, "({expr} IS NOT NULL)")
9053 } else {
9054 write!(f, "({expr} IS NULL)")
9055 }
9056 }
9057 Self::BoolTest {
9058 expr,
9059 value,
9060 negated,
9061 } => {
9062 let word = match value {
9063 Some(true) => "TRUE",
9064 Some(false) => "FALSE",
9065 None => "UNKNOWN",
9066 };
9067 if *negated {
9068 write!(f, "({expr} IS NOT {word})")
9069 } else {
9070 write!(f, "({expr} IS {word})")
9071 }
9072 }
9073 Self::FunctionCall { name, args } => {
9074 write!(f, "{name}(")?;
9075 for (i, a) in args.iter().enumerate() {
9076 if i > 0 {
9077 f.write_str(", ")?;
9078 }
9079 write!(f, "{a}")?;
9080 }
9081 f.write_str(")")
9082 }
9083 Self::Like {
9084 expr,
9085 pattern,
9086 negated,
9087 case_insensitive,
9088 } => {
9089 let op = match (negated, case_insensitive) {
9090 (false, false) => "LIKE",
9091 (true, false) => "NOT LIKE",
9092 (false, true) => "ILIKE",
9093 (true, true) => "NOT ILIKE",
9094 };
9095 write!(f, "({expr} {op} {pattern})")
9096 }
9097 Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
9098 Self::WindowFunction {
9099 name,
9100 args,
9101 partition_by,
9102 order_by,
9103 frame,
9104 null_treatment,
9105 filter,
9106 } => {
9107 write!(f, "{name}(")?;
9108 for (i, a) in args.iter().enumerate() {
9109 if i > 0 {
9110 f.write_str(", ")?;
9111 }
9112 write!(f, "{a}")?;
9113 }
9114 f.write_str(")")?;
9115 // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
9116 // OVER; it round-trips so a window body's Display re-parses.
9117 if let Some(cond) = filter {
9118 write!(f, " FILTER (WHERE {cond})")?;
9119 }
9120 // v7.30.1 (mailrs round-24 class audit) — IGNORE
9121 // NULLS sits between the arg list and OVER; dropping
9122 // it reverted replayed queries to RESPECT NULLS.
9123 if matches!(null_treatment, NullTreatment::Ignore) {
9124 f.write_str(" IGNORE NULLS")?;
9125 }
9126 f.write_str(" OVER (")?;
9127 if !partition_by.is_empty() {
9128 f.write_str("PARTITION BY ")?;
9129 for (i, p) in partition_by.iter().enumerate() {
9130 if i > 0 {
9131 f.write_str(", ")?;
9132 }
9133 write!(f, "{p}")?;
9134 }
9135 }
9136 if !order_by.is_empty() {
9137 if !partition_by.is_empty() {
9138 f.write_str(" ")?;
9139 }
9140 f.write_str("ORDER BY ")?;
9141 for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
9142 if i > 0 {
9143 f.write_str(", ")?;
9144 }
9145 write!(f, "{e}")?;
9146 if *desc {
9147 f.write_str(" DESC")?;
9148 }
9149 match nulls_first {
9150 Some(true) => f.write_str(" NULLS FIRST")?,
9151 Some(false) => f.write_str(" NULLS LAST")?,
9152 None => {}
9153 }
9154 }
9155 }
9156 if let Some(fr) = frame {
9157 if !partition_by.is_empty() || !order_by.is_empty() {
9158 f.write_str(" ")?;
9159 }
9160 let k = match fr.kind {
9161 FrameKind::Rows => "ROWS",
9162 FrameKind::Range => "RANGE",
9163 FrameKind::Groups => "GROUPS",
9164 };
9165 if let Some(end) = &fr.end {
9166 write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
9167 } else {
9168 write!(f, "{k} {}", fr.start)?;
9169 }
9170 }
9171 f.write_str(")")
9172 }
9173 Self::ScalarSubquery(s) => write!(f, "({s})"),
9174 Self::Exists { subquery, negated } => {
9175 if *negated {
9176 write!(f, "NOT EXISTS ({subquery})")
9177 } else {
9178 write!(f, "EXISTS ({subquery})")
9179 }
9180 }
9181 Self::InSubquery {
9182 expr,
9183 subquery,
9184 negated,
9185 } => {
9186 if *negated {
9187 write!(f, "({expr} NOT IN ({subquery}))")
9188 } else {
9189 write!(f, "({expr} IN ({subquery}))")
9190 }
9191 }
9192 Self::RowInSubquery {
9193 row,
9194 subquery,
9195 negated,
9196 } => {
9197 write!(f, "(")?;
9198 for (i, e) in row.iter().enumerate() {
9199 if i > 0 {
9200 write!(f, ", ")?;
9201 }
9202 write!(f, "{e}")?;
9203 }
9204 let kw = if *negated { ") NOT IN (" } else { ") IN (" };
9205 write!(f, "{kw}{subquery})")
9206 }
9207 Self::RowCmpSubquery { row, op, subquery } => {
9208 write!(f, "(")?;
9209 for (i, e) in row.iter().enumerate() {
9210 if i > 0 {
9211 write!(f, ", ")?;
9212 }
9213 write!(f, "{e}")?;
9214 }
9215 write!(f, ") {op} ({subquery})")
9216 }
9217 Self::InList {
9218 expr,
9219 list,
9220 negated,
9221 } => {
9222 let kw = if *negated { " NOT IN (" } else { " IN (" };
9223 write!(f, "({expr}{kw}")?;
9224 for (i, e) in list.iter().enumerate() {
9225 if i > 0 {
9226 f.write_str(", ")?;
9227 }
9228 write!(f, "{e}")?;
9229 }
9230 f.write_str("))")
9231 }
9232 Self::Array(items) => {
9233 f.write_str("ARRAY[")?;
9234 for (i, e) in items.iter().enumerate() {
9235 if i > 0 {
9236 f.write_str(", ")?;
9237 }
9238 write!(f, "{e}")?;
9239 }
9240 f.write_str("]")
9241 }
9242 Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
9243 Self::ArraySlice { target, lo, hi } => {
9244 write!(f, "({target}[")?;
9245 if let Some(l) = lo {
9246 write!(f, "{l}")?;
9247 }
9248 write!(f, ":")?;
9249 if let Some(h) = hi {
9250 write!(f, "{h}")?;
9251 }
9252 write!(f, "])")
9253 }
9254 Self::AnyAll {
9255 expr,
9256 op,
9257 array,
9258 is_any,
9259 } => {
9260 let kw = if *is_any { "ANY" } else { "ALL" };
9261 write!(f, "({expr} {op} {kw}({array}))")
9262 }
9263 Self::Case {
9264 operand,
9265 branches,
9266 else_branch,
9267 } => {
9268 f.write_str("CASE")?;
9269 if let Some(op) = operand {
9270 write!(f, " {op}")?;
9271 }
9272 for (w, t) in branches {
9273 write!(f, " WHEN {w} THEN {t}")?;
9274 }
9275 if let Some(e) = else_branch {
9276 write!(f, " ELSE {e}")?;
9277 }
9278 f.write_str(" END")
9279 }
9280 }
9281 }
9282}
9283
9284/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
9285/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
9286pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
9287 use alloc::string::ToString;
9288 if scale == 0 {
9289 return alloc::format!("{unscaled}");
9290 }
9291 let neg = unscaled < 0;
9292 let digits = alloc::format!("{}", unscaled.unsigned_abs());
9293 let scale = scale as usize;
9294 let (int_part, frac_part) = if digits.len() > scale {
9295 (
9296 digits[..digits.len() - scale].to_string(),
9297 digits[digits.len() - scale..].to_string(),
9298 )
9299 } else {
9300 ("0".to_string(), alloc::format!("{digits:0>scale$}"))
9301 };
9302 alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
9303}
9304
9305/// A single-quoted SQL string, with an embedded quote doubled.
9306fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
9307 f.write_str("'")?;
9308 for c in s.chars() {
9309 if c == '\'' {
9310 f.write_str("''")?;
9311 } else {
9312 write!(f, "{c}")?;
9313 }
9314 }
9315 f.write_str("'")
9316}
9317
9318impl fmt::Display for Literal {
9319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9320 match self {
9321 Self::Integer(n) => write!(f, "{n}"),
9322 Self::Float(x) => {
9323 let s = format!("{x}");
9324 // Default Display for an integral f64 (e.g. 1.0) emits "1",
9325 // which would round-trip back to Integer. Force a dot.
9326 if s.contains('.') || s.contains('e') || s.contains('E') {
9327 f.write_str(&s)
9328 } else {
9329 write!(f, "{s}.0")
9330 }
9331 }
9332 Self::Numeric { unscaled, scale } => {
9333 // Render the exact decimal `unscaled / 10^scale`, preserving
9334 // scale (trailing zeros) — round-trips to the same literal.
9335 f.write_str(&render_exact_decimal(*unscaled, *scale))
9336 }
9337 Self::NumericBig(s) => f.write_str(s),
9338 // Printed exactly as the text form was, so a reader cannot
9339 // tell whether the constant was decoded or not.
9340 Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
9341 Self::String(s) => write_quoted(f, s),
9342 Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
9343 Self::Null => f.write_str("NULL"),
9344 // PG external array form. Display round-trip re-enters
9345 // through the column-typed text coerce, same as pgwire.
9346 Self::TextArray(items) => {
9347 f.write_str("'{")?;
9348 for (i, it) in items.iter().enumerate() {
9349 if i > 0 {
9350 f.write_str(",")?;
9351 }
9352 match it {
9353 None => f.write_str("NULL")?,
9354 Some(s) => {
9355 f.write_str("\"")?;
9356 for c in s.chars() {
9357 match c {
9358 // array-element escapes
9359 '"' | '\\' => write!(f, "\\{c}")?,
9360 // the OUTER wrapper is a SQL string
9361 // literal — embedded quotes must
9362 // double, or the rendered form
9363 // (WAL replay parses it back) is
9364 // invalid SQL
9365 '\'' => f.write_str("''")?,
9366 _ => write!(f, "{c}")?,
9367 }
9368 }
9369 f.write_str("\"")?;
9370 }
9371 }
9372 }
9373 f.write_str("}'")
9374 }
9375 Self::IntArray(items) => {
9376 f.write_str("'{")?;
9377 for (i, it) in items.iter().enumerate() {
9378 if i > 0 {
9379 f.write_str(",")?;
9380 }
9381 match it {
9382 None => f.write_str("NULL")?,
9383 Some(n) => write!(f, "{n}")?,
9384 }
9385 }
9386 f.write_str("}'")
9387 }
9388 Self::BigIntArray(items) => {
9389 f.write_str("'{")?;
9390 for (i, it) in items.iter().enumerate() {
9391 if i > 0 {
9392 f.write_str(",")?;
9393 }
9394 match it {
9395 None => f.write_str("NULL")?,
9396 Some(n) => write!(f, "{n}")?,
9397 }
9398 }
9399 f.write_str("}'")
9400 }
9401 Self::Vector(v) => {
9402 f.write_str("[")?;
9403 for (i, x) in v.iter().enumerate() {
9404 if i > 0 {
9405 f.write_str(", ")?;
9406 }
9407 let s = format!("{x}");
9408 // Mirror Float Display: force a dot so re-parse stays
9409 // numerically literal.
9410 if s.contains('.') || s.contains('e') || s.contains('E') {
9411 f.write_str(&s)?;
9412 } else {
9413 write!(f, "{s}.0")?;
9414 }
9415 }
9416 f.write_str("]")
9417 }
9418 Self::Interval { text, .. } => {
9419 f.write_str("INTERVAL '")?;
9420 for c in text.chars() {
9421 if c == '\'' {
9422 f.write_str("''")?;
9423 } else {
9424 write!(f, "{c}")?;
9425 }
9426 }
9427 f.write_str("'")
9428 }
9429 }
9430 }
9431}
9432
9433impl fmt::Display for BinOp {
9434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9435 f.write_str(match self {
9436 Self::Or => "OR",
9437 Self::And => "AND",
9438 Self::Eq => "=",
9439 Self::NotEq => "<>",
9440 Self::IsDistinctFrom => "IS DISTINCT FROM",
9441 Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
9442 Self::IntDiv => "DIV",
9443 Self::Lt => "<",
9444 Self::LtEq => "<=",
9445 Self::Gt => ">",
9446 Self::GtEq => ">=",
9447 Self::Add => "+",
9448 Self::Sub => "-",
9449 Self::Mul => "*",
9450 Self::Div => "/",
9451 Self::Mod => "%",
9452 Self::L2Distance => "<->",
9453 Self::GeomParallel => "?||",
9454 Self::OverLeft => "&<",
9455 Self::OverRight => "&>",
9456 Self::GeomPerp => "?-|",
9457 Self::GeomSameAs => "~=",
9458 Self::ClosestPoint => "##",
9459 Self::GeomHoriz => "?-",
9460 Self::InnerProduct => "<#>",
9461 Self::CosineDistance => "<=>",
9462 Self::Concat => "||",
9463 Self::BitOr => "|",
9464 Self::BitAnd => "&",
9465 Self::BitXor => "#",
9466 Self::LogicalXor => "xor",
9467 Self::JsonGet => "->",
9468 Self::JsonGetText => "->>",
9469 Self::JsonGetPath => "#>",
9470 Self::JsonGetPathText => "#>>",
9471 Self::JsonContains => "@>",
9472 Self::JsonPathExists => "@?",
9473 Self::JsonContainedBy => "<@",
9474 Self::JsonKeyExists => "?",
9475 Self::JsonKeysAny => "?|",
9476 Self::JsonKeysAll => "?&",
9477 Self::JsonDeletePath => "#-",
9478 Self::TsMatch => "@@",
9479 Self::InetContainedBy => "<<",
9480 Self::InetContainedByEq => "<<=",
9481 Self::InetContains => ">>",
9482 Self::InetContainsEq => ">>=",
9483 Self::InetOverlap => "&&",
9484 Self::Intersects => "?#",
9485 Self::IsBelow => "<^",
9486 Self::IsAbove => ">^",
9487 Self::PatternLt => "~<~",
9488 Self::PatternLtEq => "~<=~",
9489 Self::PatternGt => "~>~",
9490 Self::PatternGtEq => "~>=~",
9491 })
9492 }
9493}
9494
9495/// Quote `s` as a PG double-quoted identifier when required (keyword,
9496/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
9497/// Otherwise return it as-is. Returns an owned `String` to keep the call site
9498/// uniform.
9499pub(crate) fn quote_ident(s: &str) -> String {
9500 let needs_quote = match s.chars().next() {
9501 None => true,
9502 Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
9503 _ => {
9504 s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
9505 || s.chars().any(|c| c.is_ascii_uppercase())
9506 || is_keyword(s)
9507 }
9508 };
9509 if !needs_quote {
9510 return s.to_string();
9511 }
9512 let mut out = String::with_capacity(s.len() + 2);
9513 out.push('"');
9514 for c in s.chars() {
9515 if c == '"' {
9516 out.push_str("\"\"");
9517 } else {
9518 out.push(c);
9519 }
9520 }
9521 out.push('"');
9522 out
9523}
9524
9525fn is_keyword(s: &str) -> bool {
9526 matches!(
9527 &*s.to_ascii_lowercase(),
9528 "select"
9529 | "from"
9530 | "where"
9531 | "as"
9532 | "null"
9533 | "true"
9534 | "false"
9535 | "and"
9536 | "or"
9537 | "not"
9538 | "create"
9539 | "table"
9540 | "insert"
9541 | "into"
9542 | "values"
9543 | "index"
9544 | "on"
9545 | "begin"
9546 | "commit"
9547 | "rollback"
9548 | "is"
9549 | "between"
9550 | "in"
9551 | "like"
9552 | "group"
9553 | "distinct"
9554 | "union"
9555 | "all"
9556 | "join"
9557 | "inner"
9558 | "left"
9559 | "cross"
9560 | "outer"
9561 | "default"
9562 | "savepoint"
9563 | "release"
9564 | "to"
9565 | "having"
9566 | "show"
9567 | "extract"
9568 | "offset"
9569 | "asc"
9570 | "desc"
9571 | "interval"
9572 )
9573}
9574
9575#[cfg(test)]
9576mod tests {
9577 use super::*;
9578 use alloc::vec;
9579
9580 #[test]
9581 fn integer_literal_renders_without_dot() {
9582 assert_eq!(Literal::Integer(42).to_string(), "42");
9583 }
9584
9585 #[test]
9586 fn integral_float_keeps_dot() {
9587 assert_eq!(Literal::Float(1.0).to_string(), "1.0");
9588 assert_eq!(Literal::Float(1.5).to_string(), "1.5");
9589 assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
9590 }
9591
9592 #[test]
9593 fn string_literal_doubles_quote() {
9594 assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
9595 }
9596
9597 #[test]
9598 fn bool_and_null_render_uppercase() {
9599 assert_eq!(Literal::Bool(true).to_string(), "TRUE");
9600 assert_eq!(Literal::Bool(false).to_string(), "FALSE");
9601 assert_eq!(Literal::Null.to_string(), "NULL");
9602 }
9603
9604 #[test]
9605 fn binary_op_always_parenthesised() {
9606 let e = Expr::Binary {
9607 lhs: Box::new(Expr::Literal(Literal::Integer(1))),
9608 op: BinOp::Add,
9609 rhs: Box::new(Expr::Literal(Literal::Integer(2))),
9610 };
9611 assert_eq!(e.to_string(), "(1 + 2)");
9612 }
9613
9614 #[test]
9615 fn select_star_from_table() {
9616 let s = SelectStatement {
9617 locking: None,
9618 items: vec![SelectItem::Wildcard],
9619 from: Some(FromClause {
9620 primary: TableRef {
9621 name: "users".into(),
9622 alias: None,
9623 only: false,
9624 as_of_segment: None,
9625 unnest_expr: None,
9626 unnest_column_aliases: Vec::new(),
9627 with_ordinality: false,
9628 generate_series_args: None,
9629 lateral_subquery: None,
9630 jsonb_each_text_arg: None,
9631 table_fn_call: None,
9632 rows_from: None,
9633 json_table: None,
9634 scalar_fn_item: false,
9635 },
9636 joins: vec![],
9637 }),
9638 where_: None,
9639 group_by: None,
9640 group_by_all: false,
9641 having: None,
9642 unions: vec![],
9643 order_by: Vec::new(),
9644 limit: None,
9645 offset: None,
9646 limit_with_ties: false,
9647 window_check_exprs: Vec::new(),
9648 distinct: false,
9649 distinct_on: Vec::new(),
9650 ctes: vec![],
9651 };
9652 assert_eq!(s.to_string(), "SELECT * FROM users");
9653 }
9654
9655 #[test]
9656 fn quote_ident_for_uppercase_and_keyword() {
9657 assert_eq!(quote_ident("foo"), "foo");
9658 assert_eq!(quote_ident("Foo"), "\"Foo\"");
9659 assert_eq!(quote_ident("select"), "\"select\"");
9660 assert_eq!(quote_ident(""), "\"\"");
9661 assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
9662 }
9663}