Skip to main content

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    /// v7.40.11 — MySQL only. `SET character_set_results = NULL` is the
1076    /// second statement Connector/J sends on every connection, and it
1077    /// means "send results in the column's own charset, do not
1078    /// transcode". PostgreSQL 18.6 answers `syntax error at or near
1079    /// "NULL"` for the same shape (measured), so the parser produces
1080    /// this variant only for a MySQL-dialect session.
1081    Null,
1082}
1083
1084/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1085/// at parse time and tracks the selected value on the engine. The
1086/// actual semantic differentiation (REPEATABLE READ snapshot,
1087/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1088/// today every level reads as effective READ COMMITTED (which is
1089/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1090/// READ COMMITTED). Default = `ReadCommitted`.
1091#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1092pub enum IsolationLevel {
1093    ReadUncommitted,
1094    #[default]
1095    ReadCommitted,
1096    RepeatableRead,
1097    Serializable,
1098}
1099
1100impl IsolationLevel {
1101    /// Canonical PG-style display name, as `SHOW transaction_isolation`
1102    /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1103    /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1104    /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1105    /// `read uncommitted`) and only BEHAVES as read committed; the old
1106    /// fold renamed the label too.
1107    /// v7.39 — the MySQL display name, which is NOT the PG one: MySQL
1108    /// hyphenates and upper-cases. Measured on MySQL 9.7.2 by setting
1109    /// each level and reading `@@transaction_isolation` back:
1110    /// `READ-UNCOMMITTED` / `READ-COMMITTED` / `REPEATABLE-READ` /
1111    /// `SERIALIZABLE` (the last has no hyphen because it is one word).
1112    ///
1113    /// This exists so the two MySQL surfaces cannot drift: both
1114    /// `SHOW VARIABLES` and `@@transaction_isolation` used to carry
1115    /// their own hard-coded literal, and the literals disagreed —
1116    /// one said `REPEATABLE-READ` while the engine ran read committed.
1117    /// v7.39 — parse what `default_transaction_isolation` holds. PG
1118    /// accepts the SQL spellings and stores them lower-cased with a
1119    /// space; anything else is not a level this understands and the
1120    /// caller keeps its own default rather than guessing.
1121    #[must_use]
1122    pub fn from_pg_name(name: &str) -> Option<Self> {
1123        match name.trim().to_ascii_lowercase().as_str() {
1124            "read uncommitted" => Some(Self::ReadUncommitted),
1125            "read committed" => Some(Self::ReadCommitted),
1126            "repeatable read" => Some(Self::RepeatableRead),
1127            "serializable" => Some(Self::Serializable),
1128            _ => None,
1129        }
1130    }
1131
1132    #[must_use]
1133    pub fn as_mysql_str(self) -> &'static str {
1134        match self {
1135            Self::ReadUncommitted => "READ-UNCOMMITTED",
1136            Self::ReadCommitted => "READ-COMMITTED",
1137            Self::RepeatableRead => "REPEATABLE-READ",
1138            Self::Serializable => "SERIALIZABLE",
1139        }
1140    }
1141
1142    pub fn as_pg_str(self) -> &'static str {
1143        match self {
1144            Self::ReadUncommitted => "read uncommitted",
1145            Self::ReadCommitted => "read committed",
1146            Self::RepeatableRead => "repeatable read",
1147            Self::Serializable => "serializable",
1148        }
1149    }
1150}
1151
1152impl core::fmt::Display for IsolationLevel {
1153    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1154        f.write_str(self.as_pg_str())
1155    }
1156}
1157
1158/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1159/// single fixed-shape DDL; the WITH-clause options PG supports
1160/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1161/// scope for v6.1.4 — `enabled` defaults to true and there are
1162/// no other knobs to set in v6.1.x.
1163#[derive(Debug, Clone, PartialEq, Eq)]
1164pub struct CreateSubscriptionStatement {
1165    pub name: String,
1166    /// Connection string in PG keyword=value form (e.g.
1167    /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1168    /// `host` and `port` fields; the rest is reserved for
1169    /// future v6.1.x options.
1170    pub conn_str: String,
1171    /// One or more publications on the remote side. Order is
1172    /// preserved verbatim from the DDL; the worker requests them
1173    /// in this order. v6.1.4 records the list; v6.1.5
1174    /// publisher-side filtering enforces it.
1175    pub publications: Vec<String>,
1176}
1177
1178/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1179#[derive(Debug, Clone, PartialEq, Eq)]
1180pub struct CreateSequenceStatement {
1181    pub name: String,
1182    pub if_not_exists: bool,
1183    pub temporary: bool,
1184    /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1185    pub data_type: Option<SequenceDataType>,
1186    pub options: SequenceOptions,
1187}
1188
1189/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1191pub enum SequenceDataType {
1192    SmallInt,
1193    Int,
1194    BigInt,
1195}
1196
1197/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1198/// All fields are optional. `min_value`/`max_value` carry
1199/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1200#[derive(Debug, Clone, Default, PartialEq, Eq)]
1201pub struct SequenceOptions {
1202    pub increment: Option<i64>,
1203    pub min_value: Option<SeqBound>,
1204    pub max_value: Option<SeqBound>,
1205    pub start: Option<i64>,
1206    /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1207    /// RESTART, `Some(Some(n))` = RESTART WITH n.
1208    pub restart: Option<Option<i64>>,
1209    pub cache: Option<i64>,
1210    pub cycle: Option<bool>,
1211    pub owned_by: Option<SequenceOwnedBy>,
1212}
1213
1214/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1216pub enum SeqBound {
1217    Value(i64),
1218    NoBound,
1219}
1220
1221/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1222#[derive(Debug, Clone, PartialEq, Eq)]
1223pub enum SequenceOwnedBy {
1224    None,
1225    Column { table: String, column: String },
1226}
1227
1228/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1229#[derive(Debug, Clone, PartialEq)]
1230pub struct CreateMaterializedViewStatement {
1231    pub name: String,
1232    pub if_not_exists: bool,
1233    /// Optional `(col, col, …)` rename list. Applies to the
1234    /// backing table at CREATE / REFRESH time.
1235    pub columns: Vec<String>,
1236    /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1237    /// the cached rows.
1238    pub body: SelectStatement,
1239    /// `WITH DATA` (default) = materialise the rows at CREATE
1240    /// time. `WITH NO DATA` = create an empty backing table;
1241    /// callers must REFRESH before SELECT returns rows.
1242    pub with_data: bool,
1243    /// v7.38 (read01 P6.49) — when true this node came from
1244    /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1245    /// executor creates a plain table and does NOT register it in the
1246    /// materialized-view registry (no REFRESH semantics).
1247    pub as_plain_table: bool,
1248    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1249    /// meaningful together with `as_plain_table`; the executor puts the
1250    /// resulting table in the creating session's namespace.
1251    pub temporary: bool,
1252}
1253
1254/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1255/// auto-updatable view. `Cascaded` is PG's default when the bare
1256/// `WITH CHECK OPTION` is written.
1257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1258pub enum ViewCheckOption {
1259    Local,
1260    Cascaded,
1261}
1262
1263/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1264#[derive(Debug, Clone, PartialEq)]
1265pub struct CreateViewStatement {
1266    pub name: String,
1267    pub or_replace: bool,
1268    pub if_not_exists: bool,
1269    pub temporary: bool,
1270    /// Optional `(col, col, …)` rename list. When non-empty,
1271    /// these override the body's projected column names per-
1272    /// position at SELECT-from-view time.
1273    pub columns: Vec<String>,
1274    /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1275    /// time to materialise the view as a synthetic CTE.
1276    pub body: SelectStatement,
1277    /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1278    /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1279    /// 44000). `None` = no check option.
1280    pub check_option: Option<ViewCheckOption>,
1281}
1282
1283/// v7.17.0 — `ALTER SEQUENCE` AST node.
1284#[derive(Debug, Clone, PartialEq, Eq)]
1285pub struct AlterSequenceStatement {
1286    pub name: String,
1287    pub if_exists: bool,
1288    pub options: SequenceOptions,
1289    /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1290    /// instead of `options`; the two forms are mutually exclusive in PG.
1291    pub rename_to: Option<String>,
1292}
1293
1294/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1295/// the [`PublicationScope`] shape. v6.1.2 only accepted
1296/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1297/// variants by flipping the parser gate (no AST migration).
1298#[derive(Debug, Clone, PartialEq, Eq)]
1299pub struct CreatePublicationStatement {
1300    pub name: String,
1301    pub scope: PublicationScope,
1302}
1303
1304/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1305/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1306/// variants — the on-disk shape, snapshot serialisation, and the
1307/// AST round-trip Display path were already in place in v6.1.2
1308/// so this is a parser-only widening.
1309#[derive(Debug, Clone, PartialEq, Eq)]
1310pub enum PublicationScope {
1311    AllTables,
1312    ForTables(Vec<String>),
1313    AllTablesExcept(Vec<String>),
1314    /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1315    /// (PG 15+). AST-only: the executor folds `public` to
1316    /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1317    /// and refuses any other schema with PG's sentence, so the
1318    /// catalog / serializer / replication filter never see it.
1319    TablesInSchema(String),
1320}
1321
1322#[derive(Debug, Clone, PartialEq, Eq)]
1323pub struct AlterIndexStatement {
1324    pub name: String,
1325    pub target: AlterIndexTarget,
1326}
1327
1328#[derive(Debug, Clone, PartialEq, Eq)]
1329pub enum AlterIndexTarget {
1330    /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1331    /// rebuilds the existing graph in place without touching the
1332    /// column encoding; `Some(enc)` re-encodes every cell first.
1333    Rebuild { encoding: Option<VecEncoding> },
1334    /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1335    /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1336    /// uses it to make the migration idempotent (re-running on a
1337    /// DB where the rename already happened is a no-op rather
1338    /// than an error).
1339    Rename { new: String, if_exists: bool },
1340    /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1341    /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1342    /// does not exist`), so the index is validated and the storage
1343    /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1344    /// SET/RESET arms already record).
1345    StorageParams,
1346}
1347
1348/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1349/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1350/// can add more SET subjects without changing the dispatch shape.
1351#[derive(Debug, Clone, PartialEq)]
1352pub struct AlterTableStatement {
1353    pub name: String,
1354    /// v7.13.2 — mailrs round-6 S1. One or more subactions
1355    /// separated by commas in the source SQL. PG-semantic apply
1356    /// is sequential; engine bails on first error (no
1357    /// transactional rollback of completed subactions in v7.13).
1358    /// Single-subaction shape stays a 1-element vec.
1359    pub targets: Vec<AlterTableTarget>,
1360}
1361/// v7.39.9 — the `FIRST` / `AFTER c` trailer, written back the way it
1362/// was read.
1363fn write_column_position(
1364    f: &mut core::fmt::Formatter<'_>,
1365    pos: Option<&ColumnPosition>,
1366) -> core::fmt::Result {
1367    match pos {
1368        Some(ColumnPosition::First) => f.write_str(" FIRST"),
1369        Some(ColumnPosition::After(c)) => write!(f, " AFTER {}", quote_ident(c)),
1370        None => Ok(()),
1371    }
1372}
1373
1374/// v7.39.9 — where MySQL's `ADD` / `MODIFY` / `CHANGE` puts a column.
1375///
1376/// The row encoding is positional and `SELECT *` reads it in order, so
1377/// this is an answer, not a formatting preference.
1378#[derive(Debug, Clone, PartialEq, Eq)]
1379pub enum ColumnPosition {
1380    First,
1381    After(String),
1382}
1383
1384#[derive(Debug, Clone, PartialEq)]
1385#[allow(clippy::large_enum_variant)]
1386pub enum AlterTableTarget {
1387    /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1388    ///
1389    /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1390    /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1391    /// the reasoning went stale: `NO INHERIT` reported success while the
1392    /// child stayed attached, which is the worst kind of answer — the
1393    /// statement says it worked and the catalog disagrees.
1394    Inherit { parent: String, detach: bool },
1395    /// Per-table hot-tier byte budget override. The freezer
1396    /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1397    SetHotTierBytes(u64),
1398    /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1399    /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1400    /// Engine validates existing rows against the new constraint
1401    /// before installing it.
1402    AddForeignKey(ForeignKeyConstraint),
1403    /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1404    /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1405    /// no-op when no FK with that name exists; otherwise raises.
1406    DropForeignKey { name: String, if_exists: bool },
1407    /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1408    /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1409    /// as the standalone `DROP INDEX` statement.
1410    DropIndex { name: String, if_exists: bool },
1411    /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1412    /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1413    /// (20 migrate-*.sql hits). Engine appends the column to the
1414    /// schema and back-fills every existing row with the DEFAULT
1415    /// (or NULL when no DEFAULT and the column is nullable).
1416    AddColumn {
1417        column: ColumnDef,
1418        if_not_exists: bool,
1419        /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>`, which say where
1420        /// the column goes. `None` is the PostgreSQL form and appends.
1421        position: Option<ColumnPosition>,
1422    },
1423    /// v7.39.9 — MySQL's `MODIFY COLUMN c <definition>` and
1424    /// `CHANGE COLUMN old new <definition>`.
1425    ///
1426    /// Both REPLACE the column's definition rather than amending it,
1427    /// which is the part that cannot be expressed by the PostgreSQL
1428    /// spellings SPG already had. Measured on MySQL 9.7.2: a column
1429    /// declared `INT NOT NULL DEFAULT 5`, after `MODIFY COLUMN b
1430    /// BIGINT`, is `bigint` NULLABLE with NO default — restating them
1431    /// keeps them, omitting them drops them. `CHANGE` is the same and
1432    /// also renames.
1433    ModifyColumn {
1434        /// The column as it is named now.
1435        column: String,
1436        /// `CHANGE`'s new name; `None` for `MODIFY`, which keeps it.
1437        rename_to: Option<String>,
1438        /// The whole new definition, exactly as written.
1439        definition: ColumnDef,
1440        position: Option<ColumnPosition>,
1441    },
1442    /// v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
1443    RenameIndex { old: String, new: String },
1444    /// v7.39.9 — MySQL's `ALTER TABLE t AUTO_INCREMENT = n`, which sets
1445    /// the value the NEXT insert takes. Measured on 9.7.2: after
1446    /// `= 100`, the next row's id is 100.
1447    SetTableAutoIncrement(i64),
1448    /// v7.39.9 — MySQL's `ENGINE = <name>`. SPG has one storage engine
1449    /// and substitutes for every name MySQL knows, exactly as
1450    /// `CREATE TABLE` already does; a name MySQL does not know is
1451    /// refused with its 1286, because a typo in a migration must not
1452    /// quietly become SPG's storage.
1453    SetEngine(String),
1454    /// v7.39.9 — MySQL's `CONVERT TO CHARACTER SET <cs> [COLLATE <c>]`.
1455    /// SPG stores UTF-8 throughout, so a charset it can represent is
1456    /// accepted and one it cannot is refused with MySQL's 1115.
1457    ConvertToCharacterSet {
1458        charset: String,
1459        collate: Option<String>,
1460    },
1461    /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1462    /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1463    /// existing row's column value by evaluating the optional
1464    /// USING expression (default `col::<ty>`) and re-coercing
1465    /// against the new column type.
1466    AlterColumnType {
1467        column: String,
1468        new_type: ColumnTypeName,
1469        using: Option<Expr>,
1470        /// v7.39 (round 713) — `COLLATE <name>` between the type and
1471        /// USING. PG re-collates the column, and an ABSENT clause RESETS
1472        /// the collation to the type default (measured round 713) — so
1473        /// `None` is not "leave it alone". The type parser consumed the
1474        /// clause all along and this surface dropped it on the floor:
1475        /// the statement succeeded and the ordering did not change, the
1476        /// silent-divergence shape. Folded variant + the name as written.
1477        collation: Option<(Collation, String)>,
1478    },
1479    /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1480    /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1481    /// every row's value at that position is removed; any index
1482    /// on the column is dropped. `if_exists` makes the drop a
1483    /// no-op when the column is missing. `cascade` removes
1484    /// dependents (FKs referencing the column, partial indexes
1485    /// whose predicate names the column); without it, the engine
1486    /// rejects when dependents exist.
1487    DropColumn {
1488        column: String,
1489        if_exists: bool,
1490        cascade: bool,
1491    },
1492    /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1493    /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1494    /// CONSTRAINT name CHECK (expr)` — table-level constraints
1495    /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1496    /// separate ALTER TABLE statement, so this surface lets the
1497    /// dump load straight through.
1498    AddTableConstraint(TableConstraint),
1499    /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1500    /// there is nothing to record; what PG does that SPG did not is
1501    /// REFUSE a role that does not exist. The name has to reach the
1502    /// engine for that, because only the engine knows the roles.
1503    OwnerTo { role: String },
1504    /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1505    /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1506    /// the hint is still a no-op; naming an index that does not exist is
1507    /// not.
1508    ClusterOn { index: Option<String> },
1509    /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1510    /// already in the table against a constraint added `NOT VALID` and,
1511    /// if they all pass, mark it validated. It used to be swallowed as a
1512    /// no-op on the theory that SPG validated at ADD time; SPG did not.
1513    ValidateConstraint { name: String },
1514    /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1515    /// Renames the column in the schema and propagates the rename
1516    /// to every stored source string that references it as a
1517    /// (potentially-qualified) column identifier: CHECK predicates,
1518    /// partial-index predicates, runtime DEFAULT expressions, and
1519    /// triggers' `UPDATE OF` column lists. Function bodies and
1520    /// trigger bodies are NOT auto-rewritten — they're loose
1521    /// source text and may contain references SPG can't statically
1522    /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1523    /// the column even if dependents exist; users renaming a
1524    /// column referenced by a function body update the function
1525    /// body separately.
1526    RenameColumn { old: String, new: String },
1527    /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1528    /// Reachable now that the schema stores user-supplied constraint names.
1529    RenameConstraint { old: String, new: String },
1530    /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1531    /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1532    /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1533    /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1534    /// (identity); both lower to this. SPG's auto-increment is
1535    /// max+1-scan based, so the dump's `setval(…)` calls stay
1536    /// no-ops without losing the sequence position.
1537    SetColumnAutoIncrement {
1538        column: String,
1539        /// The implicit sequence pg_dump names for an identity
1540        /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1541        /// nextval target for a serial default. The engine creates
1542        /// it if absent so the dump's later `setval(s, …)` lands.
1543        seq_name: Option<String>,
1544    },
1545    /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1546    /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1547    /// migrate-042 uses it). The engine moves the table entry
1548    /// in the catalog under the new name; child catalog state
1549    /// (FKs pointing at this table, triggers watching this
1550    /// table) tracks the rename through the storage layer.
1551    RenameTable { new: String },
1552    /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1553    /// { ALL | <name> }`. Toggles whether row-level triggers
1554    /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1555    /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1556    /// ENABLE epilogue around every table's data block so the
1557    /// rows already-computed in prod don't get re-rewritten
1558    /// (and so trigger-driven side effects like
1559    /// audit/queueing don't re-fire during a bulk reload).
1560    /// `which == TriggerSelector::All` toggles every trigger
1561    /// on the table; `Named(name)` toggles one trigger. The
1562    /// engine persists the disabled state on `TriggerDef.enabled`
1563    /// (catalog FILE_VERSION 25+) and the row-write paths skip
1564    /// the trigger when `!enabled`.
1565    SetTriggerEnabled {
1566        which: TriggerSelector,
1567        enabled: bool,
1568    },
1569    /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1570    /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1571    /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1572    /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1573    SetRowSecurity {
1574        enabled: Option<bool>,
1575        force: Option<bool>,
1576    },
1577    /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1578    /// <bounds>`. Promotes an existing table `child` to a partition
1579    /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1580    /// Engine validates that `child`'s columns are layout-compatible
1581    /// with `parent` and that every row in `child` satisfies the
1582    /// bound before installing the role.
1583    AttachPartition {
1584        child: String,
1585        bounds: PartitionOfBoundsAst,
1586    },
1587    /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1588    /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1589    /// to a standalone table (clears `partition_role`) and removes
1590    /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1591    /// is parser-accepted; engine performs the same atomic detach
1592    /// (single-engine, no replication lag — the PG semantics that
1593    /// require the two-phase split don't apply).
1594    DetachPartition {
1595        child: String,
1596        concurrently: bool,
1597        finalize: bool,
1598    },
1599    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1600    /// <expr>`. Engine re-parses + freezes the literal at this point,
1601    /// matching CREATE TABLE-side default semantics. Volatile shapes
1602    /// (`now()` / `nextval`) take the runtime-default path.
1603    AlterColumnSetDefault { column: String, default_expr: Expr },
1604    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1605    AlterColumnDropDefault { column: String },
1606    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1607    /// Engine validates that no existing row has NULL in that column
1608    /// before flipping the flag (PG semantics — partial NOT NULL
1609    /// would surface inconsistently).
1610    AlterColumnSetNotNull { column: String },
1611    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1612    AlterColumnDropNotNull { column: String },
1613    /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1614    /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1615    /// column's start value = 1). Engine records a next-value floor over
1616    /// SPG's max+1 identity allocation.
1617    AlterColumnRestart { column: String, with: Option<i64> },
1618    /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1619    /// EXPRESSION` turns a stored generated column into a plain column
1620    /// (its generation expression is removed; existing values are kept).
1621    AlterColumnDropExpression { column: String, if_exists: bool },
1622    /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1623    /// de-generate an identity column into a plain column.
1624    AlterColumnDropIdentity { column: String, if_exists: bool },
1625    /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1626    /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1627    /// expression and recomputes every existing row.
1628    AlterColumnSetExpression { column: String, expr: Expr },
1629    /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1630    /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1631    /// (PG: `type "x" does not exist`).
1632    OfType { type_name: String },
1633    /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1634    /// identity setting no-ops (SPG has no logical replication consumer);
1635    /// the INDEX must exist on this table (PG: `index "i" for table "t"
1636    /// does not exist`).
1637    ReplicaIdentityUsingIndex { index: String },
1638}
1639
1640/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1641/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1642/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1643/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1644/// shouldn't surface from a dump.
1645#[derive(Debug, Clone, PartialEq, Eq)]
1646pub enum TriggerSelector {
1647    /// Every trigger on the table.
1648    All,
1649    /// A specific trigger by name.
1650    Named(String),
1651}
1652
1653/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1654/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1655/// bitflags word or a nested options struct would only relocate the lint
1656/// while making the option each caller sets harder to read.
1657#[allow(clippy::struct_excessive_bools)]
1658#[derive(Debug, Clone, PartialEq)]
1659pub struct ExplainStatement {
1660    pub analyze: bool,
1661    /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1662    /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1663    /// `Insert on / Update on / Delete on` trees for them.
1664    pub inner: Box<Statement>,
1665    /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1666    /// advisor pass: after the regular plan tree, the engine
1667    /// emits one suggestion line per column referenced in the
1668    /// query's WHERE / JOIN that has no covering index on the
1669    /// owning table.
1670    pub suggest: bool,
1671    /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1672    /// `elapsed=…us` annotations from the Total line (and any
1673    /// future cost-bearing lines). PG-standard option used by
1674    /// regression suites and diff-friendly EXPLAIN output. When
1675    /// `true`, takes precedence over the per-session
1676    /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1677    pub costs_off: bool,
1678    /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1679    /// option that surfaces hot/cold/shared block counters. SPG's
1680    /// hot-tier scan path counts examined rows; the BUFFERS option
1681    /// makes that an explicit per-operator annotation.
1682    pub buffers: bool,
1683    /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1684    /// uses this to disable per-operator timing while still
1685    /// emitting actual-row counts (cheaper than ANALYZE). Default
1686    /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1687    /// timing portion of the Total line. Decoupled from `costs_off`:
1688    /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1689    /// measured wall-clock.
1690    pub timing_off: bool,
1691    /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1692    /// modified GUC values to the plan output. SPG emits the
1693    /// session params that diverge from default after the main
1694    /// plan body.
1695    pub settings: bool,
1696    /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1697    /// bytes / records / FPI emitted by the query. SPG's
1698    /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1699    /// ANALYZE) report against the engine WAL counter delta.
1700    pub wal: bool,
1701    /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1702    /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1703    /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1704    /// this is set.
1705    pub summary_off: bool,
1706    /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1707    /// PG's standard format selector. Default is text. JSON / XML
1708    /// / YAML emit a single-row TEXT result whose body wraps the
1709    /// existing line-per-operator text in the chosen container —
1710    /// PG-compatible just enough for dashboards that parse those
1711    /// container shapes (pgAdmin's JSON path picker, etc.).
1712    pub format: ExplainFormat,
1713}
1714
1715#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1716pub enum ExplainFormat {
1717    #[default]
1718    Text,
1719    Json,
1720    Xml,
1721    Yaml,
1722}
1723
1724/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1726pub enum PolicyCmd {
1727    All,
1728    Select,
1729    Insert,
1730    Update,
1731    Delete,
1732}
1733
1734/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1735/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1736#[derive(Debug, Clone, PartialEq)]
1737pub struct CreatePolicyStatement {
1738    pub name: String,
1739    pub table: String,
1740    /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1741    pub permissive: bool,
1742    pub cmd: PolicyCmd,
1743    /// Empty = PUBLIC.
1744    pub roles: Vec<String>,
1745    pub using: Option<Expr>,
1746    pub with_check: Option<Expr>,
1747}
1748
1749/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1750/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1751/// or the command (matches PG).
1752#[derive(Debug, Clone, PartialEq)]
1753pub struct AlterPolicyStatement {
1754    pub name: String,
1755    pub table: String,
1756    pub rename_to: Option<String>,
1757    pub roles: Option<Vec<String>>,
1758    pub using: Option<Expr>,
1759    pub with_check: Option<Expr>,
1760}
1761
1762/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1763#[derive(Debug, Clone, PartialEq, Eq)]
1764pub struct DropPolicyStatement {
1765    pub name: String,
1766    pub table: String,
1767    pub if_exists: bool,
1768}
1769
1770#[derive(Debug, Clone, PartialEq, Eq)]
1771pub struct CreateUserStatement {
1772    pub name: String,
1773    /// Empty when the statement carried no PASSWORD — legal for a bare
1774    /// `CREATE ROLE`, which cannot log in anyway.
1775    pub password: String,
1776    /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1777    /// the parser; the engine validates against `Role::parse` so a
1778    /// typo lands as a runtime error with a clear message rather than
1779    /// a parse failure.
1780    pub role: String,
1781    /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1782    /// statement did not say, so the default for its spelling applies:
1783    /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1784    /// both default to INHERIT and NOSUPERUSER.
1785    pub login: Option<bool>,
1786    pub inherit: Option<bool>,
1787    pub superuser: Option<bool>,
1788    /// `true` when spelled `CREATE USER` (LOGIN by default).
1789    pub is_user: bool,
1790}
1791
1792/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1793/// it tells the planner how far a call may be moved or folded. SPG records
1794/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1795/// yet exploit it for constant folding.
1796#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1797pub enum FunctionVolatility {
1798    Immutable,
1799    Stable,
1800    #[default]
1801    Volatile,
1802}
1803
1804impl FunctionVolatility {
1805    /// PG's one-character `pg_proc.provolatile` code.
1806    #[must_use]
1807    pub const fn as_pg_char(self) -> &'static str {
1808        match self {
1809            Self::Immutable => "i",
1810            Self::Stable => "s",
1811            Self::Volatile => "v",
1812        }
1813    }
1814
1815    #[must_use]
1816    pub const fn as_sql(self) -> &'static str {
1817        match self {
1818            Self::Immutable => "IMMUTABLE",
1819            Self::Stable => "STABLE",
1820            Self::Volatile => "VOLATILE",
1821        }
1822    }
1823}
1824
1825/// v7.39 (round 322, V46) — PG's parallel-safety class.
1826#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1827pub enum FunctionParallel {
1828    #[default]
1829    Unsafe,
1830    Restricted,
1831    Safe,
1832}
1833
1834impl FunctionParallel {
1835    /// PG's one-character `pg_proc.proparallel` code.
1836    #[must_use]
1837    pub const fn as_pg_char(self) -> &'static str {
1838        match self {
1839            Self::Unsafe => "u",
1840            Self::Restricted => "r",
1841            Self::Safe => "s",
1842        }
1843    }
1844
1845    #[must_use]
1846    pub const fn as_sql(self) -> &'static str {
1847        match self {
1848            Self::Unsafe => "PARALLEL UNSAFE",
1849            Self::Restricted => "PARALLEL RESTRICTED",
1850            Self::Safe => "PARALLEL SAFE",
1851        }
1852    }
1853}
1854
1855/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1856/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1857/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1858/// language's default cost / rows.
1859#[derive(Debug, Clone, Copy, PartialEq, Default)]
1860pub struct FunctionAttrs {
1861    pub volatility: FunctionVolatility,
1862    /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1863    /// argument returns NULL without running the body.
1864    pub strict: bool,
1865    pub security_definer: bool,
1866    pub leakproof: bool,
1867    pub parallel: FunctionParallel,
1868    /// `COST n` — `None` leaves PG's per-language default.
1869    pub cost: Option<f64>,
1870    /// `ROWS n` — set-returning functions only; `None` = default.
1871    pub rows: Option<f64>,
1872}
1873
1874impl FunctionAttrs {
1875    /// The attribute words `pg_get_functiondef` puts on their own line,
1876    /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1877    /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1878    /// at its default — PG then emits no such line at all.
1879    #[must_use]
1880    pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1881        let mut out = alloc::vec::Vec::new();
1882        if self.volatility != FunctionVolatility::Volatile {
1883            out.push(alloc::string::String::from(self.volatility.as_sql()));
1884        }
1885        if self.parallel != FunctionParallel::Unsafe {
1886            out.push(alloc::string::String::from(self.parallel.as_sql()));
1887        }
1888        if self.strict {
1889            out.push(alloc::string::String::from("STRICT"));
1890        }
1891        if self.security_definer {
1892            out.push(alloc::string::String::from("SECURITY DEFINER"));
1893        }
1894        if self.leakproof {
1895            out.push(alloc::string::String::from("LEAKPROOF"));
1896        }
1897        if let Some(c) = self.cost {
1898            out.push(alloc::format!("COST {}", render_attr_number(c)));
1899        }
1900        if let Some(r) = self.rows {
1901            out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1902        }
1903        out
1904    }
1905}
1906
1907/// PG prints a whole-numbered cost / rows without a decimal point.
1908fn render_attr_number(v: f64) -> alloc::string::String {
1909    // no_std: `f64::fract` lives in std, so compare against the truncation.
1910    let whole = v as i64;
1911    if v.abs() < 1e15 && (whole as f64) == v {
1912        alloc::format!("{whole}")
1913    } else {
1914        alloc::format!("{v}")
1915    }
1916}
1917
1918/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1919/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1920/// (the row-level trigger body the CREATE TRIGGER below references).
1921/// Non-trigger user-defined functions parse but error at execution
1922/// time with a clear unsupported message; that surface lands in
1923/// v7.12.5+.
1924#[derive(Debug, Clone, PartialEq)]
1925pub struct CreateFunctionStatement {
1926    pub name: String,
1927    /// `OR REPLACE` was present; an existing function with the
1928    /// same name is overwritten instead of erroring.
1929    pub or_replace: bool,
1930    /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1931    /// list `()` (sufficient for trigger functions). Other shapes
1932    /// parse and store the args but the executor refuses to call
1933    /// them.
1934    pub args: Vec<FunctionArg>,
1935    /// `RETURNS <type>` — `trigger` is the supported shape for
1936    /// v7.12.4; arbitrary return types parse to
1937    /// [`FunctionReturn::Other`].
1938    pub returns: FunctionReturn,
1939    /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1940    /// side of `AS $$...$$`; the parser canonicalises to one slot.
1941    /// `plpgsql` and `sql` are the two interesting values.
1942    pub language: String,
1943    /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1944    /// a structured AST; non-trigger / non-plpgsql bodies stay as
1945    /// the raw source text so the v7.12.5+ executor can pick them
1946    /// up without a parser rev.
1947    pub body: FunctionBody,
1948    /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1949    /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1950    /// on either side of the body; before this they were a parse error, so
1951    /// PG's own `pg_dump` output would not restore.
1952    pub attrs: FunctionAttrs,
1953}
1954
1955/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1956#[derive(Debug, Clone, PartialEq)]
1957pub struct FunctionArg {
1958    /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1959    /// (the default); `OUT` / `INOUT` parse but the executor
1960    /// refuses them.
1961    pub mode: FunctionArgMode,
1962    /// Optional arg name. Trigger functions traditionally don't
1963    /// name their args (they read NEW/OLD instead), so `None` is
1964    /// the common case.
1965    pub name: Option<String>,
1966    /// Declared type, normalised to the SPG `DataType` mapping
1967    /// where one exists. Unknown / extension types parse as a
1968    /// raw string under [`FunctionArgType::Raw`].
1969    pub ty: FunctionArgType,
1970}
1971
1972#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1973pub enum FunctionArgMode {
1974    In,
1975    Out,
1976    InOut,
1977}
1978
1979#[derive(Debug, Clone, PartialEq)]
1980pub enum FunctionArgType {
1981    Typed(ColumnTypeName),
1982    /// Unknown / extension types — kept as the parser-side raw
1983    /// identifier so error messages can name them precisely.
1984    Raw(String),
1985}
1986
1987#[derive(Debug, Clone, PartialEq)]
1988pub enum FunctionReturn {
1989    /// `RETURNS TRIGGER` — the row-level trigger function shape.
1990    /// v7.12.4 ships exactly this for execution.
1991    Trigger,
1992    /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
1993    /// the function is unused (since v7.12.4 doesn't ship scalar
1994    /// function invocation).
1995    Void,
1996    /// `RETURNS <type>` for any concrete data type. Reserved for
1997    /// v7.12.5+'s scalar UDF surface.
1998    Type(ColumnTypeName),
1999    /// `RETURNS <ident>` for types SPG doesn't know — extension
2000    /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
2001    Other(String),
2002}
2003
2004#[derive(Debug, Clone, PartialEq)]
2005pub enum FunctionBody {
2006    /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
2007    /// trigger-function executor walks this directly without
2008    /// re-parsing.
2009    PlPgSql(PlPgSqlBlock),
2010    /// Raw source text — parser couldn't (or didn't try to)
2011    /// structure-parse the body. Used for `LANGUAGE sql`
2012    /// functions and any PL/pgSQL body that contains v7.12.5+
2013    /// features the v7.12.4 parser doesn't yet recognise. The
2014    /// executor returns an unsupported error when invoked.
2015    Raw(String),
2016}
2017
2018/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
2019/// from assignment + return to a real-PL/pgSQL surface:
2020/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
2021/// control flow, `RAISE` diagnostics, and embedded SQL
2022/// statements that execute through the regular engine path.
2023/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
2024/// which mailrs's trigger doesn't need but other PG customers
2025/// may; deferred to a future minor release.
2026#[derive(Debug, Clone, PartialEq)]
2027pub struct PlPgSqlBlock {
2028    /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
2029    /// preceding `BEGIN`. Empty when the body opens directly with
2030    /// `BEGIN`. Declarations execute in order; each may reference
2031    /// earlier-declared locals in its init expression.
2032    pub declarations: Vec<PlPgSqlDeclare>,
2033    pub statements: Vec<PlPgSqlStmt>,
2034    /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
2035    /// <body>` handlers appended to the block. Empty when no
2036    /// EXCEPTION clause is present. When a body statement raises
2037    /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
2038    /// handlers are tried in order; the first matching condition
2039    /// runs its body and the block terminates cleanly. `OTHERS`
2040    /// matches any exception. Unhandled exceptions propagate.
2041    pub exception_handlers: Vec<ExceptionHandler>,
2042}
2043
2044/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
2045/// arm inside an EXCEPTION block.
2046#[derive(Debug, Clone, PartialEq)]
2047pub struct ExceptionHandler {
2048    /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
2049    /// conditions joined by `OR` share one handler body.
2050    pub conditions: Vec<String>,
2051    /// Statements to run when a matching exception is caught.
2052    pub body: Vec<PlPgSqlStmt>,
2053}
2054
2055/// v7.12.6 — single `DECLARE` entry: variable name + declared
2056/// type + optional initialiser. Variables default to SQL NULL
2057/// when no init is given (matches PG).
2058#[derive(Debug, Clone, PartialEq)]
2059pub struct PlPgSqlDeclare {
2060    pub name: String,
2061    /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
2062    /// knows it; raw text otherwise).
2063    pub ty: FunctionArgType,
2064    pub default: Option<Expr>,
2065}
2066
2067#[derive(Debug, Clone, PartialEq)]
2068pub enum PlPgSqlStmt {
2069    /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
2070    /// for clarity in error reporting (PG also forbids it) — the
2071    /// executor errors with a clear "OLD is read-only" message.
2072    Assign { target: AssignTarget, value: Expr },
2073    /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
2074    /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
2075    /// the SELECT statement with the INTO clause stripped; the
2076    /// engine runs it via `Engine::execute`, takes the first
2077    /// row's first column, and assigns to the local variable
2078    /// in the DECLARE scope. Single-column / single-row
2079    /// queries only at v7.16.2; multi-target (`INTO a, b`) is
2080    /// a v7.16.x follow-up.
2081    SelectInto {
2082        var: String,
2083        body: Box<SelectStatement>,
2084    },
2085    /// `RETURN <target>;` — trigger functions canonically return
2086    /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
2087    /// expression for forward compatibility with scalar UDFs.
2088    Return(ReturnTarget),
2089    /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
2090    /// set a SETOF function is building, and KEEP GOING. Not a return.
2091    ReturnNext(Expr),
2092    /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
2093    /// query yields, and keep going. It used to desugar to a side-effect
2094    /// statement whose result was DISCARDED — in a SETOF function that is the
2095    /// whole answer thrown away.
2096    ReturnQuery(Box<SelectStatement>),
2097    /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
2098    /// twin. Its rows go to the set too; it used to run and discard them.
2099    ReturnQueryExecute { sql: Expr },
2100    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
2101    /// [ELSE body] END IF;`. Branches are tried in order; first
2102    /// truthy condition wins; the optional ELSE runs when no
2103    /// condition matched.
2104    If {
2105        branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
2106        else_branch: Vec<PlPgSqlStmt>,
2107    },
2108    /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
2109    /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
2110    /// (logging — observable side effect only) or `EXCEPTION`
2111    /// (aborts the trigger and propagates as an error). v7.12.6
2112    /// supports the basic format-string substitution PG uses
2113    /// (`%` placeholders consumed positionally).
2114    Raise {
2115        level: RaiseLevel,
2116        message: String,
2117        args: Vec<Expr>,
2118    },
2119    /// v7.12.6 — embedded SQL statement inside the trigger body
2120    /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
2121    /// NEW.col / OLD.col references inside the embedded
2122    /// statement's expression tree are substituted with the
2123    /// current trigger context before the engine re-executes the
2124    /// statement. Recursion depth into nested triggers is
2125    /// bounded by the engine's existing trigger-fire guard.
2126    EmbeddedSql(Box<Statement>),
2127    /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
2128    /// the condition evaluates falsy the trigger / DO block aborts
2129    /// with the message (defaulting to a generic shape when none
2130    /// is provided). Same propagation shape as `RAISE EXCEPTION`
2131    /// — the error reaches the caller's query path. PG's behaviour
2132    /// is identical except for a `plpgsql.check_asserts` GUC that
2133    /// can disable the check globally; SPG always evaluates.
2134    Assert {
2135        condition: Expr,
2136        message: Option<Expr>,
2137    },
2138    /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2139    /// Iterate the body while condition evaluates truthy. Iteration
2140    /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2141    /// loops; the executor errors out when reached. EXIT / CONTINUE
2142    /// inside the body queue with 20.2.
2143    While {
2144        condition: Expr,
2145        body: Vec<PlPgSqlStmt>,
2146    },
2147    /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2148    /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2149    /// bounds inclusive on both sides. REVERSE walks backward.
2150    /// Iteration budget guards runaway.
2151    ForRange {
2152        var: String,
2153        start: Expr,
2154        end: Expr,
2155        reverse: bool,
2156        body: Vec<PlPgSqlStmt>,
2157    },
2158    /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2159    /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2160    /// budget guards runaway.
2161    Loop { body: Vec<PlPgSqlStmt> },
2162    /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2163    /// Unconditional (no WHEN) or conditional (only breaks when
2164    /// condition is truthy). Bubbles up as BodyOutcome::Break which
2165    /// the enclosing loop catches. Outside a loop it's a no-op.
2166    Exit { when: Option<Expr> },
2167    /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2168    /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2169    /// which the enclosing loop catches, skipping the remainder of
2170    /// the body and jumping to the next iteration.
2171    Continue { when: Option<Expr> },
2172    /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2173    /// computed SQL statement. The expression is evaluated to a
2174    /// text value, the resulting string is parsed and dispatched
2175    /// through the engine like an EmbeddedSql. USING <param_list>
2176    /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2177    ExecuteDynamic { sql: Expr },
2178    /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2179    /// END LOOP;`. Runs the SELECT once, iterates the resulting
2180    /// rows, binds the first column of each row to `var` as a
2181    /// scalar Value, then runs the body per iteration. EXIT /
2182    /// CONTINUE / ASSERT / RAISE etc. propagate through the
2183    /// enclosing loop's BodyOutcome discipline the same way
2184    /// FOR range and WHILE do. Full record-binding (var as
2185    /// composite carrying all columns) queues with v7.40 record
2186    /// type infrastructure.
2187    ForQuery {
2188        var: String,
2189        query: Box<SelectStatement>,
2190        body: Vec<PlPgSqlStmt>,
2191    },
2192    /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2193    /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2194    /// computed at runtime from a text expression, parsed on the
2195    /// fly, then iterated. Enables dynamic queries where the
2196    /// projection / FROM / WHERE clauses depend on runtime values.
2197    ForExecute {
2198        var: String,
2199        sql_expr: Expr,
2200        body: Vec<PlPgSqlStmt>,
2201    },
2202}
2203
2204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2205pub enum RaiseLevel {
2206    /// `RAISE NOTICE` — diagnostic message, observable in the
2207    /// server log. Does not affect the trigger's outcome.
2208    Notice,
2209    /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2210    Warning,
2211    /// `RAISE INFO` — like NOTICE, slightly quieter.
2212    Info,
2213    /// `RAISE LOG` — like NOTICE, lower priority.
2214    Log,
2215    /// `RAISE DEBUG` — like NOTICE, lowest priority.
2216    Debug,
2217    /// `RAISE EXCEPTION` — aborts the trigger function with the
2218    /// given message, propagating up to the caller as a query-
2219    /// level error.
2220    Exception,
2221}
2222
2223#[derive(Debug, Clone, PartialEq)]
2224pub enum AssignTarget {
2225    NewColumn(String),
2226    OldColumn(String),
2227    /// Reserved for v7.12.5 DECLARE'd local variables.
2228    Local(String),
2229}
2230
2231#[derive(Debug, Clone, PartialEq)]
2232pub enum ReturnTarget {
2233    /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2234    /// actually gets written (possibly with NEW.col mutations
2235    /// applied). For AFTER triggers, the return value is ignored.
2236    New,
2237    /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2238    /// the delete proceed; for BEFORE UPDATE / INSERT it's
2239    /// equivalent to dropping the write.
2240    Old,
2241    /// `RETURN NULL;` — for BEFORE triggers, skips the write
2242    /// entirely. For AFTER, the return value is ignored.
2243    Null,
2244    /// `RETURN <expr>;` — non-row return shape; reserved for the
2245    /// scalar UDF surface in v7.12.5+. Executor errors when used
2246    /// inside a trigger function.
2247    Expr(Expr),
2248}
2249
2250/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2251/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2252/// but the executor refuses them. `WHEN (cond)` clauses are out
2253/// of scope; the trigger function can short-circuit on a leading
2254/// IF inside its body once v7.12.5 lands IF.
2255#[derive(Debug, Clone, PartialEq)]
2256pub struct CreateTriggerStatement {
2257    pub name: String,
2258    pub or_replace: bool,
2259    pub timing: TriggerTiming,
2260    /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2261    /// three entries in order.
2262    pub events: Vec<TriggerEvent>,
2263    pub table: String,
2264    /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2265    /// only `Row`; `Statement` parses but the executor refuses.
2266    pub for_each: TriggerForEach,
2267    /// Name of the function to invoke. The function must exist at
2268    /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2269    /// forward reference (`function no_such_fn() does not exist`), so
2270    /// requiring it IS the PG behaviour (the old note claimed the
2271    /// opposite).
2272    pub function: String,
2273    /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2274    /// (mailrs round-5 G7). Non-empty only when the events list
2275    /// contains UPDATE and the user wrote the column-list filter.
2276    /// PG fires the trigger only when at least one of these
2277    /// columns appears in the SET clause; SPG conservatively
2278    /// fires on any UPDATE matching the listed columns or
2279    /// rewriting them at the row level. Empty vec = no filter
2280    /// (fire on every UPDATE).
2281    pub update_columns: Vec<String>,
2282    /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2283    /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2284    /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2285    pub when_condition: Option<Expr>,
2286}
2287
2288/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2289#[derive(Debug, Clone, PartialEq)]
2290pub struct CreateRuleStatement {
2291    pub name: String,
2292    pub or_replace: bool,
2293    /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2294    pub event: String,
2295    pub table: String,
2296    /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2297    /// (run alongside; PG's default when neither keyword is written).
2298    pub instead: bool,
2299    /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2300    pub when_condition: Option<Expr>,
2301    /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2302    pub commands: Vec<Statement>,
2303}
2304
2305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2306pub enum TriggerTiming {
2307    /// Fires before the row is written; the trigger function's
2308    /// return value (NEW or NULL) decides the row content and
2309    /// whether the write proceeds at all.
2310    Before,
2311    /// Fires after the row is written; the return value is
2312    /// ignored.
2313    After,
2314    /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2315    /// v7.12.4 (SPG has no updatable-view surface).
2316    InsteadOf,
2317}
2318
2319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2320pub enum TriggerEvent {
2321    Insert,
2322    Update,
2323    Delete,
2324    /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2325    /// so the trigger never fires.
2326    Truncate,
2327}
2328
2329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2330pub enum TriggerForEach {
2331    Row,
2332    Statement,
2333}
2334
2335/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2336///
2337/// SPG's index does not scan in a direction, but `indexdef` reproduces
2338/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2339/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2340/// which case PG's default applies — LAST for ascending, FIRST for
2341/// descending, and neither is rendered.
2342#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2343pub struct IndexColumnOrder {
2344    pub descending: bool,
2345    pub nulls_first: Option<bool>,
2346}
2347
2348#[derive(Debug, Clone, PartialEq)]
2349pub struct CreateIndexStatement {
2350    pub name: String,
2351    /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2352    /// either way, so this changes nothing about how the index is made
2353    /// — it is carried because PG refuses the CONCURRENTLY form inside
2354    /// a transaction block and accepts the plain one, and the engine
2355    /// cannot tell them apart without it.
2356    pub concurrently: bool,
2357    /// v7.39 (round 537) — the leading key column's ordering clause,
2358    /// which is the column SPG indexes.
2359    pub key_order: IndexColumnOrder,
2360    /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2361    /// written. SPG orders text by bytes, so honouring it changes
2362    /// nothing; PG prints it, because an explicitly named collation and
2363    /// the one a column inherits are different objects.
2364    pub key_collation: Option<String>,
2365    pub table: String,
2366    pub column: String,
2367    /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2368    /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2369    /// any NULL in the key exempts the row from the uniqueness check.
2370    pub nulls_not_distinct: bool,
2371    /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2372    /// graph for vector kNN); unspecified is the default B-tree index.
2373    pub method: IndexMethod,
2374    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2375    /// index name already exists, instead of raising `DuplicateIndex`.
2376    pub if_not_exists: bool,
2377    /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2378    /// non-key columns the planner should treat as "covered" by
2379    /// this index when checking whether a query can run as an
2380    /// index-only scan. Empty when no `INCLUDE` clause was given.
2381    pub included_columns: Vec<String>,
2382    /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2383    /// for which `<expr>` evaluates truthy enter the index;
2384    /// queries whose `WHERE` clause's canonical Display form
2385    /// matches this expression's Display form can be served by the
2386    /// partial index. Stored as a parsed `Expr` so the engine
2387    /// re-uses the existing evaluation path; storage persists the
2388    /// Display form on the catalog snapshot.
2389    pub partial_predicate: Option<Expr>,
2390    /// v6.8.2 — expression-based index. When `Some(expr)`, the
2391    /// index key is the result of `expr` evaluated on each row
2392    /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2393    /// field still names the *primary* column the expression
2394    /// touches so existing planner shortcuts that resolve a
2395    /// column position stay valid. `None` = plain
2396    /// column-reference index (the legacy shape).
2397    pub expression: Option<Expr>,
2398    /// v7.9.14 — extra column names after the leading column in a
2399    /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2400    /// planner today still only uses the leading column for index
2401    /// seeks; the extras are tracked verbatim so the same DDL
2402    /// round-trips through WAL replay + catalog snapshot, and so
2403    /// the engine can emit a clear warning at INDEX CREATE time
2404    /// that only the leading column is currently honoured.
2405    /// Composite BTree index keys land in v7.10.
2406    pub extra_columns: Vec<String>,
2407    /// v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
2408    /// / `NULLS LAST`, positionally aligned with `extra_columns`. The
2409    /// parser used to discard these, so a composite index's direction
2410    /// survived only on the leading column and `pg_get_indexdef`
2411    /// rendered `(a, b DESC)` back as `(a, b)`.
2412    pub extra_orders: Vec<IndexColumnOrder>,
2413    /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2414    /// enforces uniqueness on the indexed key (combined with the
2415    /// `partial_predicate` filter — only rows where the predicate
2416    /// evaluates truthy enter the uniqueness check). Standard SQL
2417    /// and PG's canonical way to express conditional uniqueness.
2418    /// mailrs K1.
2419    pub is_unique: bool,
2420    /// v7.15.0 — operator class on the leading column, when the
2421    /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2422    /// Lower-cased. Most opclasses are still informational; the
2423    /// engine routes on `gin_trgm_ops` specifically to build a
2424    /// trigram-shingle GIN over a TEXT column, and otherwise
2425    /// keeps the current "accepted and discarded" behaviour for
2426    /// pg_dump compatibility.
2427    pub opclass: Option<String>,
2428    /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2429    /// there was no `USING` clause.
2430    ///
2431    /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2432    /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2433    /// implementation for still load. That degradation is deliberate, but
2434    /// it loses the name — and the operator-class check needs it, both to
2435    /// look the class up under the AM the user actually named and to say
2436    /// which AM it was missing from, the way PG's message does.
2437    pub method_name: Option<String>,
2438}
2439
2440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2441pub enum IndexMethod {
2442    /// Default — B-tree over `IndexKey`. Used for equality / range
2443    /// lookups on scalar columns.
2444    BTree,
2445    /// `USING hnsw` — NSW graph for kNN over a vector column.
2446    Hnsw,
2447    /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2448    /// metadata that records (min_key, max_key) for each page in a
2449    /// cold-tier segment, on the indexed column. The optimizer
2450    /// can use these summaries to skip pages whose range does NOT
2451    /// overlap a query's WHERE predicate. BRIN indexes carry no
2452    /// in-memory data — the summaries live in the segment v2
2453    /// envelope's sidecar. Created via the standard
2454    /// `CREATE INDEX … USING brin (col)` syntax.
2455    Brin,
2456    /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2457    /// column. Posting lists map `lexeme word` → row locators; the
2458    /// planner uses them to narrow `WHERE col @@ tsquery` to the
2459    /// candidate rows whose vectors contain a matching term, then
2460    /// re-evaluates the full `@@` semantics on each candidate.
2461    /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2462    /// silently degraded to a full scan at query time.
2463    Gin,
2464}
2465
2466/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2467/// inside a CREATE TABLE column list.
2468///
2469/// The source table's shape can only be read from the catalog, so the
2470/// parser records the clause and the engine expands it. `at` is how many
2471/// explicit columns preceded it: PG keeps the written order, so
2472/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2473#[derive(Debug, Clone, PartialEq)]
2474pub struct LikeSpec {
2475    pub source: String,
2476    pub at: usize,
2477    pub options: LikeOptions,
2478    /// v7.40.0 — MySQL's `CREATE TABLE b LIKE a` keeps the source's
2479    /// index names (`PRIMARY`, `ks`); PostgreSQL's
2480    /// `CREATE TABLE b (LIKE a INCLUDING ALL)` renames the copies after
2481    /// the new table (`lb_pkey`, `lb_s_idx`). Both measured. The
2482    /// spelling says which engine's rule applies.
2483    pub keep_index_names: bool,
2484}
2485
2486/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2487/// types and NOT NULL and nothing else — measured on PG18, where a
2488/// copied generated column becomes a plain one and a copied identity
2489/// column loses its identity.
2490#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2491pub struct LikeOptions {
2492    pub defaults: bool,
2493    pub constraints: bool,
2494    pub identity: bool,
2495    pub generated: bool,
2496    pub indexes: bool,
2497    pub comments: bool,
2498}
2499
2500#[derive(Debug, Clone, PartialEq)]
2501pub struct CreateTableStatement {
2502    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2503    /// creating session's own namespace: it shadows a permanent table of the
2504    /// same name, other sessions never see it, and it is dropped when the
2505    /// session ends. A `bool` here lands in the struct's existing padding.
2506    pub temporary: bool,
2507    pub name: String,
2508    /// v7.39 — the `ENGINE=` a MySQL dump names. Consumed and discarded
2509    /// before, so `ENGINE=NONSUCH` built a table where MySQL 9.7.2
2510    /// answers `ERROR 1286`, and `sql_mode` claimed
2511    /// `NO_ENGINE_SUBSTITUTION` while doing it.
2512    pub engine: Option<String>,
2513    /// v7.40.0 — the `AUTO_INCREMENT=N` table option: the next value
2514    /// the table hands out. It was consumed and dropped, so the first
2515    /// row of a table declared `AUTO_INCREMENT=100` got 1 where MySQL
2516    /// 9.7.2 gives it 100 — and `SHOW CREATE TABLE`, which reproduces
2517    /// the option from the counter, round-tripped a different number.
2518    pub auto_increment: Option<i64>,
2519    pub columns: Vec<ColumnDef>,
2520    /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2521    /// the order written. Empty for a table that has none.
2522    pub like_specs: Vec<LikeSpec>,
2523    /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2524    /// Empty for a table that inherits from nothing. Order matters:
2525    /// the child takes each parent's columns in this order before its
2526    /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2527    pub inherits: Vec<String>,
2528    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2529    /// table name already exists, instead of raising `DuplicateTable`.
2530    pub if_not_exists: bool,
2531    /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2532    /// constraints. Column-level `REFERENCES` (single-column inline
2533    /// form) is normalised into this vec at parse time so the engine
2534    /// sees one uniform list.
2535    pub foreign_keys: Vec<ForeignKeyConstraint>,
2536    /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2537    /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2538    /// Engine resolves each into a BTree index named after the
2539    /// constraint's leading column at CREATE TABLE time; INSERT
2540    /// path enforces composite uniqueness via row scan on the
2541    /// leading column index.
2542    pub table_constraints: Vec<TableConstraint>,
2543    /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2544    /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2545    /// the engine creates a parent table whose own rows stay
2546    /// empty and routes INSERT/SELECT through children. Mutually
2547    /// exclusive with `partition_of` (parser enforces).
2548    pub partition_by: Option<PartitionBySpec>,
2549    /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2550    /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2551    /// the table inherits its column list from `parent` (the
2552    /// parser rejects an explicit column list when this is set);
2553    /// engine routes child rows back to the parent at INSERT.
2554    pub partition_of: Option<PartitionOfSpec>,
2555}
2556
2557/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2558/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2559/// future LIST / HASH without breaking the public AST shape.
2560#[derive(Debug, Clone, PartialEq)]
2561pub struct PartitionBySpec {
2562    pub kind: PartitionKindAst,
2563    /// One or more ident references into the parent's column list.
2564    /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2565    /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2566    /// shape PG-compatible.
2567    pub key_columns: Vec<String>,
2568}
2569
2570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2571pub enum PartitionKindAst {
2572    Range,
2573    /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2574    /// `FOR VALUES IN (lit, lit, …)`.
2575    List,
2576    /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2577    /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2578    Hash,
2579}
2580
2581/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2582/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2583/// or the catch-all `DEFAULT` partition.
2584#[derive(Debug, Clone, PartialEq)]
2585pub struct PartitionOfSpec {
2586    pub parent_name: String,
2587    pub bounds: PartitionOfBoundsAst,
2588}
2589
2590#[derive(Debug, Clone, PartialEq)]
2591pub enum PartitionOfBoundsAst {
2592    /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2593    /// (lits include vector bodies), so we box both bounds to keep
2594    /// the variant size in line with `Default` for clippy and to
2595    /// minimise per-statement footprint when the partition shape
2596    /// isn't in use.
2597    Range {
2598        lower: Box<Expr>,
2599        upper: Box<Expr>,
2600    },
2601    /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2602    /// expr resolves to a typed literal at child-create time.
2603    List {
2604        values: Vec<Expr>,
2605    },
2606    /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2607    /// PG enforces `0 ≤ r < m`; m must be positive.
2608    Hash {
2609        modulus: u32,
2610        remainder: u32,
2611    },
2612    Default,
2613}
2614
2615/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2616/// column list. Either a composite PRIMARY KEY or a UNIQUE
2617/// (single- or multi-column).
2618#[derive(Debug, Clone, PartialEq)]
2619pub enum TableConstraint {
2620    /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2621    /// referenced column. Engine builds a BTree index named
2622    /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2623    PrimaryKey {
2624        name: Option<String>,
2625        columns: Vec<String>,
2626        /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2627        /// Round 621 consumed the clauses; these carry them.
2628        deferrable: bool,
2629        initially_deferred: bool,
2630    },
2631    /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2632    /// named `<table>_<leading_col>_key` (single-column) or
2633    /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2634    /// uniqueness on INSERT.
2635    Unique {
2636        name: Option<String>,
2637        columns: Vec<String>,
2638        /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2639        /// G10). PG 15+ flips the NULL handling so any number of
2640        /// NULL rows collide on the constraint. Default is
2641        /// `false` (NULLS DISTINCT, standard SQL behaviour).
2642        nulls_not_distinct: bool,
2643        /// v7.39 (round 711) — see PrimaryKey.
2644        deferrable: bool,
2645        initially_deferred: bool,
2646        /// v7.40.0 — MySQL's per-column index prefix on a
2647        /// `UNIQUE KEY k (b(4))`, aligned with `columns`. Unlike a
2648        /// plain KEY's, this one CHANGES what the constraint accepts:
2649        /// MySQL rejects two rows sharing the first four characters.
2650        /// Empty for every PostgreSQL spelling.
2651        prefix_lengths: Vec<Option<u32>>,
2652    },
2653    /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2654    /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2655    /// this same variant at parse time. Engine evaluates the
2656    /// predicate against each INSERT/UPDATE candidate row; a
2657    /// false / NULL result rejects the mutation.
2658    /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2659    /// PG adds such a constraint without scanning the existing rows: new
2660    /// rows are checked, the ones already there are grandfathered in, and
2661    /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2662    /// scans and flips it. pg_dump emits the suffix for exactly those, so
2663    /// validating them on restore would refuse a dump PG itself produced.
2664    Check {
2665        name: Option<String>,
2666        expr: Expr,
2667        not_valid: bool,
2668    },
2669    /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2670    /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2671    /// every element (the booking/scheduling non-overlap constraint,
2672    /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2673    /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2674    /// enforcement doesn't build the index yet). Each element pairs a
2675    /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2676    Exclude {
2677        name: Option<String>,
2678        method: Option<String>,
2679        elements: Vec<(String, String)>,
2680    },
2681    /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2682    /// non-unique secondary-index declaration inline in CREATE
2683    /// TABLE. Engine builds a BTree index on the leading column
2684    /// (composite columns parse but only the leading column is
2685    /// honoured at v7.15 — matches the existing
2686    /// `CreateIndexStatement::extra_columns` semantics). Useful
2687    /// for `mysql/blog`-style schemas that lean on routine
2688    /// secondary indexes for ORM lookups.
2689    Index {
2690        name: Option<String>,
2691        columns: Vec<String>,
2692        /// v7.40.0 — MySQL's per-column index prefix, `KEY k (b(4))`,
2693        /// positionally aligned with `columns`. `None` for a column
2694        /// written without one, which is every PostgreSQL index key.
2695        ///
2696        /// It was skipped by the parser and dropped, so the declaration
2697        /// was accepted and the index built over the whole column with
2698        /// nothing recording that a prefix had been asked for —
2699        /// `SHOW INDEX` then reported `Sub_part` NULL and
2700        /// `SHOW CREATE TABLE` printed `(b)` where MySQL 9.7.2 prints
2701        /// `(b(4))`.
2702        prefix_lengths: Vec<Option<u32>>,
2703    },
2704    /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2705    /// (cols)` inline declaration. Pre-v7.17 the parser
2706    /// silently dropped these so MyISAM-imported FULLTEXT
2707    /// indexes vanished; v7.17 routes them through the
2708    /// existing tsvector-GIN engine path so MATCH AGAINST
2709    /// queries get a real inverted index instead of falling
2710    /// back to a full scan. Multi-column FULLTEXT KEYs build
2711    /// one GIN per column at v7.17 (per-column posting lists);
2712    /// the leading column drives query planning.
2713    FulltextIndex {
2714        name: Option<String>,
2715        columns: Vec<String>,
2716    },
2717}
2718
2719#[derive(Debug, Clone, PartialEq)]
2720#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2721pub struct ColumnDef {
2722    pub name: String,
2723    pub ty: ColumnTypeName,
2724    pub nullable: bool,
2725    /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2726    /// evaluates this once (with an empty row) and caches the resulting
2727    /// `Value` on the column schema.
2728    pub default: Option<Expr>,
2729    /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2730    /// per such column and fills the slot when INSERT leaves it
2731    /// unbound (omitted from a column-list INSERT or explicitly NULL).
2732    pub auto_increment: bool,
2733    /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2734    /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2735    /// an implicit BTree index named `<table>_pkey` over this
2736    /// column at CREATE TABLE time, satisfying the parent-side
2737    /// index requirement for any FOREIGN KEY pointing at it.
2738    pub is_primary_key: bool,
2739    /// v7.13.0 — inline `UNIQUE` column constraint
2740    /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2741    /// into a single-column `TableConstraint::Unique` so the
2742    /// engine path stays uniform with table-level UNIQUE.
2743    pub is_unique: bool,
2744    /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2745    /// inline column constraint: treat NULL keys as equal so only one NULL
2746    /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2747    /// `TableConstraint::Unique { nulls_not_distinct }`.
2748    pub unique_nulls_not_distinct: bool,
2749    /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2750    /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2751    /// since this round so the fold into the table-level constraint keeps it.
2752    pub constraint_deferrable: bool,
2753    pub constraint_initially_deferred: bool,
2754    /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2755    /// (mailrs round-5 G3). Stored alongside the column so the
2756    /// CREATE TABLE handler can fold these into table-level
2757    /// CHECK constraints. Multiple inline CHECKs on the same
2758    /// column are concatenated with AND at the table level.
2759    pub check: Option<Expr>,
2760    /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2761    /// parser sees an unknown column-type ident (anything not in
2762    /// the built-in `parse_column_type_name` table), it sets
2763    /// `ty = ColumnTypeName::Text` and records the original name
2764    /// here. The engine resolves at CREATE TABLE time: if a
2765    /// catalog enum/domain with this name exists, the column is
2766    /// bound to it (label-checked on INSERT for enums; CHECK-
2767    /// constrained for domains); otherwise the CREATE TABLE
2768    /// errors with "unknown type".
2769    pub user_type_ref: Option<String>,
2770    /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2771    /// CURRENT_TIMESTAMP` column attribute. When set, an
2772    /// UPDATE that does NOT explicitly bind this column
2773    /// overrides the new value with `now()` (engine clock).
2774    /// Pre-v7.17 SPG silently accepted the syntax and never
2775    /// fired the override — `updated_at` columns from mysqldump
2776    /// stayed pinned at their initial DEFAULT forever, an
2777    /// audit Tier-S silent-failure. Generalised as a stored
2778    /// expression source so future shapes (`ON UPDATE
2779    /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2780    /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2781    pub on_update_runtime: Option<Expr>,
2782    /// v7.17.0 Phase 2.5 — text collation derived from the
2783    /// post-fix `COLLATE <name>` clause (and / or the table-level
2784    /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2785    /// per column). Pre-2.5 SPG accepted the clause and
2786    /// discarded the name, leaving every column byte-compared
2787    /// — a Tier-S silent failure when the customer expected
2788    /// `_ci` / `case_insensitive` semantics. Parser normalises
2789    /// the raw collation name into the variants in `Collation`.
2790    /// Default `Binary` preserves the legacy compare path.
2791    pub collation: Collation,
2792    /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2793    /// explicit `COLLATE <name>` clause rather than the default. Under the
2794    /// MySQL dialect a text column with NO explicit clause takes the
2795    /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2796    /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2797    /// flag is the only thing that tells them apart.
2798    pub collation_explicit: bool,
2799    /// v7.39 (round 676) — the collation name AS WRITTEN, because
2800    /// `collation` above cannot carry it: `Collation` is a two-variant
2801    /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2802    /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2803    /// tell them apart.
2804    pub collation_name: Option<String>,
2805    /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2806    /// 4.4 SPG accepted and discarded the keyword, leaving
2807    /// negative values silently accepted on a column the
2808    /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2809    /// rejects negative INSERT / UPDATE values on UNSIGNED int
2810    /// columns. SPG widening to `u64`-shaped storage is out of
2811    /// v7.17 scope; the upper bound remains the signed-type max
2812    /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2813    /// exceeds what every mailrs / Rails app actually uses.
2814    pub is_unsigned: bool,
2815    /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2816    /// value list captured at parse time. When `Some`, the parser
2817    /// recognised `ENUM(...)` in the type slot; the engine
2818    /// validates INSERT cells against this list at
2819    /// column_def_to_schema time and persists the variants on
2820    /// `ColumnSchema.inline_enum_variants`. None for all
2821    /// non-ENUM columns.
2822    pub inline_enum_variants: Option<Vec<String>>,
2823    /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2824    /// value list. Distinct from ENUM (subset semantics rather
2825    /// than pick-one). None for all non-SET columns.
2826    pub inline_set_variants: Option<Vec<String>>,
2827    /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2828    /// STORED` computed-column source. When `Some`, the engine
2829    /// stores the Display-form of the parsed expression on
2830    /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2831    /// and re-evaluates the expression against every INSERT /
2832    /// UPDATE candidate row, overwriting whatever the caller
2833    /// supplied for this column. Boxed to keep `ColumnDef` from
2834    /// blowing past the `large_enum_variant` clippy ceiling
2835    /// (`Expr` widens with vector literals).
2836    pub generated_stored_expr: Option<Box<Expr>>,
2837    /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2838    /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2839    /// `auto_increment`; this additionally marks the ALWAYS one, whose
2840    /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2841    /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2842    /// VALUE`. Only meaningful when the column is also an identity column.
2843    pub identity_always: bool,
2844    /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2845    /// integer width (TINYINT / MEDIUMINT), captured before the type
2846    /// collapses to SmallInt / Int. The engine copies it to
2847    /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2848    /// path can enforce the real range. None for every other column and
2849    /// under the PG dialect.
2850    pub mysql_int_width: Option<MysqlIntWidth>,
2851    /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2852    /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2853    /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2854    /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2855    /// CREATE TABLE time so the write path can truncate and the render path
2856    /// can pad. None under the PG dialect, where temporal columns keep full
2857    /// microseconds.
2858    pub mysql_fsp: Option<u8>,
2859    /// v7.39.2 — the column was written `TIMESTAMP` rather than
2860    /// `DATETIME` in a MySQL session. The engine copies it to
2861    /// `ColumnSchema.mysql_declared_timestamp` at CREATE TABLE.
2862    pub mysql_declared_timestamp: bool,
2863    /// v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair, copied to
2864    /// `ColumnSchema.mysql_float_md` at CREATE TABLE.
2865    pub mysql_float_md: Option<(u8, u8)>,
2866}
2867
2868/// v7.17.0 Phase 2.5 — text collation classification surfaced
2869/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2870/// engine bridges between the two at CREATE TABLE time.
2871///
2872/// Recognised collation-name patterns (case-insensitive):
2873///   * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase`         → CaseInsensitive
2874///   * Everything else (`C`, `POSIX`, `default`,
2875///     `pg_catalog.default`, `*_cs`, `*_bin`, unknown names)   → Binary
2876#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2877pub enum Collation {
2878    Binary,
2879    CaseInsensitive,
2880}
2881
2882/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2883/// integer width for a column whose `ColumnTypeName` is too wide to carry
2884/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2885/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2886/// TABLE time. Only recorded under the MySQL dialect.
2887#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2888pub enum MysqlIntWidth {
2889    Tiny,
2890    Small,
2891    Medium,
2892    Int,
2893    /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2894    Big,
2895}
2896
2897#[allow(clippy::derivable_impls)]
2898impl Default for Collation {
2899    fn default() -> Self {
2900        Self::Binary
2901    }
2902}
2903
2904impl Collation {
2905    /// Classify a `COLLATE <name>` ident into one of the supported
2906    /// variants. Empty / unknown names fall back to `Binary` —
2907    /// matches the pre-2.5 silent-accept behaviour for snapshots
2908    /// that load through but don't actually depend on the
2909    /// collation semantics.
2910    #[must_use]
2911    pub fn from_collation_name(name: &str) -> Self {
2912        let lc = name.trim().to_ascii_lowercase();
2913        // Strip any quotes / schema-qualifier the parser left on
2914        // (e.g. `pg_catalog.default`).
2915        let bare = lc
2916            .trim_matches(|c: char| c == '"' || c == '\'')
2917            .rsplit('.')
2918            .next()
2919            .unwrap_or("");
2920        if bare.is_empty() {
2921            return Self::Binary;
2922        }
2923        if bare == "case_insensitive" || bare == "nocase" {
2924            return Self::CaseInsensitive;
2925        }
2926        // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2927        // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2928        if bare.ends_with("_ci") {
2929            return Self::CaseInsensitive;
2930        }
2931        Self::Binary
2932    }
2933}
2934
2935/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2936/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2937/// parse into this shape — the column-level form has a single-entry
2938/// `columns` / `parent_columns`.
2939#[derive(Debug, Clone, PartialEq)]
2940pub struct ForeignKeyConstraint {
2941    /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2942    /// today but parses + stores it so a future ALTER TABLE DROP
2943    /// CONSTRAINT can target by name (v7.6.8).
2944    pub name: Option<String>,
2945    /// Local columns participating in the FK (≥ 1).
2946    pub columns: Vec<String>,
2947    /// Referenced parent table.
2948    pub parent_table: String,
2949    /// Referenced parent columns. Must have the same arity as
2950    /// `columns`; engine validates parent has a PK / UNIQUE index
2951    /// on exactly this column set (v7.6.1).
2952    pub parent_columns: Vec<String>,
2953    /// `ON DELETE` action. Defaults to `Restrict` if absent.
2954    pub on_delete: FkAction,
2955    /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2956    pub on_update: FkAction,
2957    /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2958    pub match_type: MatchType,
2959    /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2960    /// dropped on the floor, so a constraint declared DEFERRABLE was
2961    /// enforced immediately and a circular-FK migration could not load.
2962    pub deferrable: bool,
2963    /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2964    /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2965    pub initially_deferred: bool,
2966}
2967
2968/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2969/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2970/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2971#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2972pub enum MatchType {
2973    #[default]
2974    Simple,
2975    Full,
2976}
2977
2978/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2979#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2980pub enum FkAction {
2981    /// Reject the parent mutation if any child row references it.
2982    /// SQL spec default; SPG default when no clause is given.
2983    Restrict,
2984    /// Recursively propagate the parent's delete / update to the
2985    /// child rows. Same TX.
2986    Cascade,
2987    /// Set the child FK column(s) to NULL. Requires the FK columns
2988    /// to be NULL-able.
2989    SetNull,
2990    /// Set the child FK column(s) to their declared DEFAULT.
2991    /// Requires the child column(s) to have DEFAULT.
2992    SetDefault,
2993    /// SQL spec `NO ACTION` (deferred check). SPG treats this as
2994    /// `Restrict` because the single-writer model has no deferred
2995    /// constraint window; the keyword is accepted for compatibility.
2996    NoAction,
2997}
2998
2999/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
3000/// optional `USING <encoding>` clause; omitting it keeps the
3001/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
3002/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
3003/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
3004/// binary16 (2× compression, ~3 decimal digits of precision).
3005#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3006pub enum VecEncoding {
3007    /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
3008    /// uncompressed `vector` type wire / storage layout.
3009    #[default]
3010    F32,
3011    /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
3012    /// `spg_storage::quantize::Sq8Vector` for the math + recall
3013    /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
3014    /// dim ≥ 32).
3015    Sq8,
3016    /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
3017    /// per-element. DDL keyword `HALF` (pgvector convention).
3018    /// Bit-exact dequantise to f32 at the storage layer; no
3019    /// rerank pass needed for kNN search.
3020    F16,
3021}
3022
3023impl fmt::Display for VecEncoding {
3024    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3025        match self {
3026            Self::F32 => f.write_str("F32"),
3027            Self::Sq8 => f.write_str("SQ8"),
3028            // pgvector convention: DDL keyword is `HALF`, not `F16`.
3029            Self::F16 => f.write_str("HALF"),
3030        }
3031    }
3032}
3033
3034/// SQL-level type names. The mapping to the storage runtime's `DataType`
3035/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
3036#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3037pub enum ColumnTypeName {
3038    /// v7.39 (round 291) — PG's `name`, the identifier type its
3039    /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
3040    /// answered `type "name" does not exist` to.
3041    Name,
3042    /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
3043    /// 32-bit wrapping counter the row header carries; `xid8` is the
3044    /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
3045    /// SPG answered `type "xid" does not exist` to.
3046    Xid,
3047    Xid8,
3048    /// v7.39 (round 667) — `OID`. `XID` was already a column type here
3049    /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
3050    /// `type "oid" does not exist` while `t(x XID)` built fine.
3051    Oid,
3052    SmallInt,
3053    Int,
3054    BigInt,
3055    Float,
3056    /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
3057    /// IEEE. It used to map to [`Self::Float`] on the theory that a
3058    /// wider float is harmless, but the width is observable: a `real`
3059    /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
3060    /// answered false where PG answers true.
3061    Real,
3062    Text,
3063    /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
3064    Varchar(u32),
3065    /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
3066    Char(u32),
3067    Bool,
3068    /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
3069    /// `USING <encoding>` clause; omitting it surfaces as
3070    /// `encoding = VecEncoding::F32` (the pre-v6 default).
3071    Vector {
3072        dim: u32,
3073        encoding: VecEncoding,
3074    },
3075    /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
3076    /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
3077    /// v7.39 (round 271) — scale widened to u16 alongside the value's.
3078    /// v7.39 (round 272) — precision too: PG's runs to 1000.
3079    /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
3080    /// a negative one rounds to tens / hundreds. A VALUE's display scale
3081    /// stays unsigned.
3082    Numeric(u16, i16),
3083    /// `DATE` — calendar day, no time-of-day component.
3084    Date,
3085    /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
3086    /// precision.
3087    Timestamp,
3088    /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
3089    /// stores all timestamps as UTC microseconds-since-epoch and
3090    /// does not carry per-row offset (PG's internal representation
3091    /// is the same — TZ is a display convention). The distinction
3092    /// from `TIMESTAMP` exists for the PG-wire layer to advertise
3093    /// OID 1184 so sqlx-style clients decode into
3094    /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
3095    Timestamptz,
3096    /// v4.9 `JSON` — text-backed JSON document. No parse-time
3097    /// validation; the engine round-trips the literal verbatim.
3098    /// PG OID 114 on the wire.
3099    Json,
3100    /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
3101    /// PG OID 3802 on the wire so sqlx-style binary-typed clients
3102    /// decode without a custom type registration.
3103    Jsonb,
3104    /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
3105    /// Literal forms (decoded by the engine at coercion time):
3106    ///   - PG hex form: `'\xDEADBEEF'`
3107    ///   - Escape form: `'foo\\000bar'` (backslash octal triples)
3108    Bytes,
3109    /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
3110    /// OID 1009. Literal forms accepted by the parser:
3111    ///   - `ARRAY['a', 'b', NULL]`
3112    ///   - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
3113    ///     form at coerce time)
3114    TextArray,
3115    /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
3116    /// 1007. Same literal forms as TEXT[] (substituting integer
3117    /// elements).
3118    IntArray,
3119    /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
3120    /// OID 1016.
3121    BigIntArray,
3122    /// v7.40.0 `OID[]` — single-dimension oid array. PG wire OID
3123    /// 1028.
3124    ///
3125    /// `DataType::OidArray` and its value, codec tag, wire encoding
3126    /// and every naming surface have existed since v7.39 (round 694);
3127    /// what was missing was only the DDL spelling, so
3128    /// `CREATE TABLE t (c oid[])` answered `Oid[] not yet supported`
3129    /// while PostgreSQL 18.6 accepts it. Capability present, routing
3130    /// absent — the same shape as this repository's other hand-kept
3131    /// lists.
3132    OidArray,
3133    /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
3134    /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
3135    /// external form). G-CRIT-3.
3136    TsVector,
3137    /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
3138    /// wire OID 3615.
3139    TsQuery,
3140    /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
3141    /// Literal input accepts canonical hyphenated, unhyphenated,
3142    /// uppercase, and `{...}`-braced forms; display normalises to
3143    /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
3144    /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
3145    /// gen_random_uuid()`.
3146    Uuid,
3147    /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
3148    /// microseconds since 00:00:00. PG wire OID 1083. Literal
3149    /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
3150    /// (6-digit microsecond precision). Display normalises to
3151    /// the canonical `HH:MM:SS[.ffffff]`.
3152    Time,
3153    /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
3154    /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
3155    /// PG OID; advertised as INT4 on the wire. Display always
3156    /// 4 digits zero-padded.
3157    Year,
3158    /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
3159    /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
3160    /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
3161    /// Offset range: ±14 hours.
3162    TimeTz,
3163    /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
3164    /// (locale-independent storage). Wire OID 790. Literal input
3165    /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
3166    /// major units), optional leading `-`. Display: en_US locale.
3167    Money,
3168    /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
3169    /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
3170    /// — the engine bridges to `DataType::Range(RangeKind)`.
3171    Range(RangeKindAst),
3172    /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
3173    /// `text => text` map with NULL value support.
3174    Hstore,
3175    /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
3176    IntArray2D,
3177    BigIntArray2D,
3178    TextArray2D,
3179    /// v7.39 (read01 round 75) — `bool[][]`.
3180    BoolArray2D,
3181    /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
3182    /// three-field {months, days, micros} struct (PG-byte-equal),
3183    /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
3184    /// β-P2 `INTERVAL` was runtime-only — literal in expression
3185    /// position but rejected at CREATE TABLE.
3186    Interval,
3187    /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
3188    /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
3189    /// PG external form quotes each non-NULL element because
3190    /// interval text contains spaces / colons
3191    /// (`{"1 day","24:00:00",NULL}`).
3192    IntervalArray,
3193    /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3194    /// mirrors a scalar `ColumnTypeName` that already existed.
3195    BoolArray,
3196    SmallIntArray,
3197    FloatArray,
3198    NumericArray,
3199    DateArray,
3200    TimestampArray,
3201    TimestamptzArray,
3202    UuidArray,
3203    JsonArray,
3204    JsonbArray,
3205    BytesArray,
3206    VarcharArray,
3207    CharArray,
3208    /// v7.40.0 — five array spellings PG 18.6 accepts at
3209    /// `CREATE TABLE` and SPG refused at the type name. The
3210    /// element types were all present; only the `[]` step was.
3211    RealArray,
3212    TimeArray,
3213    TimeTzArray,
3214    InetArray,
3215    XmlArray,
3216    /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3217    /// as `Range(RangeKindAst)` — one column type variant covers
3218    /// all six builtin multiranges, kind pins the element type.
3219    /// Wire OIDs in pgwire.
3220    Multirange(RangeKindAst),
3221    /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3222    /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3223    /// Wire OIDs in pgwire.
3224    Point,
3225    Lseg,
3226    Path,
3227    PgBox,
3228    Polygon,
3229    Line,
3230    Circle,
3231    /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3232    Inet,
3233    Cidr,
3234    Macaddr,
3235    Macaddr8,
3236    /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3237    Bit(u32),
3238    /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3239    BitVarying(u32),
3240    Xml,
3241    Char1,
3242    MoneyArray,
3243}
3244
3245/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3246/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3247/// crate doesn't depend on storage. Bridged at engine boundary.
3248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3249pub enum RangeKindAst {
3250    Int4,
3251    Int8,
3252    Num,
3253    Ts,
3254    TsTz,
3255    Date,
3256}
3257
3258impl fmt::Display for ColumnTypeName {
3259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3260        match self {
3261            Self::SmallInt => f.write_str("SMALLINT"),
3262            Self::Int => f.write_str("INT"),
3263            Self::BigInt => f.write_str("BIGINT"),
3264            Self::Float => f.write_str("FLOAT"),
3265            Self::Real => f.write_str("REAL"),
3266            Self::Text => f.write_str("TEXT"),
3267            Self::Name => f.write_str("name"),
3268            Self::Xid => f.write_str("xid"),
3269            Self::Xid8 => f.write_str("xid8"),
3270            Self::Oid => f.write_str("oid"),
3271            Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3272            Self::Char(n) => write!(f, "CHAR({n})"),
3273            Self::Bool => f.write_str("BOOL"),
3274            Self::Vector { dim, encoding } => match encoding {
3275                VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3276                VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3277                VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3278            },
3279            Self::Json => f.write_str("JSON"),
3280            Self::Jsonb => f.write_str("JSONB"),
3281            Self::Bytes => f.write_str("BYTEA"),
3282            Self::TextArray => f.write_str("TEXT[]"),
3283            Self::IntArray => f.write_str("INT[]"),
3284            Self::BigIntArray => f.write_str("BIGINT[]"),
3285            Self::OidArray => f.write_str("oid[]"),
3286            Self::TsVector => f.write_str("TSVECTOR"),
3287            Self::TsQuery => f.write_str("TSQUERY"),
3288            Self::Uuid => f.write_str("UUID"),
3289            Self::Numeric(p, s) => {
3290                if *s == 0 {
3291                    write!(f, "NUMERIC({p})")
3292                } else {
3293                    write!(f, "NUMERIC({p}, {s})")
3294                }
3295            }
3296            Self::Date => f.write_str("DATE"),
3297            Self::Timestamp => f.write_str("TIMESTAMP"),
3298            Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3299            Self::Time => f.write_str("TIME"),
3300            Self::Year => f.write_str("YEAR"),
3301            Self::TimeTz => f.write_str("TIMETZ"),
3302            Self::Money => f.write_str("MONEY"),
3303            Self::Range(k) => f.write_str(match k {
3304                RangeKindAst::Int4 => "INT4RANGE",
3305                RangeKindAst::Int8 => "INT8RANGE",
3306                RangeKindAst::Num => "NUMRANGE",
3307                RangeKindAst::Ts => "TSRANGE",
3308                RangeKindAst::TsTz => "TSTZRANGE",
3309                RangeKindAst::Date => "DATERANGE",
3310            }),
3311            Self::Hstore => f.write_str("HSTORE"),
3312            Self::Interval => f.write_str("INTERVAL"),
3313            Self::IntervalArray => f.write_str("INTERVAL[]"),
3314            Self::BoolArray => f.write_str("BOOL[]"),
3315            Self::SmallIntArray => f.write_str("SMALLINT[]"),
3316            Self::FloatArray => f.write_str("FLOAT[]"),
3317            Self::NumericArray => f.write_str("NUMERIC[]"),
3318            Self::DateArray => f.write_str("DATE[]"),
3319            Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3320            Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3321            Self::UuidArray => f.write_str("UUID[]"),
3322            Self::JsonArray => f.write_str("JSON[]"),
3323            Self::JsonbArray => f.write_str("JSONB[]"),
3324            Self::BytesArray => f.write_str("BYTEA[]"),
3325            Self::VarcharArray => f.write_str("VARCHAR[]"),
3326            Self::CharArray => f.write_str("CHAR[]"),
3327            Self::RealArray => f.write_str("REAL[]"),
3328            Self::TimeArray => f.write_str("TIME[]"),
3329            Self::TimeTzArray => f.write_str("TIMETZ[]"),
3330            Self::InetArray => f.write_str("INET[]"),
3331            Self::XmlArray => f.write_str("XML[]"),
3332            Self::Multirange(k) => f.write_str(match k {
3333                RangeKindAst::Int4 => "INT4MULTIRANGE",
3334                RangeKindAst::Int8 => "INT8MULTIRANGE",
3335                RangeKindAst::Num => "NUMMULTIRANGE",
3336                RangeKindAst::Ts => "TSMULTIRANGE",
3337                RangeKindAst::TsTz => "TSTZMULTIRANGE",
3338                RangeKindAst::Date => "DATEMULTIRANGE",
3339            }),
3340            Self::Point => f.write_str("POINT"),
3341            Self::Lseg => f.write_str("LSEG"),
3342            Self::Path => f.write_str("PATH"),
3343            Self::PgBox => f.write_str("BOX"),
3344            Self::Polygon => f.write_str("POLYGON"),
3345            Self::Line => f.write_str("LINE"),
3346            Self::Circle => f.write_str("CIRCLE"),
3347            Self::Inet => f.write_str("INET"),
3348            Self::Cidr => f.write_str("CIDR"),
3349            Self::Macaddr => f.write_str("MACADDR"),
3350            Self::Macaddr8 => f.write_str("MACADDR8"),
3351            Self::Bit(0) => f.write_str("BIT"),
3352            Self::Bit(n) => write!(f, "BIT({n})"),
3353            Self::BitVarying(0) => f.write_str("VARBIT"),
3354            Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3355            Self::Xml => f.write_str("XML"),
3356            Self::Char1 => f.write_str("\"char\""),
3357            Self::MoneyArray => f.write_str("MONEY[]"),
3358            Self::IntArray2D => f.write_str("INT[][]"),
3359            Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3360            Self::TextArray2D => f.write_str("TEXT[][]"),
3361            Self::BoolArray2D => f.write_str("BOOL[][]"),
3362        }
3363    }
3364}
3365
3366/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3367/// engine evaluates `expr` per matched row in the table's row order
3368/// and rewrites cells in place. Indexed columns are dropped + re-
3369/// inserted into the affected B-tree on each row change.
3370/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3371/// tail on a DML statement. Boxed off the statement struct so the PG-only
3372/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3373/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3374/// the identical meaning, so both share this one payload rather than each
3375/// growing its own.
3376#[derive(Debug, Clone, PartialEq)]
3377pub struct DmlOrderLimit {
3378    pub order_by: Vec<OrderBy>,
3379    pub limit: Option<u32>,
3380}
3381
3382/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3383/// FROM, kept so the engine can finish the job.
3384///
3385/// The parser rewrites the statement onto correlated subqueries, and it
3386/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3387/// name belongs to the target or to a source needs their column lists,
3388/// which parse time does not have. Carrying the clause lets the engine
3389/// — which has the catalog — resolve the rest.
3390#[derive(Debug, Clone, PartialEq)]
3391pub struct UpdateFromSources {
3392    pub from: FromClause,
3393    pub sub_where: Option<Expr>,
3394}
3395
3396#[derive(Debug, Clone, PartialEq)]
3397pub struct UpdateStatement {
3398    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3399    /// level UPDATE. Empty for a plain UPDATE.
3400    pub ctes: Vec<Cte>,
3401    pub table: String,
3402    /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3403    /// to `t`'s own rows and not to anything that descends from it.
3404    ///
3405    /// Round 644 taught the FROM clause the keyword and left DML behind
3406    /// because it needed a field here, and this struct carries a warning
3407    /// that round 413 measured widening it in place overflowing the
3408    /// parser's nesting stack. That warning was about `from_sources`, a
3409    /// struct wide enough to need boxing; a `bool` lands in the padding
3410    /// already present — same as `CreateTableStatement::temporary`.
3411    ///
3412    /// It also earns its keep beyond the spelling: the inheritance
3413    /// fan-out needs a way to say "the parent's own rows" as a
3414    /// statement, or running one on the parent recurses forever.
3415    pub only: bool,
3416    /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3417    /// statement's expressions refer to the target row by. PG allows the
3418    /// bare spelling here (unlike INSERT, which requires AS).
3419    pub alias: Option<String>,
3420    pub assignments: Vec<(String, Expr)>,
3421    /// v7.39 (round 533) — boxed: round 413 measured that widening this
3422    /// struct in place overflows the parser's nesting stack.
3423    pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3424    pub where_: Option<Expr>,
3425    /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3426    /// mutate the first `limit` rows in the given order. PG has no such
3427    /// clause; the parser accepts it only under the MySQL dialect. Boxed
3428    /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3429    /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3430    /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3431    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3432    /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3433    /// clause (legacy CommandComplete path). Some = engine
3434    /// evaluates the projection over each mutated row and
3435    /// streams the result as a Rows QueryResult.
3436    pub returning: Option<Vec<SelectItem>>,
3437}
3438
3439/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3440/// from the active catalog and prunes them from every index.
3441#[derive(Debug, Clone, PartialEq)]
3442pub struct DeleteStatement {
3443    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3444    /// level DELETE. Empty for a plain DELETE.
3445    pub ctes: Vec<Cte>,
3446    pub table: String,
3447    /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3448    /// to `t`'s own rows and not to anything that descends from it.
3449    ///
3450    /// Round 644 taught the FROM clause the keyword and left DML behind
3451    /// because it needed a field here, and this struct carries a warning
3452    /// that round 413 measured widening it in place overflowing the
3453    /// parser's nesting stack. That warning was about `from_sources`, a
3454    /// struct wide enough to need boxing; a `bool` lands in the padding
3455    /// already present — same as `CreateTableStatement::temporary`.
3456    ///
3457    /// It also earns its keep beyond the spelling: the inheritance
3458    /// fan-out needs a way to say "the parent's own rows" as a
3459    /// statement, or running one on the parent recurses forever.
3460    pub only: bool,
3461    /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3462    /// the WHERE / RETURNING expressions refer to the target row by.
3463    pub alias: Option<String>,
3464    pub where_: Option<Expr>,
3465    /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3466    /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3467    /// form (round 413), so it shares that payload — and it is boxed for
3468    /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3469    /// statement tipped the parser's 512 KiB nesting stack.
3470    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3471    /// v7.9.4 — `RETURNING <projection>`.
3472    pub returning: Option<Vec<SelectItem>>,
3473}
3474
3475/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3476/// One WHEN clause fires per source row depending on whether the
3477/// `on` condition matched any target row(s); the executor walks
3478/// `clauses` in declaration order and fires the first whose
3479/// `matched` kind and optional `condition` are both satisfied.
3480#[derive(Debug, Clone, PartialEq)]
3481pub struct MergeStatement {
3482    /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3483    /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3484    /// in PG). Each CTE materialises before the merge runs and its alias
3485    /// resolves as a source relation.
3486    pub ctes: Vec<Cte>,
3487    pub target: String,
3488    pub target_alias: Option<String>,
3489    pub source: String,
3490    pub source_alias: Option<String>,
3491    /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3492    /// the engine materialises this SELECT for the source rows and `source`
3493    /// is empty; the alias (required by PG for a subquery source) is in
3494    /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3495    pub source_select: Option<Box<SelectStatement>>,
3496    /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3497    /// positional column-alias list after the source alias. Empty when
3498    /// the statement carries none; the engine renames the materialised
3499    /// source columns positionally (PG's rule).
3500    pub source_column_aliases: Vec<String>,
3501    pub on: Expr,
3502    pub clauses: Vec<MergeWhenClause>,
3503    /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3504    /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3505    /// target/source aliases. `None` = no RETURNING (the common form).
3506    pub returning: Option<Vec<SelectItem>>,
3507}
3508
3509#[derive(Debug, Clone, PartialEq)]
3510pub struct MergeWhenClause {
3511    pub matched: MergeMatched,
3512    /// Optional `AND <expr>` filter — when present, the clause
3513    /// only fires for the source rows whose match-pair satisfies
3514    /// the predicate.
3515    pub condition: Option<Expr>,
3516    pub action: MergeAction,
3517}
3518
3519#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3520pub enum MergeMatched {
3521    Matched,
3522    /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3523    /// target row (the classic insert branch).
3524    NotMatched,
3525    /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3526    /// row no source row matches. Actions are UPDATE / DELETE / DO
3527    /// NOTHING only (INSERT is a syntax error, as in PG).
3528    NotMatchedBySource,
3529}
3530
3531#[derive(Debug, Clone, PartialEq)]
3532pub enum MergeAction {
3533    /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3534    /// explicit column list (the bare `INSERT VALUES (vals)`
3535    /// shape lands later).
3536    Insert {
3537        columns: Vec<String>,
3538        values: Vec<Expr>,
3539    },
3540    /// `UPDATE SET col = expr [, …]` — applied to every matched
3541    /// target row for the firing source row.
3542    Update { assignments: Vec<(String, Expr)> },
3543    /// `DELETE` — drop every matched target row.
3544    Delete,
3545    /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3546    /// the clause and SPG mirrors so a customer-side MERGE that
3547    /// uses it for branch-control doesn't error).
3548    DoNothing,
3549}
3550
3551#[derive(Debug, Clone, PartialEq)]
3552pub struct InsertStatement {
3553    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3554    /// level INSERT (writable CTE outer body). Empty for a plain
3555    /// INSERT. PG semantics: each CTE materialises before the
3556    /// outer INSERT runs, sharing the same transaction.
3557    pub ctes: Vec<Cte>,
3558    pub table: String,
3559    /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3560    /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3561    /// row by. PG requires the AS keyword in this position.
3562    pub alias: Option<String>,
3563    /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3564    /// `None`, every tuple is positional and must match the table arity.
3565    /// When `Some`, the engine maps each tuple slot to the named column and
3566    /// fills the rest with NULL (must be nullable).
3567    pub columns: Option<Vec<String>>,
3568    /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3569    /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3570    /// `select_source` is `Some` (the engine builds rows from the
3571    /// inner SELECT result set instead).
3572    pub rows: Vec<Vec<Expr>>,
3573    /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3574    /// round-5 G4). When present, `rows` is empty and the engine
3575    /// materialises the SELECT result, coerces each output tuple to
3576    /// the target column types, and inserts as a single batch.
3577    pub select_source: Option<Box<SelectStatement>>,
3578    /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3579    /// upsert clause. None = legacy INSERT (conflict raises a
3580    /// DuplicateKey error). mailrs migration blocker #2.
3581    pub on_conflict: Option<OnConflictClause>,
3582    /// v7.9.4 — `RETURNING <projection>`.
3583    pub returning: Option<Vec<SelectItem>>,
3584    /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3585    /// between the column list and VALUES. Governs how explicitly-supplied
3586    /// values interact with `GENERATED … AS IDENTITY` columns:
3587    ///   * `None` — default. A `GENERATED ALWAYS` identity column rejects
3588    ///     an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3589    ///   * `System` — override the ALWAYS restriction: the explicit value
3590    ///     is used verbatim, as for a `BY DEFAULT` column.
3591    ///   * `User` — ignore any explicit value on a `BY DEFAULT` identity
3592    ///     column and generate from the sequence instead (no effect on
3593    ///     non-identity columns).
3594    pub overriding: Overriding,
3595    /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3596    /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3597    /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3598    /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3599    /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3600    /// into a NOT NULL column becomes the type's default), and the engine
3601    /// cannot recover that intent from the conflict clause alone. A plain
3602    /// `bool` lands in this struct's existing padding, so the AST does not
3603    /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3604    pub mysql_ignore: bool,
3605}
3606
3607/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3608#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3609pub enum Overriding {
3610    /// No `OVERRIDING` clause.
3611    #[default]
3612    None,
3613    /// `OVERRIDING SYSTEM VALUE`.
3614    System,
3615    /// `OVERRIDING USER VALUE`.
3616    User,
3617}
3618
3619/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3620#[derive(Debug, Clone, PartialEq)]
3621pub struct OnConflictClause {
3622    /// Local columns that identify the conflict (must match a
3623    /// UNIQUE / PRIMARY KEY index on the target table). Empty
3624    /// list means the user wrote `ON CONFLICT DO …` without a
3625    /// target — the engine arbitrates on every unique constraint
3626    /// (round 240).
3627    pub target_columns: Vec<String>,
3628    /// v7.39 (round 240) — the index predicate after the target list
3629    /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3630    /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3631    /// which satisfy any predicate, so it is parsed and carried but not
3632    /// consulted (recorded residual: partial-unique-index arbiters).
3633    pub index_where: Option<Expr>,
3634    /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3635    /// <name>`: the pg_dump conflict-target form. The engine
3636    /// resolves the name to the constraint's columns.
3637    pub constraint_name: Option<String>,
3638    /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3639    /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3640    /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3641    /// `ON CONFLICT DO UPDATE` is refused (42601).
3642    pub mysql_lowered: bool,
3643    /// The action on conflict.
3644    pub action: OnConflictAction,
3645}
3646
3647/// v7.9.7 — action on conflict.
3648#[derive(Debug, Clone, PartialEq)]
3649pub enum OnConflictAction {
3650    /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3651    /// silently skips conflicting ones.
3652    Nothing,
3653    /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3654    /// may reference `EXCLUDED.col` to read the incoming row's
3655    /// value (engine wires `EXCLUDED` as a virtual table).
3656    Update {
3657        assignments: Vec<(String, Expr)>,
3658        where_: Option<Expr>,
3659    },
3660}
3661
3662/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3663///
3664/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3665/// policies are spelled again here and mapped at the engine boundary.
3666/// v7.39 — the modes `BEGIN` / `START TRANSACTION` / `SET TRANSACTION`
3667/// / `SET SESSION CHARACTERISTICS` accept. `read_only` used to be parsed
3668/// and dropped on the floor, so `BEGIN READ ONLY` opened an ordinary
3669/// read-write transaction and every write in it was accepted.
3670///
3671/// `None` on either field means the statement did not name that mode, so
3672/// the session default applies — which is not the same as naming the
3673/// default explicitly.
3674#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3675pub struct TransactionModes {
3676    pub isolation: Option<IsolationLevel>,
3677    /// `Some(true)` = READ ONLY, `Some(false)` = READ WRITE.
3678    pub read_only: Option<bool>,
3679}
3680
3681/// v7.39 — what a read-only transaction refuses, and what PG calls it.
3682///
3683/// SPG did not enforce read-only transactions at all: `BEGIN READ ONLY;
3684/// INSERT …` answered `INSERT 0 1` and committed, and
3685/// `default_transaction_read_only = on` changed nothing. Both GUCs were
3686/// in the inventory, so a session could set one, read it back, and be
3687/// told it held a guarantee nothing was enforcing. Applications open
3688/// read-only transactions as a SAFETY measure — a reporting connection,
3689/// a read-only leg in a pool, a "this path must not write" discipline —
3690/// so accepting the writes is the worst possible answer.
3691///
3692/// `Some(tag)` means refuse with PG's message, `cannot execute {tag} in
3693/// a read-only transaction` (SQLSTATE 25006). Every tag below was read
3694/// back from PostgreSQL 18.6 by running the statement inside
3695/// `BEGIN READ ONLY`, one statement per transaction so no error could be
3696/// attributed to the wrong line.
3697///
3698/// Several answers were not what one would guess, which is why they were
3699/// measured rather than reasoned:
3700///
3701///   * `CREATE TEMP TABLE` is REFUSED, tagged `CREATE TABLE`.
3702///   * `NOTIFY`, `LISTEN` and `REINDEX` are ALLOWED.
3703///   * `PREPARE` of an INSERT is ALLOWED — only the EXECUTE writes.
3704///   * `UPDATE … WHERE false`, which changes nothing, is still REFUSED:
3705///     the verb decides, not the row count.
3706///   * `GRANT`, `COMMENT ON` and `SELECT … FOR SHARE` are all REFUSED.
3707///
3708/// The match is exhaustive on purpose. A new statement cannot be added
3709/// without deciding here whether it writes, which is the failure this
3710/// repository keeps meeting: one member of a family gets handled and its
3711/// siblings quietly do not.
3712impl Statement {
3713    #[must_use]
3714    pub fn read_only_violation_tag(&self) -> Option<&'static str> {
3715        match self {
3716            // v7.39.9 — MySQL's RENAME TABLE is DDL, refused read-only
3717            // for the same reason ALTER TABLE … RENAME TO is.
3718            Self::RenameTables(_) => Some("RENAME TABLE"),
3719            // ---- writes rows -------------------------------------------
3720            Self::Insert { .. } => Some("INSERT"),
3721            Self::Update { .. } => Some("UPDATE"),
3722            Self::Delete { .. } => Some("DELETE"),
3723            Self::Merge { .. } => Some("MERGE"),
3724            Self::Truncate { .. } => Some("TRUNCATE TABLE"),
3725            Self::CopyFromFile { .. } => Some("COPY FROM"),
3726
3727            // A SELECT that takes row locks writes lock state, and PG
3728            // names the strength it was asked for.
3729            Self::Select(sel) => sel.locking.as_ref().map(|l| match l.strength {
3730                LockStrength::Update => "SELECT FOR UPDATE",
3731                LockStrength::NoKeyUpdate => "SELECT FOR NO KEY UPDATE",
3732                LockStrength::Share => "SELECT FOR SHARE",
3733                LockStrength::KeyShare => "SELECT FOR KEY SHARE",
3734            }),
3735
3736            // ---- changes the catalog -----------------------------------
3737            Self::CreateTable { .. } => Some("CREATE TABLE"),
3738            Self::DropTable { .. } => Some("DROP TABLE"),
3739            Self::AlterTable { .. } => Some("ALTER TABLE"),
3740            Self::CreateIndex { .. } => Some("CREATE INDEX"),
3741            Self::DropIndex { .. } => Some("DROP INDEX"),
3742            Self::AlterIndex { .. } => Some("ALTER INDEX"),
3743            Self::CreateView { .. } => Some("CREATE VIEW"),
3744            Self::DropView { .. } => Some("DROP VIEW"),
3745            Self::CreateMaterializedView { .. } => Some("CREATE MATERIALIZED VIEW"),
3746            Self::RefreshMaterializedView { .. } => Some("REFRESH MATERIALIZED VIEW"),
3747            Self::DropMaterializedView { .. } => Some("DROP MATERIALIZED VIEW"),
3748            Self::CreateSequence { .. } => Some("CREATE SEQUENCE"),
3749            Self::AlterSequence { .. } => Some("ALTER SEQUENCE"),
3750            Self::DropSequence { .. } => Some("DROP SEQUENCE"),
3751            Self::CreateType { .. } => Some("CREATE TYPE"),
3752            Self::DropType { .. } => Some("DROP TYPE"),
3753            Self::AlterTypeAddValue { .. } | Self::AlterTypeRenameValue { .. } => {
3754                Some("ALTER TYPE")
3755            }
3756            Self::CreateDomain { .. } => Some("CREATE DOMAIN"),
3757            Self::AlterDomain { .. } => Some("ALTER DOMAIN"),
3758            Self::DropDomain { .. } => Some("DROP DOMAIN"),
3759            Self::CreateSchema { .. } => Some("CREATE SCHEMA"),
3760            Self::DropSchema { .. } => Some("DROP SCHEMA"),
3761            Self::CreateFunction { .. } => Some("CREATE FUNCTION"),
3762            Self::DropFunction { .. } => Some("DROP FUNCTION"),
3763            Self::CreateTrigger { .. } => Some("CREATE TRIGGER"),
3764            Self::DropTrigger { .. } => Some("DROP TRIGGER"),
3765            Self::CreateRule { .. } => Some("CREATE RULE"),
3766            Self::DropRule { .. } => Some("DROP RULE"),
3767            Self::CreateExtension { .. } => Some("CREATE EXTENSION"),
3768            Self::CreateStatistics { .. } => Some("CREATE STATISTICS"),
3769            Self::DropStatistics { .. } => Some("DROP STATISTICS"),
3770            Self::DropAggregate { .. } => Some("DROP AGGREGATE"),
3771            Self::CommentOn { .. } => Some("COMMENT"),
3772            Self::DropDatabase { .. } => Some("DROP DATABASE"),
3773            Self::CreatePublication { .. } => Some("CREATE PUBLICATION"),
3774            Self::DropPublication { .. } => Some("DROP PUBLICATION"),
3775            Self::CreateSubscription { .. } => Some("CREATE SUBSCRIPTION"),
3776            Self::DropSubscription { .. } => Some("DROP SUBSCRIPTION"),
3777
3778            // ---- changes roles / permissions ---------------------------
3779            Self::CreateUser { .. } => Some("CREATE ROLE"),
3780            Self::DropUser { .. } => Some("DROP ROLE"),
3781            Self::AlterRolePassword { .. } | Self::SetDbRoleSetting { .. } => Some("ALTER ROLE"),
3782            Self::Grant { .. } => Some("GRANT"),
3783            Self::Revoke { .. } => Some("REVOKE"),
3784            Self::CreatePolicy { .. } => Some("CREATE POLICY"),
3785            Self::AlterPolicy { .. } => Some("ALTER POLICY"),
3786            Self::DropPolicy { .. } => Some("DROP POLICY"),
3787            Self::AlterSystem { .. } => Some("ALTER SYSTEM"),
3788
3789            // ---- SPG's own writers -------------------------------------
3790            // Rewrites cold-tier segments on disk. PG has no equivalent to
3791            // ask, so the test is what it does, not what it is called.
3792            Self::CompactColdSegments => Some("COMPACT COLD SEGMENTS"),
3793
3794            // ---- allowed -----------------------------------------------
3795            // Reads, transaction control, session state, cursors, and the
3796            // maintenance statements PG itself permits. `REINDEX` really is
3797            // allowed in a read-only transaction (measured), which is why
3798            // `Maintain` is here.
3799            //
3800            // `Prepare`, `Execute`, `Call` and `DoBlock` are allowed at
3801            // this level for the reason PG allows them: the write inside
3802            // is refused when it runs, by this same check. Measured:
3803            // `PREPARE p AS INSERT …` succeeds; `DO $$ … INSERT … $$`
3804            // fails with `cannot execute INSERT`.
3805            Self::Explain { .. }
3806            | Self::CopyTo { .. }
3807            | Self::CopyToFile { .. }
3808            | Self::Analyze { .. }
3809            | Self::Maintain { .. }
3810            | Self::Vacuum { .. }
3811            | Self::Begin { .. }
3812            | Self::Commit
3813            | Self::Rollback
3814            | Self::Savepoint { .. }
3815            | Self::RollbackToSavepoint { .. }
3816            | Self::ReleaseSavepoint { .. }
3817            | Self::PrepareTransaction { .. }
3818            | Self::SetTransaction { .. }
3819            | Self::SetConstraints { .. }
3820            | Self::SetParameter { .. }
3821            | Self::SetParameterList { .. }
3822            | Self::SetUserVars { .. }
3823            | Self::SetRole { .. }
3824            | Self::ResetParameter { .. }
3825            | Self::ShowParameter { .. }
3826            | Self::Discard { .. }
3827            | Self::Prepare { .. }
3828            | Self::Execute { .. }
3829            | Self::Deallocate { .. }
3830            | Self::Call { .. }
3831            | Self::DoBlock { .. }
3832            | Self::DeclareCursor { .. }
3833            | Self::FetchCursor { .. }
3834            | Self::MoveCursor { .. }
3835            | Self::CloseCursor { .. }
3836            | Self::Listen { .. }
3837            | Self::Notify { .. }
3838            | Self::Unlisten { .. }
3839            | Self::Kill { .. }
3840            | Self::WaitForWalPosition { .. }
3841            | Self::ValidateOnly { .. }
3842            | Self::NoOpPreventedInTransaction { .. }
3843            | Self::Empty
3844            | Self::ShowTables
3845            | Self::ShowDatabases
3846            | Self::UseDatabase(_)
3847            | Self::ShowCreateTable { .. }
3848            | Self::ShowIndexes { .. }
3849            | Self::ShowStatus
3850            | Self::ShowVariables
3851            | Self::ShowVariablesLike { .. }
3852            | Self::ShowProcesslist
3853            | Self::ShowColumns { .. }
3854            | Self::ShowUsers
3855            | Self::ShowPublications
3856            | Self::ShowSubscriptions => None,
3857        }
3858    }
3859}
3860
3861#[derive(Debug, Clone, PartialEq, Eq)]
3862pub struct LockingClause {
3863    pub strength: LockStrength,
3864    /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3865    pub of_tables: Vec<String>,
3866    pub policy: LockWait,
3867}
3868
3869/// PG's four tuple-lock strengths, weakest first.
3870#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3871pub enum LockStrength {
3872    KeyShare,
3873    Share,
3874    NoKeyUpdate,
3875    Update,
3876}
3877
3878/// What to do when the row is already locked.
3879#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3880pub enum LockWait {
3881    /// Block until it is free — PG's default.
3882    #[default]
3883    Wait,
3884    /// `NOWAIT` — fail the statement with 55P03.
3885    NoWait,
3886    /// `SKIP LOCKED` — leave the row out of the result.
3887    SkipLocked,
3888}
3889
3890#[derive(Debug, Clone, PartialEq, Default)]
3891pub struct SelectStatement {
3892    /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3893    /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3894    /// whole syntax and locked nothing: two workers running the classic
3895    /// `SKIP LOCKED` queue take both took the same row.
3896    /// v7.39 (round 305) — boxed. A locking clause appears on a
3897    /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3898    /// cost every `SelectStatement` 32 bytes, and this struct sits in
3899    /// recursive evaluation frames where the engine already runs close to
3900    /// its stack budget (a 512 KB depth guard is the canary).
3901    pub locking: Option<alloc::boxed::Box<LockingClause>>,
3902    /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3903    /// expressions, materialised once at query start before the
3904    /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3905    /// only — no `WITH RECURSIVE` for v4.x.
3906    pub ctes: Vec<Cte>,
3907    pub distinct: bool,
3908    /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3909    /// keep the first row (per ORDER BY) of each group the
3910    /// expressions define. Empty = no DISTINCT ON.
3911    pub distinct_on: Vec<Expr>,
3912    pub items: Vec<SelectItem>,
3913    pub from: Option<FromClause>,
3914    pub where_: Option<Expr>,
3915    pub group_by: Option<Vec<Expr>>,
3916    /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3917    /// expands `group_by` to every non-aggregate SELECT-list item
3918    /// before the executor runs. Mutually exclusive with an
3919    /// explicit `group_by` list (the parser sets exactly one).
3920    pub group_by_all: bool,
3921    /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3922    /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3923    /// aggregate executor resolves them through the same synthetic
3924    /// schema used for the SELECT items.
3925    pub having: Option<Expr>,
3926    /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3927    /// itself a `SelectStatement` with `order_by = None` and `limit =
3928    /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3929    /// top of the chain).
3930    pub unions: Vec<(UnionKind, SelectStatement)>,
3931    /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3932    /// Keys are matched left-to-right: first key decides, ties break
3933    /// to the second, etc.
3934    pub order_by: Vec<OrderBy>,
3935    /// `LIMIT <n>` — bound on row output. `n` is an integer
3936    /// literal **or** (v7.9.24) a placeholder `$N` resolved
3937    /// against the prepared-statement Bind values. mailrs
3938    /// migration follow-up H2.
3939    pub limit: Option<LimitExpr>,
3940    /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3941    /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3942    pub offset: Option<LimitExpr>,
3943    /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3944    /// (SQL:2008). When true and an ORDER BY is present, the
3945    /// executor extends past the LIMIT-truncated tail to include
3946    /// every row whose ORDER BY key equals the last-kept row's
3947    /// key. Requires an ORDER BY; the executor errors otherwise
3948    /// (matching PG's `WITH TIES` rule). The parser was already
3949    /// accepting `WITH TIES` since Phase 5.1; this field captures
3950    /// the choice so the executor can act on it.
3951    pub limit_with_ties: bool,
3952    /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3953    /// that NOTHING referenced. PG analyses every definition whether
3954    /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3955    /// and silently succeeded here — the referenced ones get their columns
3956    /// resolved through the WindowFunction nodes they were inlined into,
3957    /// and the unreferenced ones used to be dropped at parse, unexamined.
3958    /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3959    ///
3960    /// Not part of `Display`: an unreferenced definition has no effect on
3961    /// the result, so a deparsed body (a stored view) omits it.
3962    pub window_check_exprs: Vec<Expr>,
3963}
3964
3965impl Expr {
3966    /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3967    /// directly inside this expression to `f`. `f` receives each nested
3968    /// statement once; descending further (into that statement's own
3969    /// clauses) is the caller's job, which keeps this walk finite and
3970    /// lets the caller order the recursion.
3971    ///
3972    /// The match is deliberately **wildcard-free**: a new `Expr` variant
3973    /// does not compile until it says whether it can carry a subquery.
3974    /// The row-count resolution pass is built on this, and a shape it
3975    /// silently failed to visit would leave a `LimitExpr::Expr` behind —
3976    /// which every row-count reader would take as "no limit", i.e. the
3977    /// whole table. Compile-time exhaustiveness is what rules that out.
3978    /// Iterative on purpose. Expression trees here get deep (long
3979    /// boolean chains, big IN lists), and this walk is on the path of
3980    /// every statement; recursing would add a frame per node to a stack
3981    /// budget the engine already runs close to — a depth guard that runs
3982    /// on a deliberately small stack caught exactly that. Depth costs
3983    /// heap here instead.
3984    pub fn for_each_subquery_mut<E>(
3985        &mut self,
3986        f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
3987    ) -> Result<(), E> {
3988        let mut stack: Vec<&mut Self> = alloc::vec![self];
3989        while let Some(e) = stack.pop() {
3990            match e {
3991                Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
3992                Self::NamedArg { expr, .. }
3993                | Self::Collate { expr, .. }
3994                | Self::Variadic(expr)
3995                | Self::Unary { expr, .. }
3996                | Self::Cast { expr, .. }
3997                | Self::FieldAccess { base: expr, .. }
3998                | Self::IsNull { expr, .. }
3999                | Self::BoolTest { expr, .. }
4000                | Self::Extract { source: expr, .. } => stack.push(expr),
4001                Self::Binary { lhs, rhs, .. } => {
4002                    stack.push(lhs);
4003                    stack.push(rhs);
4004                }
4005                Self::Like { expr, pattern, .. } => {
4006                    stack.push(expr);
4007                    stack.push(pattern);
4008                }
4009                Self::ArraySubscript { target, index } => {
4010                    stack.push(target);
4011                    stack.push(index);
4012                }
4013                Self::ArraySlice { target, lo, hi } => {
4014                    stack.push(target);
4015                    stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
4016                }
4017                Self::AnyAll { expr, array, .. } => {
4018                    stack.push(expr);
4019                    stack.push(array);
4020                }
4021                Self::FunctionCall { args, .. } | Self::Array(args) => {
4022                    stack.extend(args.iter_mut());
4023                }
4024                Self::AggregateOrdered {
4025                    call,
4026                    order_by,
4027                    filter,
4028                    ..
4029                } => {
4030                    stack.push(call);
4031                    stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
4032                    stack.extend(filter.iter_mut().map(|b| &mut **b));
4033                }
4034                Self::WindowFunction {
4035                    args,
4036                    partition_by,
4037                    order_by,
4038                    filter,
4039                    ..
4040                } => {
4041                    // `frame` bounds hold folded numbers / interval
4042                    // parts, never expressions — nothing to visit there.
4043                    stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
4044                    stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
4045                    stack.extend(filter.iter_mut().map(|b| &mut **b));
4046                }
4047                Self::InList { expr, list, .. } => {
4048                    stack.push(expr);
4049                    stack.extend(list.iter_mut());
4050                }
4051                Self::Case {
4052                    operand,
4053                    branches,
4054                    else_branch,
4055                } => {
4056                    stack.extend(
4057                        operand
4058                            .iter_mut()
4059                            .chain(else_branch.iter_mut())
4060                            .map(|b| &mut **b),
4061                    );
4062                    for (when, then) in branches.iter_mut() {
4063                        stack.push(when);
4064                        stack.push(then);
4065                    }
4066                }
4067                Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
4068                Self::InSubquery { expr, subquery, .. } => {
4069                    stack.push(expr);
4070                    f(subquery)?;
4071                }
4072                Self::RowInSubquery { row, subquery, .. }
4073                | Self::RowCmpSubquery { row, subquery, .. } => {
4074                    stack.extend(row.iter_mut());
4075                    f(subquery)?;
4076                }
4077            }
4078        }
4079        Ok(())
4080    }
4081
4082    /// The shared twin of [`Self::for_each_subquery_mut`], for analysis
4083    /// that reads a statement rather than rewriting it.
4084    ///
4085    /// Same wildcard-free match, same iterative walk. Rust cannot write
4086    /// one body generic over `&`/`&mut`, so the two must be edited
4087    /// together; exhaustiveness is what makes that a compile error
4088    /// rather than a silent gap in one of them.
4089    ///
4090    /// # Errors
4091    /// Whatever `f` returns.
4092    pub fn for_each_subquery<'a, E>(
4093        &'a self,
4094        f: &mut impl FnMut(&'a SelectStatement) -> Result<(), E>,
4095    ) -> Result<(), E> {
4096        let mut stack: Vec<&'a Self> = alloc::vec![self];
4097        while let Some(e) = stack.pop() {
4098            match e {
4099                Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
4100                Self::NamedArg { expr, .. }
4101                | Self::Collate { expr, .. }
4102                | Self::Variadic(expr)
4103                | Self::Unary { expr, .. }
4104                | Self::Cast { expr, .. }
4105                | Self::FieldAccess { base: expr, .. }
4106                | Self::IsNull { expr, .. }
4107                | Self::BoolTest { expr, .. }
4108                | Self::Extract { source: expr, .. } => stack.push(expr),
4109                Self::Binary { lhs, rhs, .. } => {
4110                    stack.push(lhs);
4111                    stack.push(rhs);
4112                }
4113                Self::Like { expr, pattern, .. } => {
4114                    stack.push(expr);
4115                    stack.push(pattern);
4116                }
4117                Self::ArraySubscript { target, index } => {
4118                    stack.push(target);
4119                    stack.push(index);
4120                }
4121                Self::ArraySlice { target, lo, hi } => {
4122                    stack.push(target);
4123                    stack.extend(lo.iter().chain(hi.iter()).map(|b| &**b));
4124                }
4125                Self::AnyAll { expr, array, .. } => {
4126                    stack.push(expr);
4127                    stack.push(array);
4128                }
4129                Self::FunctionCall { args, .. } | Self::Array(args) => {
4130                    stack.extend(args.iter());
4131                }
4132                Self::AggregateOrdered {
4133                    call,
4134                    order_by,
4135                    filter,
4136                    ..
4137                } => {
4138                    stack.push(call);
4139                    stack.extend(order_by.iter().map(|o| &o.expr));
4140                    stack.extend(filter.iter().map(|b| &**b));
4141                }
4142                Self::WindowFunction {
4143                    args,
4144                    partition_by,
4145                    order_by,
4146                    filter,
4147                    ..
4148                } => {
4149                    stack.extend(args.iter().chain(partition_by.iter()));
4150                    stack.extend(order_by.iter().map(|(e, _, _)| e));
4151                    stack.extend(filter.iter().map(|b| &**b));
4152                }
4153                Self::InList { expr, list, .. } => {
4154                    stack.push(expr);
4155                    stack.extend(list.iter());
4156                }
4157                Self::Case {
4158                    operand,
4159                    branches,
4160                    else_branch,
4161                } => {
4162                    stack.extend(operand.iter().chain(else_branch.iter()).map(|b| &**b));
4163                    for (when, then) in branches {
4164                        stack.push(when);
4165                        stack.push(then);
4166                    }
4167                }
4168                Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
4169                Self::InSubquery { expr, subquery, .. } => {
4170                    stack.push(expr);
4171                    f(subquery)?;
4172                }
4173                Self::RowInSubquery { row, subquery, .. }
4174                | Self::RowCmpSubquery { row, subquery, .. } => {
4175                    stack.extend(row.iter());
4176                    f(subquery)?;
4177                }
4178            }
4179        }
4180        Ok(())
4181    }
4182}
4183
4184/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
4185/// time or a placeholder `$N` resolved during extended-query
4186/// Bind. mailrs migration follow-up H2.
4187///
4188/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
4189/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
4190/// made the compiler point at every site that used to duplicate a
4191/// row-count out of the AST, which is exactly the set that must not
4192/// bypass the resolution pre-pass.
4193#[derive(Debug, Clone, PartialEq)]
4194pub enum LimitExpr {
4195    /// `LIMIT 10` — value known at parse time.
4196    Literal(u32),
4197    /// `LIMIT $N` — the 1-based parameter index, resolved against
4198    /// the bind values when the prepared statement executes.
4199    Placeholder(u16),
4200    /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
4201    /// greatest(2,3)`: a row-count expression that isn't constant, so
4202    /// it can't be folded at parse time. Evaluated once, before
4203    /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
4204    /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
4205    /// "no limit"). **No execution path may see this variant** —
4206    /// `as_literal` would report `None`, which every row-count reader
4207    /// takes to mean "unlimited", i.e. the whole table.
4208    Expr(alloc::boxed::Box<Expr>),
4209}
4210
4211impl fmt::Display for LimitExpr {
4212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4213        match self {
4214            Self::Literal(n) => write!(f, "{n}"),
4215            Self::Placeholder(n) => write!(f, "${n}"),
4216            // Parenthesised so the round-trip text re-parses as one
4217            // row-count expression (`LIMIT (SELECT 4)`), which is also
4218            // the only spelling `FETCH FIRST` accepts.
4219            Self::Expr(e) => write!(f, "({e})"),
4220        }
4221    }
4222}
4223
4224impl LimitExpr {
4225    /// Convenience for the simple-query path where no placeholders
4226    /// can possibly exist. Returns the literal value or `None` if
4227    /// this is a placeholder (caller must surface as Unsupported).
4228    ///
4229    /// v7.39 (round 305) — `None` is read by every row-count consumer as
4230    /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
4231    /// therefore silently return the whole table, so the engine's
4232    /// `resolve_limit_exprs` pre-pass rewrites the variant away before
4233    /// dispatch. The assertion makes a missed nesting site fail loudly
4234    /// in every test build rather than quietly widening a result set.
4235    #[must_use]
4236    pub fn as_literal(&self) -> Option<u32> {
4237        match self {
4238            Self::Literal(n) => Some(*n),
4239            Self::Placeholder(_) => None,
4240            Self::Expr(_) => {
4241                debug_assert!(
4242                    false,
4243                    "LimitExpr::Expr reached execution — resolve_limit_exprs \
4244                     missed a nesting site; treating it as `no limit` would \
4245                     return every row"
4246                );
4247                None
4248            }
4249        }
4250    }
4251}
4252
4253/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
4254/// the engine's `substitute_placeholders` pass these are
4255/// always Literal; in the simple-query path a Placeholder
4256/// shape returns None (executor surfaces as
4257/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
4258impl SelectStatement {
4259    #[must_use]
4260    pub fn limit_literal(&self) -> Option<u32> {
4261        self.limit.as_ref().and_then(LimitExpr::as_literal)
4262    }
4263    #[must_use]
4264    pub fn offset_literal(&self) -> Option<u32> {
4265        self.offset.as_ref().and_then(LimitExpr::as_literal)
4266    }
4267}
4268
4269#[derive(Debug, Clone, PartialEq)]
4270pub struct Cte {
4271    pub name: String,
4272    /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
4273    /// classical case) or a data-modifying statement
4274    /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
4275    /// CTE semantics. The modifying body's RETURNING projection
4276    /// becomes the materialised CTE table the outer query can
4277    /// reference; the modifying statement runs once before the
4278    /// outer query, within the same transaction.
4279    pub body: CteBody,
4280    /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
4281    /// RECURSIVE keyword. Applies to every CTE in the clause per
4282    /// PG semantics. A non-recursive body in a RECURSIVE WITH is
4283    /// allowed; the engine just runs it once.
4284    pub recursive: bool,
4285    /// v4.22: optional `WITH name(a, b, c)` column-name list. When
4286    /// non-empty, these override the body's output column names
4287    /// position-by-position; the engine errors out if the count
4288    /// doesn't match the body's projection width.
4289    pub column_overrides: Vec<String>,
4290    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
4291    /// SET seqcol` on a recursive CTE. Desugared at parse time into an
4292    /// extra ordering column on the body (see `rewrite_search_and_cycle`).
4293    pub search: Option<SearchClause>,
4294    /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
4295    /// USING pathcol` cycle detection, desugared at parse time.
4296    pub cycle: Option<CycleClause>,
4297}
4298
4299/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
4300#[derive(Debug, Clone, PartialEq)]
4301pub struct SearchClause {
4302    /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
4303    pub depth_first: bool,
4304    /// The CTE output columns the search orders by.
4305    pub by_columns: Vec<String>,
4306    /// The new column holding the ordering key (a row-array for depth,
4307    /// a `(depth, keys…)` row for breadth).
4308    pub set_column: String,
4309}
4310
4311/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
4312#[derive(Debug, Clone, PartialEq)]
4313pub struct CycleClause {
4314    /// Columns whose repetition along a path marks a cycle.
4315    pub columns: Vec<String>,
4316    /// The new boolean-ish column set to `mark_value` on a cycle.
4317    pub mark_column: String,
4318    /// Value written to `mark_column` when a cycle is detected (default
4319    /// `true`); `default_value` otherwise. PG allows any type; SPG carries
4320    /// them as literals.
4321    pub mark_value: Option<Literal>,
4322    pub default_value: Option<Literal>,
4323    /// The new column accumulating the visited-row path array.
4324    pub path_column: String,
4325}
4326
4327/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
4328/// (Insert / Update / Delete with optional RETURNING). The
4329/// data-modifying variants must carry a RETURNING projection for the
4330/// outer query to reference the CTE alias by; an empty RETURNING is
4331/// only valid if no outer reference materialises (rare — typically
4332/// caught at planning).
4333#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
4334#[derive(Debug, Clone, PartialEq)]
4335pub enum CteBody {
4336    Select(SelectStatement),
4337    Insert(Box<InsertStatement>),
4338    Update(Box<UpdateStatement>),
4339    Delete(Box<DeleteStatement>),
4340    /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
4341    /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
4342    Merge(Box<MergeStatement>),
4343}
4344
4345impl CteBody {
4346    /// Convenience accessor used by classical (read-only) CTE
4347    /// callsites that still expect a SELECT body. Returns None for
4348    /// data-modifying CTEs; callers must explicitly route those
4349    /// through `exec_with_ctes`'s modifying branch.
4350    #[must_use]
4351    pub fn as_select(&self) -> Option<&SelectStatement> {
4352        match self {
4353            Self::Select(s) => Some(s),
4354            _ => None,
4355        }
4356    }
4357
4358    #[must_use]
4359    pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
4360        match self {
4361            Self::Select(s) => Some(s),
4362            _ => None,
4363        }
4364    }
4365
4366    #[must_use]
4367    pub fn is_modifying(&self) -> bool {
4368        !matches!(self, Self::Select(_))
4369    }
4370}
4371
4372#[derive(Debug, Clone, PartialEq)]
4373pub struct OrderBy {
4374    pub expr: Expr,
4375    /// `false` = ASC (default), `true` = DESC.
4376    pub desc: bool,
4377    /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
4378    /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
4379    /// NULLS FIRST for DESC); the engine resolves the effective
4380    /// value via `nulls_first.unwrap_or(desc)`.
4381    pub nulls_first: Option<bool>,
4382    /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
4383    /// It lives here rather than in the expression for the same reason
4384    /// `desc` does: at an ORDER BY key a collation is ordering
4385    /// information, and nothing downstream of the sort needs it. A new
4386    /// `Expr` variant would instead put a new arm on `eval_expr`, which
4387    /// this repo has measured to overflow the debug stack.
4388    ///
4389    /// `None` means none was written, and the key falls back to whatever
4390    /// its COLUMN declares — which is every key that existed before this.
4391    pub collation: Option<String>,
4392}
4393
4394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4395pub enum UnionKind {
4396    /// `UNION` — dedupes the combined set.
4397    Distinct,
4398    /// `UNION ALL` — concatenates without dedup.
4399    All,
4400    /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
4401    /// present on both sides.
4402    Intersect,
4403    /// `INTERSECT ALL` — multiset intersection (min per-row count).
4404    IntersectAll,
4405    /// `EXCEPT` — distinct left rows absent from the right.
4406    Except,
4407    /// `EXCEPT ALL` — multiset subtraction.
4408    ExceptAll,
4409}
4410
4411#[derive(Debug, Clone, PartialEq)]
4412pub enum SelectItem {
4413    Wildcard,
4414    /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
4415    /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
4416    /// `NEW` pseudo-relation).
4417    QualifiedWildcard(String),
4418    Expr {
4419        expr: Expr,
4420        alias: Option<String>,
4421    },
4422}
4423
4424#[derive(Debug, Clone, PartialEq)]
4425pub struct TableRef {
4426    pub name: String,
4427    pub alias: Option<String>,
4428    /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
4429    /// children.
4430    ///
4431    /// The keyword used to be absorbed at parse time, on the reasoning
4432    /// that SPG's inheritance children are separate relations a plain
4433    /// scan does not descend into — so ONLY already described what the
4434    /// scan did. That stopped being true when a partition parent
4435    /// started unioning its children: measured, `SELECT count(*) FROM
4436    /// ONLY <partitioned parent>` answered 2 where PG answers 0.
4437    pub only: bool,
4438    /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
4439    /// When `Some(id)`, the scan restricts to rows that live in
4440    /// segment `<id>` only — useful for forensic inspection of a
4441    /// specific freezer-emitted segment without exposing the hot
4442    /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
4443    /// is STABILITY carve-out for v6.10 — needs the freezer to
4444    /// stamp each segment with a wall-clock at creation time.
4445    pub as_of_segment: Option<u32>,
4446    /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
4447    /// source. When `Some`, `name` is the alias (defaulting to
4448    /// `"unnest"` when no `AS` is given) and the engine builds a
4449    /// synthetic single-column table by evaluating the expression
4450    /// once at SELECT entry. Each TEXT[] element becomes one row;
4451    /// NULL elements become NULL cells. v7.11 supported
4452    /// uncorrelated UNNEST only as the FROM primary; v7.13.2
4453    /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
4454    /// position (cross-join with regular tables).
4455    pub unnest_expr: Option<Box<Expr>>,
4456    /// v7.13.2 — mailrs round-6 S5. PG-standard
4457    /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
4458    /// when non-empty, the first entry overrides the projected
4459    /// column name for the unnested column. Empty = fall back to
4460    /// the table alias (pre-v7.13.2 behaviour).
4461    pub unnest_column_aliases: Vec<String>,
4462    /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
4463    /// row-stream gains a trailing BIGINT column counting rows
4464    /// from 1 in element order. PG names it `ordinality`; a second
4465    /// entry in the column-alias list renames it.
4466    pub with_ordinality: bool,
4467    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4468    /// [, step])` set-returning source. When `Some`, the engine
4469    /// materialises a single-column virtual table by stepping
4470    /// `start` to `stop` inclusive. Args are the literal arg list
4471    /// (2 for default-step, 3 for explicit-step). Supports:
4472    ///   * SmallInt / Int / BigInt with integer step (default = 1)
4473    ///   * Timestamp with INTERVAL step (PG date-range pattern)
4474    /// Mutually exclusive with `unnest_expr` — both populate the
4475    /// same downstream dispatch slot. `name` defaults to
4476    /// `"generate_series"` when no alias is provided.
4477    pub generate_series_args: Option<Vec<Expr>>,
4478    /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
4479    /// table. When `Some`, the TableRef is a parenthesised SELECT
4480    /// that may reference columns from the preceding FROM items
4481    /// (correlated derived table). The executor materialises the
4482    /// subquery per left-row, substituting outer-column references
4483    /// against the current join row's values before running the
4484    /// inner SELECT, then cross-joins the result back.
4485    /// Mutually exclusive with `name` / `unnest_expr` /
4486    /// `generate_series_args`.
4487    pub lateral_subquery: Option<Box<SelectStatement>>,
4488    /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
4489    /// function as a FROM item. PG semantics: for each key/value
4490    /// pair in the JSONB object argument, emit one (key TEXT,
4491    /// value TEXT) row. When prefixed by `LATERAL` and joined via
4492    /// `CROSS JOIN LATERAL`, the argument may reference columns
4493    /// from a preceding FROM item, in which case the executor
4494    /// evaluates `<expr>` per outer row.
4495    /// Mutually exclusive with `unnest_expr` / `generate_series_args`
4496    /// / `lateral_subquery`. The optional `LATERAL` keyword does not
4497    /// require a separate flag — the executor evaluates per-row
4498    /// whenever the join sits in a JoinKind context.
4499    ///
4500    /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4501    /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4502    /// `json_each` / `json_each_text`) so the executor picks the
4503    /// value-column rendering (JSON text vs unwrapped text).
4504    pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4505    /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4506    /// function channel: `(lowercase fn name, args)`. Carries
4507    /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4508    /// dispatches by name.
4509    pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4510    /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4511    /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4512    /// reference to it yields the value, not a one-field composite
4513    /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4514    /// desugared shape is indistinguishable from a hand-written
4515    /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4516    /// only the parser knows which one it built, so it says so here.
4517    pub scalar_fn_item: bool,
4518    /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4519    /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4520    /// target-list SRFs follow — see round 67). The array-returning family keeps
4521    /// its own lowering; this channel carries the ones that have no array form
4522    /// (`generate_series`, a user `RETURNS SETOF` function).
4523    pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4524    /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4525    /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4526    /// tables (implicit LATERAL, like every SRF channel). Executed by
4527    /// walking the row path over the parsed doc, then each column's
4528    /// path per row-item; NESTED expands as a per-parent outer join.
4529    pub json_table: Option<Box<JsonTable>>,
4530}
4531
4532/// What a FROM item IS.
4533///
4534/// v7.40.10 — a boolean cannot make a consumer handle a new kind; an
4535/// enum can. Every consumer that must know the difference writes a
4536/// `match` with no wildcard arm, so a variant added here is a compile
4537/// error at each of them rather than a defect at whichever one the new
4538/// shape reaches first.
4539///
4540/// The engine asked "is this item synthesised?" in fifty-six places and
4541/// exactly one of them listed every field. The rest were missing
4542/// between one and five, and the gaps were reachable:
4543/// `SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)` answered
4544/// `relation "jsonb_each_text" does not exist` over the extended
4545/// protocol because one such list named four of seven.
4546#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4547pub enum FromItemKind {
4548    /// A table, view or CTE named in the catalog.
4549    Relation,
4550    /// `unnest(...)` and the set-returning functions the parser lowers
4551    /// onto the same slot (`string_to_table`, `jsonb_object_keys`, …).
4552    Unnest,
4553    /// `generate_series(...)`.
4554    GenerateSeries,
4555    /// A derived table or LATERAL subquery.
4556    Subquery,
4557    /// `jsonb_each(...)` / `jsonb_each_text(...)`.
4558    JsonbEach,
4559    /// A table function call the parser kept as a name plus arguments.
4560    TableFn,
4561    /// `ROWS FROM (...)`.
4562    RowsFrom,
4563    /// `JSON_TABLE(...)`.
4564    JsonTable,
4565    /// A scalar function in FROM position, which yields one row.
4566    ScalarFn,
4567}
4568
4569/// One walkable slot of a FROM item: an expression, or a nested SELECT.
4570///
4571/// v7.40.10 — see [`TableRef::try_for_each_slot_mut`].
4572#[derive(Debug)]
4573pub enum FromSlot<'a> {
4574    Expr(&'a mut Expr),
4575    Select(&'a mut SelectStatement),
4576}
4577
4578/// The shared half of [`FromSlot`].
4579///
4580/// v7.40.11 — analysis reads a statement it must not modify, and Rust
4581/// has no way to write one walk that is generic over `&`/`&mut`. The two
4582/// bodies are the same destructure and must move together; the
4583/// destructures are total, so a new slot fails to compile in both.
4584#[derive(Debug)]
4585pub enum FromSlotRef<'a> {
4586    Expr(&'a Expr),
4587    Select(&'a SelectStatement),
4588}
4589
4590impl TableRef {
4591    /// Whether this FROM item NAMES A RELATION — a table, view or CTE —
4592    /// rather than producing its own rows.
4593    ///
4594    /// **A total destructure, no `..`.** A field added to `TableRef` is a
4595    /// compile error here.
4596    ///
4597    /// v7.40.10, on evidence. This question is asked in 56 places across
4598    /// the engine and every one of them wrote its own list of fields.
4599    /// Exactly one was complete. The others were missing between one and
4600    /// five slots each, and each gap is a defect waiting for the shape
4601    /// that reaches it:
4602    ///
4603    /// ```text
4604    ///   try_stream_single_table's guard named four of seven, so
4605    ///   SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)
4606    ///     ERROR:  relation "jsonb_each_text" does not exist
4607    ///   over the extended protocol, while count(*) over the same item
4608    ///   answered and the simple query protocol answered.
4609    /// ```
4610    ///
4611    /// `scalar_fn_item` counts as not-a-relation for the same reason the
4612    /// rest do: the row comes from the item, not from the catalog.
4613    #[must_use]
4614    pub fn names_a_relation(&self) -> bool {
4615        self.kind() == FromItemKind::Relation
4616    }
4617
4618    /// What this FROM item is.
4619    ///
4620    /// **A total destructure, no `..`.** A field added to `TableRef` is
4621    /// a compile error here — which is the point, because every
4622    /// consumer matches exhaustively on the result.
4623    ///
4624    /// The slots are mutually exclusive by construction: the parser
4625    /// fills exactly one of them, or none for a plain relation.
4626    #[must_use]
4627    pub fn kind(&self) -> FromItemKind {
4628        let Self {
4629            name: _,
4630            alias: _,
4631            only: _,
4632            as_of_segment: _,
4633            unnest_column_aliases: _,
4634            with_ordinality: _,
4635            scalar_fn_item,
4636            unnest_expr,
4637            generate_series_args,
4638            lateral_subquery,
4639            jsonb_each_text_arg,
4640            table_fn_call,
4641            rows_from,
4642            json_table,
4643        } = self;
4644        if unnest_expr.is_some() {
4645            FromItemKind::Unnest
4646        } else if generate_series_args.is_some() {
4647            FromItemKind::GenerateSeries
4648        } else if lateral_subquery.is_some() {
4649            FromItemKind::Subquery
4650        } else if jsonb_each_text_arg.is_some() {
4651            FromItemKind::JsonbEach
4652        } else if table_fn_call.is_some() {
4653            FromItemKind::TableFn
4654        } else if rows_from.is_some() {
4655            FromItemKind::RowsFrom
4656        } else if json_table.is_some() {
4657            FromItemKind::JsonTable
4658        } else if *scalar_fn_item {
4659            FromItemKind::ScalarFn
4660        } else {
4661            FromItemKind::Relation
4662        }
4663    }
4664
4665    /// Every expression this FROM item carries, and the SELECT nested in
4666    /// it — in one place, for every pass that needs them.
4667    ///
4668    /// **Written as a TOTAL destructure, with no `..`.** A field added
4669    /// to `TableRef` is a compile error here, rather than a defect in
4670    /// each pass that enumerated the slots for itself. That is the whole
4671    /// point of the function existing.
4672    ///
4673    /// v7.40.10, on evidence. `TableRef` carries seven expression slots
4674    /// and three separate passes each knew a different subset of them.
4675    /// In one day: the parameter-substitution walk knew only
4676    /// `lateral_subquery`, so `unnest($1)` reached execution still
4677    /// holding a placeholder (a customer's live 500); `describe` knew
4678    /// only `unnest_expr`, so `generate_series(…)` described no columns
4679    /// and a driver got a protocol error; and the LIMIT/OFFSET
4680    /// resolution knew CTEs and UNION peers but not a FROM subquery, so
4681    /// `LIMIT $n` inside a derived table returned every row.
4682    ///
4683    /// Fixing those three one at a time left four slots unvisited.
4684    /// Measured after the third fix shipped, all on the same message:
4685    ///
4686    /// ```text
4687    ///   jsonb_each_text($1)  parameter $1 referenced but only 0 bound
4688    ///   ROWS FROM (…$1…)     parameter $1 referenced but only 0 bound
4689    ///   json_table($1, …)    parameter $1 referenced but only 0 bound
4690    /// ```
4691    ///
4692    /// Those were the next three reports. This is what stops the fourth.
4693    ///
4694    /// # Errors
4695    /// Whatever the callbacks return; the walk stops at the first.
4696    pub fn try_for_each_slot_mut<E>(
4697        &mut self,
4698        visit: &mut dyn FnMut(FromSlot<'_>) -> Result<(), E>,
4699    ) -> Result<(), E> {
4700        // One callback rather than two, so a caller that needs the same
4701        // state for both — every caller so far — does not have to lend
4702        // it twice.
4703        let Self {
4704            // Not expressions — named so the destructure stays total.
4705            name: _,
4706            alias: _,
4707            only: _,
4708            as_of_segment: _,
4709            unnest_column_aliases: _,
4710            with_ordinality: _,
4711            scalar_fn_item: _,
4712            // The seven that carry something to walk.
4713            unnest_expr,
4714            generate_series_args,
4715            lateral_subquery,
4716            jsonb_each_text_arg,
4717            table_fn_call,
4718            rows_from,
4719            json_table,
4720        } = self;
4721        if let Some(e) = unnest_expr {
4722            visit(FromSlot::Expr(e))?;
4723        }
4724        if let Some(args) = generate_series_args {
4725            for a in args.iter_mut() {
4726                visit(FromSlot::Expr(a))?;
4727            }
4728        }
4729        if let Some(sub) = lateral_subquery {
4730            visit(FromSlot::Select(sub))?;
4731        }
4732        if let Some((_, e)) = jsonb_each_text_arg {
4733            visit(FromSlot::Expr(e))?;
4734        }
4735        if let Some(call) = table_fn_call {
4736            for a in call.1.iter_mut() {
4737                visit(FromSlot::Expr(a))?;
4738            }
4739        }
4740        if let Some(items) = rows_from {
4741            for (_, args) in items.iter_mut() {
4742                for a in args.iter_mut() {
4743                    visit(FromSlot::Expr(a))?;
4744                }
4745            }
4746        }
4747        if let Some(jt) = json_table {
4748            let JsonTable {
4749                doc,
4750                row_path: _,
4751                columns: _,
4752                passing,
4753            } = jt.as_mut();
4754            visit(FromSlot::Expr(doc))?;
4755            for (_, e) in passing.iter_mut() {
4756                visit(FromSlot::Expr(e))?;
4757            }
4758        }
4759        Ok(())
4760    }
4761
4762    /// The shared twin of [`Self::try_for_each_slot_mut`], for analysis
4763    /// that reads a statement rather than rewriting it. Same total
4764    /// destructure — a new slot is a compile error in both.
4765    ///
4766    /// # Errors
4767    /// Whatever `visit` returns.
4768    pub fn try_for_each_slot<'a, E>(
4769        &'a self,
4770        visit: &mut dyn FnMut(FromSlotRef<'a>) -> Result<(), E>,
4771    ) -> Result<(), E> {
4772        let Self {
4773            name: _,
4774            alias: _,
4775            only: _,
4776            as_of_segment: _,
4777            unnest_column_aliases: _,
4778            with_ordinality: _,
4779            scalar_fn_item: _,
4780            unnest_expr,
4781            generate_series_args,
4782            lateral_subquery,
4783            jsonb_each_text_arg,
4784            table_fn_call,
4785            rows_from,
4786            json_table,
4787        } = self;
4788        if let Some(e) = unnest_expr {
4789            visit(FromSlotRef::Expr(e))?;
4790        }
4791        if let Some(args) = generate_series_args {
4792            for a in args {
4793                visit(FromSlotRef::Expr(a))?;
4794            }
4795        }
4796        if let Some(sub) = lateral_subquery {
4797            visit(FromSlotRef::Select(sub))?;
4798        }
4799        if let Some((_, e)) = jsonb_each_text_arg {
4800            visit(FromSlotRef::Expr(e))?;
4801        }
4802        if let Some(call) = table_fn_call {
4803            for a in &call.1 {
4804                visit(FromSlotRef::Expr(a))?;
4805            }
4806        }
4807        if let Some(items) = rows_from {
4808            for (_, args) in items {
4809                for a in args {
4810                    visit(FromSlotRef::Expr(a))?;
4811                }
4812            }
4813        }
4814        if let Some(jt) = json_table {
4815            let JsonTable {
4816                doc,
4817                row_path: _,
4818                columns: _,
4819                passing,
4820            } = jt.as_ref();
4821            visit(FromSlotRef::Expr(doc))?;
4822            for (_, e) in passing {
4823                visit(FromSlotRef::Expr(e))?;
4824            }
4825        }
4826        Ok(())
4827    }
4828}
4829
4830/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4831#[derive(Debug, Clone, PartialEq)]
4832pub struct JsonTable {
4833    /// The document expression (jsonb/json/text). May reference outer
4834    /// columns → implicit LATERAL.
4835    pub doc: Box<Expr>,
4836    /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4837    /// match is one row's context item.
4838    pub row_path: String,
4839    /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4840    pub columns: Vec<JsonTableColumn>,
4841    /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4842    pub passing: Vec<(String, Expr)>,
4843}
4844
4845/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4846#[derive(Debug, Clone, PartialEq)]
4847pub enum JsonTableColumn {
4848    /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4849    Ordinality { name: String },
4850    /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4851    /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4852    /// `<name> <type> EXISTS [PATH '<p>']`.
4853    Regular {
4854        name: String,
4855        ty: ColumnTypeName,
4856        /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4857        path: String,
4858        /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4859        exists: bool,
4860        /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4861        format_json: bool,
4862        /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4863        wrapper: bool,
4864        /// Behaviour when the path matches nothing (default NULL).
4865        on_empty: JsonTableOnBehavior,
4866        /// Behaviour when coercion fails (default NULL).
4867        on_error: JsonTableOnBehavior,
4868    },
4869    /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4870    /// row like a LEFT JOIN (a parent with no nested match still emits one
4871    /// row, nested cols NULL).
4872    Nested {
4873        path: String,
4874        columns: Vec<JsonTableColumn>,
4875    },
4876}
4877
4878/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4879#[derive(Debug, Clone, PartialEq)]
4880pub enum JsonTableOnBehavior {
4881    /// Default: the column value is NULL.
4882    Null,
4883    /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4884    Error,
4885    /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4886    Default(Box<Expr>),
4887}
4888
4889/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4890/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4891/// joins evaluate left-associatively in nested-loop order.
4892#[derive(Debug, Clone, PartialEq)]
4893pub struct FromClause {
4894    pub primary: TableRef,
4895    pub joins: Vec<FromJoin>,
4896}
4897
4898#[derive(Debug, Clone, PartialEq)]
4899pub struct FromJoin {
4900    pub kind: JoinKind,
4901    pub table: TableRef,
4902    /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4903    pub on: Option<Expr>,
4904    /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4905    /// USING column list so the executor can perform PG's column-merge
4906    /// (the join columns collapse to a single unqualified output column,
4907    /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4908    /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4909    /// USING into an equivalent `on` predicate so the join filter/count
4910    /// path works unchanged; `using_cols` drives only the output-shape
4911    /// rewrite. Empty/`None` for `ON` and CROSS joins.
4912    pub using_cols: Option<Vec<String>>,
4913    /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4914    /// column names are not known until the table schemas are available
4915    /// (parse time is schema-less), so the parser only sets this flag and
4916    /// leaves `on`/`using_cols` empty; the engine resolves the common
4917    /// columns at execution time, synthesises the `on` predicate + the
4918    /// USING column-merge, and clears the flag. If there are no common
4919    /// columns PG treats it as a CROSS join.
4920    pub natural: bool,
4921}
4922
4923#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4924pub enum JoinKind {
4925    Inner,
4926    Left,
4927    Cross,
4928    /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4929    /// NULL-filling the left (drive) columns on unmatched right rows.
4930    /// The executor runs the LEFT algorithm's mirror: it tracks which
4931    /// peer rows matched and emits the unmatched ones with a NULL-left
4932    /// tuple after the probe loop. Output column order is unchanged
4933    /// (left-table cols then right-table cols).
4934    Right,
4935    /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4936    /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4937    FullOuter,
4938    /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4939    /// once, paired with the first peer row that satisfies the ON. Not
4940    /// reachable from SQL — the EXISTS pull-up emits it, which is what
4941    /// frees positive EXISTS from the round-721 uniqueness gate (an
4942    /// INNER join would multiply the outer rows; a semi join cannot).
4943    Semi,
4944}
4945
4946#[derive(Debug, Clone, PartialEq)]
4947pub enum Expr {
4948    Literal(Literal),
4949    /// v7.39.2 — `<expr> COLLATE <name>`: the collation this expression
4950    /// compares under, whatever the column or the database says.
4951    ///
4952    /// The parser used to refuse the locale names in this position and
4953    /// SILENTLY ABSORB the byte-order ones, so `'a' COLLATE "C" < 'B'`
4954    /// answered `t` where PostgreSQL 18.6 answers `f` — the one family
4955    /// it let through is the one where dropping it changes the answer.
4956    ///
4957    /// Whether dropping is safe depends on the DATABASE's own collation,
4958    /// which the parser cannot see: under `SPG_LC_COLLATE=C` absorbing
4959    /// `COLLATE "C"` is exactly right. So the name rides along and the
4960    /// engine, which knows, decides.
4961    Collate {
4962        expr: Box<Expr>,
4963        collation: String,
4964    },
4965    Column(ColumnName),
4966    /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4967    /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4968    /// callee's declared parameter names, and a user function's live in the
4969    /// catalog — which the parser cannot see. So the name rides along in the
4970    /// tree and the evaluator, which has the catalog, does the reordering.
4971    /// Appears only inside a `FunctionCall`'s argument list.
4972    NamedArg {
4973        name: String,
4974        expr: Box<Expr>,
4975    },
4976    /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
4977    /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
4978    /// expression evaluates to an array whose elements the evaluator splices
4979    /// into the call as individual trailing arguments. Appears only inside a
4980    /// `FunctionCall`'s argument list.
4981    Variadic(Box<Expr>),
4982    /// v6.1.1 — `$N` parameter placeholder for the extended query
4983    /// protocol. The number is 1-based per PostgreSQL convention.
4984    /// Evaluation looks up `params[N-1]` from the prepared-statement
4985    /// bind buffer; out-of-range indices raise a runtime error
4986    /// (same shape as a column-not-found miss).
4987    Placeholder(u16),
4988    Binary {
4989        lhs: Box<Expr>,
4990        op: BinOp,
4991        rhs: Box<Expr>,
4992    },
4993    Unary {
4994        op: UnOp,
4995        expr: Box<Expr>,
4996    },
4997    /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
4998    /// TEXT, BOOL targets; engine coerces at evaluation time.
4999    Cast {
5000        expr: Box<Expr>,
5001        target: CastTarget,
5002    },
5003    /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
5004    /// evaluates to a composite/record value (an explicit `ROW(...)`, a
5005    /// whole-row reference, or a composite-returning function); `field` names
5006    /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
5007    /// column names for a whole-row). Only the parenthesised form reaches
5008    /// here — a bare `a.b` is parsed as a qualified column reference.
5009    FieldAccess {
5010        base: Box<Expr>,
5011        field: String,
5012    },
5013    /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
5014    IsNull {
5015        expr: Box<Expr>,
5016        negated: bool,
5017    },
5018    /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
5019    /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
5020    /// `Some(false)` for FALSE and `None` for UNKNOWN.
5021    ///
5022    /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
5023    /// The semantics were right, but the AST then had no way to say what
5024    /// the user wrote, so every renderer printed the lowering:
5025    /// `CHECK ((a > 1) IS TRUE)` came back as
5026    /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
5027    /// dumped view lost the form too.
5028    BoolTest {
5029        expr: Box<Expr>,
5030        value: Option<bool>,
5031        negated: bool,
5032    },
5033    /// Function call `name(args...)`. v1.4 supports a small built-in set
5034    /// (length, upper, lower, abs, coalesce); unknown names error at eval
5035    /// time so the parser stays open for v1.5 aggregates.
5036    FunctionCall {
5037        name: String,
5038        args: Vec<Expr>,
5039    },
5040    /// v7.24 (mailrs round-16 A) — an aggregate call with an
5041    /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
5042    /// Wraps the plain [`Expr::FunctionCall`] so every existing
5043    /// FunctionCall consumer stays untouched; only the aggregate
5044    /// executor (and the expression walkers) know the wrapper.
5045    /// Non-aggregate evaluation contexts reject it at eval time.
5046    AggregateOrdered {
5047        call: Box<Expr>,
5048        order_by: Vec<OrderBy>,
5049        /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
5050        /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
5051        /// aggregate modifier so plain FunctionCall stays untouched.
5052        distinct: bool,
5053        /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
5054        /// Only the rows where `cond` is true contribute to this
5055        /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
5056        /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
5057        /// END)`, which is faithful for NULL-ignoring aggregates but
5058        /// WRONG for `array_agg` (it would collect a NULL per excluded
5059        /// row). The executor instead skips excluded rows before
5060        /// accumulation, which is correct for every aggregate.
5061        filter: Option<Box<Expr>>,
5062    },
5063    /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
5064    /// wildcards are `%` (any run) and `_` (one char), backslash escapes
5065    /// the next char (so `\%` matches a literal `%`).
5066    Like {
5067        expr: Box<Expr>,
5068        pattern: Box<Expr>,
5069        negated: bool,
5070        /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
5071        /// match. PG folds both operands.
5072        case_insensitive: bool,
5073    },
5074    /// v4.12 window function call: `name(args) OVER (PARTITION BY
5075    /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
5076    /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
5077    /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
5078    /// unordered windows and "from start of partition through
5079    /// current row" for ordered windows — no explicit ROWS /
5080    /// RANGE clause in v4.12 MVP.
5081    WindowFunction {
5082        name: String,
5083        args: Vec<Expr>,
5084        partition_by: Vec<Expr>,
5085        /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
5086        /// (None = PG default, same contract as [`OrderBy`]).
5087        order_by: Vec<(
5088            Expr,
5089            bool,         /* desc */
5090            Option<bool>, /* nulls_first */
5091        )>,
5092        /// v4.20 explicit frame. `None` means "use the default":
5093        /// whole-partition when unordered, running aggregate from
5094        /// partition start through current row when ordered.
5095        frame: Option<WindowFrame>,
5096        /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
5097        /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
5098        /// `Respect` (PG / ANSI default — NULLs participate). Other
5099        /// window functions ignore this flag.
5100        null_treatment: NullTreatment,
5101        /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
5102        /// = no FILTER. Only aggregate window functions honor it; the
5103        /// predicate restricts which peer rows contribute within the frame.
5104        filter: Option<Box<Expr>>,
5105    },
5106    /// v4.10 scalar subquery — `(SELECT ...)` used in expression
5107    /// position. Must return exactly one row × one column at eval
5108    /// time; the engine errors out otherwise. Uncorrelated only —
5109    /// the inner SELECT cannot reference outer columns.
5110    ScalarSubquery(Box<SelectStatement>),
5111    /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
5112    /// projection is ignored; only row-count matters.
5113    Exists {
5114        subquery: Box<SelectStatement>,
5115        negated: bool,
5116    },
5117    /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
5118    /// project exactly one column; membership is tested by Eq
5119    /// against each row's value (NULL handling follows ANSI:
5120    /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
5121    InSubquery {
5122        expr: Box<Expr>,
5123        subquery: Box<SelectStatement>,
5124        negated: bool,
5125    },
5126    /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
5127    /// against a multi-column subquery. Row comparisons against a *list*
5128    /// decompose to OR-of-AND at parse time, but the subquery form can't
5129    /// (its rows are only known at runtime), so this survives as its own
5130    /// node evaluated with PG's row-comparison three-valued logic.
5131    RowInSubquery {
5132        row: Vec<Expr>,
5133        subquery: Box<SelectStatement>,
5134        negated: bool,
5135    },
5136    /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
5137    /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
5138    /// RowInSubquery, the literal-RHS form decomposes at parse time but the
5139    /// subquery form can't, so it survives as its own node. The subquery
5140    /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
5141    RowCmpSubquery {
5142        row: Vec<Expr>,
5143        op: BinOp,
5144        subquery: Box<SelectStatement>,
5145    },
5146    /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
5147    /// list. Both the parser's literal-list path and the engine's
5148    /// IN-subquery materialisation used to desugar into a left-deep
5149    /// OR-Eq chain, so expression depth scaled with the element count
5150    /// — a 24k-row subquery result overflowed the 2 MiB worker stack
5151    /// (recursive eval AND recursive Box drop) and aborted embedding
5152    /// host processes. The flat node keeps depth constant: eval is an
5153    /// iterative scan with PG three-valued logic, drop is a Vec drop.
5154    InList {
5155        expr: Box<Expr>,
5156        list: Vec<Expr>,
5157        negated: bool,
5158    },
5159    /// `EXTRACT(<field> FROM <source>)` — pull an integer component
5160    /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
5161    /// because the `FROM` keyword is what separates the two halves,
5162    /// not a comma.
5163    Extract {
5164        field: ExtractField,
5165        source: Box<Expr>,
5166    },
5167    /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
5168    /// element is evaluated independently; NULLs are allowed.
5169    /// v7.10 supports only single-dimension TEXT[] semantically;
5170    /// non-text elements coerce at engine evaluation time when
5171    /// the surrounding context (column type / cast) makes the
5172    /// target clear.
5173    Array(Vec<Expr>),
5174    /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
5175    /// engine returns NULL for out-of-range indices.
5176    ArraySubscript {
5177        target: Box<Expr>,
5178        index: Box<Expr>,
5179    },
5180    /// Array slice `arr[lo:hi]` — PG 1-based, both ends
5181    /// inclusive; a missing bound extends to that end of the
5182    /// array and out-of-range bounds clamp. Returns an array of
5183    /// the same element type.
5184    ArraySlice {
5185        target: Box<Expr>,
5186        lo: Option<Box<Expr>>,
5187        hi: Option<Box<Expr>>,
5188    },
5189    /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
5190    /// operator is the comparison binary op (Eq / Ne / Lt / …);
5191    /// the engine desugars: `ANY` returns true if any element
5192    /// satisfies; `ALL` returns true only if every element does.
5193    /// NULL handling follows PG's three-valued logic.
5194    AnyAll {
5195        expr: Box<Expr>,
5196        op: BinOp,
5197        array: Box<Expr>,
5198        /// `true` = ANY, `false` = ALL.
5199        is_any: bool,
5200    },
5201    /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
5202    /// (searched form, `operand` is None) and
5203    /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
5204    /// `operand` is the lead expression compared against each
5205    /// branch's match). Each `(when_expr, then_expr)` branch
5206    /// stays as written; engine short-circuits on the first match.
5207    /// `else_branch` is `None` when no ELSE; evaluates to NULL.
5208    /// mailrs round-5 G9.
5209    Case {
5210        operand: Option<Box<Expr>>,
5211        branches: Vec<(Expr, Expr)>,
5212        else_branch: Option<Box<Expr>>,
5213    },
5214}
5215
5216/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
5217/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
5218/// in the offset walk. `Ignore` causes the function to skip NULL
5219/// values in the argument expression, returning the next non-NULL.
5220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5221pub enum NullTreatment {
5222    #[default]
5223    Respect,
5224    Ignore,
5225}
5226
5227/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
5228/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
5229/// where end implicitly = CURRENT ROW.
5230#[derive(Debug, Clone, PartialEq, Eq)]
5231pub struct WindowFrame {
5232    pub kind: FrameKind,
5233    pub start: FrameBound,
5234    pub end: Option<FrameBound>,
5235    /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
5236    /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
5237    /// no-op; CURRENT ROW drops the current row from the frame.
5238    pub exclude: FrameExclusion,
5239}
5240
5241#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5242pub enum FrameExclusion {
5243    /// Default — exclude nothing.
5244    #[default]
5245    NoOthers,
5246    /// Drop the current row from the frame.
5247    CurrentRow,
5248    /// Drop the current row's whole peer group.
5249    Group,
5250    /// Drop the current row's peers but keep the current row.
5251    Ties,
5252}
5253
5254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5255pub enum FrameKind {
5256    Rows,
5257    Range,
5258    /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
5259    /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
5260    /// bounds (no explicit integer offsets) GROUPS behaves identically
5261    /// to RANGE — both consult the peer-group of the current row.
5262    /// Integer offsets are not yet supported; the executor rejects
5263    /// them at run time.
5264    Groups,
5265}
5266
5267#[derive(Debug, Clone, PartialEq, Eq)]
5268pub enum FrameBound {
5269    UnboundedPreceding,
5270    OffsetPreceding(u64),
5271    CurrentRow,
5272    OffsetFollowing(u64),
5273    UnboundedFollowing,
5274    /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
5275    /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
5276    /// interval is folded to its (months, days, micros) components at
5277    /// parse time.
5278    IntervalPreceding {
5279        months: i32,
5280        days: i32,
5281        micros: i64,
5282    },
5283    IntervalFollowing {
5284        months: i32,
5285        days: i32,
5286        micros: i64,
5287    },
5288}
5289
5290impl fmt::Display for FrameBound {
5291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5292        match self {
5293            Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
5294            Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
5295            Self::CurrentRow => f.write_str("CURRENT ROW"),
5296            Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
5297            Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
5298            Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
5299            Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
5300        }
5301    }
5302}
5303
5304#[derive(Debug, Clone, PartialEq, Eq)]
5305pub enum ExtractField {
5306    Year,
5307    Month,
5308    Day,
5309    Hour,
5310    Minute,
5311    Second,
5312    Microsecond,
5313    /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
5314    /// SPG keeps the integer convention — truncated seconds).
5315    Epoch,
5316    /// Day of week, 0 = Sunday … 6 = Saturday.
5317    Dow,
5318    /// ISO day of week, 1 = Monday … 7 = Sunday.
5319    Isodow,
5320    /// Day of year, 1-366.
5321    Doy,
5322    /// ISO 8601 week number, 1-53.
5323    Week,
5324    /// ISO 8601 week-numbering year (pairs with `Week`).
5325    Isoyear,
5326    /// Quarter, 1-4.
5327    Quarter,
5328    /// Year divided by 10 (floor).
5329    Decade,
5330    /// Century — 2001-2100 is century 21.
5331    Century,
5332    /// Millennium — 2001-3000 is millennium 3.
5333    Millennium,
5334    /// Julian day number (truncated for timestamps).
5335    Julian,
5336    /// Seconds and fraction in milliseconds (ss·1000 + frac).
5337    Millisecond,
5338    /// UTC offset in seconds — SPG sessions run UTC, so 0.
5339    Timezone,
5340    /// Hour component of the UTC offset — 0.
5341    TimezoneHour,
5342    /// Minute component of the UTC offset — 0.
5343    TimezoneMinute,
5344    /// v7.39 (round 253) — a field name the parser does not know. PG
5345    /// resolves EXTRACT fields at RUNTIME and reports them with the
5346    /// source type (`unit "nosuch" not recognized for type timestamp
5347    /// without time zone`, 22023), so the parser carries the raw name
5348    /// instead of rejecting.
5349    Other(String),
5350}
5351
5352impl fmt::Display for ExtractField {
5353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5354        f.write_str(match self {
5355            Self::Year => "YEAR",
5356            Self::Month => "MONTH",
5357            Self::Day => "DAY",
5358            Self::Hour => "HOUR",
5359            Self::Minute => "MINUTE",
5360            Self::Second => "SECOND",
5361            Self::Microsecond => "MICROSECOND",
5362            Self::Epoch => "EPOCH",
5363            Self::Dow => "DOW",
5364            Self::Isodow => "ISODOW",
5365            Self::Doy => "DOY",
5366            Self::Week => "WEEK",
5367            Self::Isoyear => "ISOYEAR",
5368            Self::Quarter => "QUARTER",
5369            Self::Decade => "DECADE",
5370            Self::Century => "CENTURY",
5371            Self::Millennium => "MILLENNIUM",
5372            Self::Julian => "JULIAN",
5373            Self::Millisecond => "MILLISECOND",
5374            Self::Timezone => "TIMEZONE",
5375            Self::TimezoneHour => "TIMEZONE_HOUR",
5376            Self::TimezoneMinute => "TIMEZONE_MINUTE",
5377            Self::Other(name) => return f.write_str(name),
5378        })
5379    }
5380}
5381
5382#[derive(Debug, Clone, PartialEq, Eq)]
5383pub enum CastTarget {
5384    Int,
5385    BigInt,
5386    Float,
5387    Text,
5388    Bool,
5389    Vector,
5390    Date,
5391    Timestamp,
5392    /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
5393    /// H3a. Engine reuses the existing runtime-interval / timestamp
5394    /// paths (parse the text input, return the matching Value).
5395    Interval,
5396    Timestamptz,
5397    /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
5398    /// types (v7.9.0); the cast just routes Text→Json with the
5399    /// requested OID for the wire layer.
5400    Json,
5401    Jsonb,
5402    /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
5403    /// compatibility; engine surfaces as Unsupported with a
5404    /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
5405    RegType,
5406    RegClass,
5407    /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
5408    /// the PG external array form `{a,b,NULL}`.
5409    TextArray,
5410    /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
5411    /// `{1,2,3}` or widens a `TextArray` whose elements are
5412    /// integer-shaped.
5413    IntArray,
5414    BigIntArray,
5415    /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
5416    /// external form text representation. Used by pg_dump output
5417    /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
5418    TsVector,
5419    TsQuery,
5420    /// v7.17.0 — `::uuid`. Decodes the LHS Text via
5421    /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
5422    /// unhyphenated, uppercase, and brace-wrapped forms); malformed
5423    /// input is a SQL error.
5424    Uuid,
5425    /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
5426    /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
5427    /// inputs pass through unchanged. Closes the mailrs D-pre #3
5428    /// reverse-acceptance gap — anywhere a PG schema writes
5429    /// `expr::bytea`, SPG now matches.
5430    Bytea,
5431    /// v7.37.5 ship triage — generic cast target for the long tail
5432    /// of PG type names the parser meets in `expr::TYPE` shapes that
5433    /// don't deserve their own enum variant. The engine routes these
5434    /// through `column_type_to_data_type` + the existing typed
5435    /// `coerce_value` dispatch, so adding a new PG type to SPG
5436    /// implicitly adds its cast-target form too — no parser change
5437    /// per type. The string carries the lowercase PG type ident
5438    /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
5439    /// a clear message when the type isn't known.
5440    Named(String),
5441}
5442
5443impl fmt::Display for CastTarget {
5444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5445        f.write_str(match self {
5446            Self::Int => "int",
5447            Self::BigInt => "bigint",
5448            Self::Float => "float",
5449            Self::Text => "text",
5450            Self::Bool => "bool",
5451            Self::Vector => "vector",
5452            Self::Interval => "interval",
5453            Self::Timestamptz => "timestamptz",
5454            Self::Json => "json",
5455            Self::Jsonb => "jsonb",
5456            Self::RegType => "regtype",
5457            Self::RegClass => "regclass",
5458            Self::Date => "date",
5459            Self::Timestamp => "timestamp",
5460            Self::TextArray => "TEXT[]",
5461            Self::IntArray => "INT[]",
5462            Self::BigIntArray => "BIGINT[]",
5463            Self::TsVector => "tsvector",
5464            Self::TsQuery => "tsquery",
5465            Self::Uuid => "uuid",
5466            Self::Bytea => "bytea",
5467            // v7.37.5 — `Self::Named` carries its own canonical name.
5468            Self::Named(name) => return f.write_str(name),
5469        })
5470    }
5471}
5472
5473#[derive(Debug, Clone, PartialEq)]
5474pub enum Literal {
5475    Integer(i64),
5476    Float(f64),
5477    /// Exact decimal literal — a bare `12.34`-style token, kept as
5478    /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
5479    /// before it becomes a `Value::Numeric`. PG parses such literals as
5480    /// `numeric`, not `double precision`. (Scientific/huge literals stay
5481    /// `Float`.)
5482    Numeric {
5483        unscaled: i128,
5484        /// v7.39 (round 271) — widened to u16. At u8 a literal with more
5485        /// than 255 decimal places could not be represented, and the
5486        /// conversion's `.expect("lexer-validated decimal")` aborted the
5487        /// query with an internal error on SQL PG accepts.
5488        scale: u16,
5489    },
5490    /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
5491    /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
5492    /// `Value::NumericBig` at eval; previously such literals fell back to double.
5493    NumericBig(String),
5494    String(String),
5495    /// v7.38.8 — a temporal constant that has already been decoded.
5496    ///
5497    /// Without these the only way to carry one through the AST was as
5498    /// text, and a predicate comparing a `timestamp` column against a
5499    /// literal then coerced that text back into a timestamp ONCE PER
5500    /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
5501    /// profile. `constfold` produced text for the same reason: its exit
5502    /// had nothing else to hand back.
5503    ///
5504    /// `text` keeps the spelling so `Display` round-trips byte for byte,
5505    /// the way `Interval` already does and for the same reason: this
5506    /// node is printed in EXPLAIN, in dumps and in error messages, and
5507    /// none of those should change because the value stopped being
5508    /// carried as a string. The enum already holds a `String` and an
5509    /// `i128`, so neither variant widens it.
5510    Timestamp {
5511        micros: i64,
5512        text: String,
5513    },
5514    /// Days since the epoch `Value::Date` counts from. See
5515    /// [`Literal::Timestamp`].
5516    Date {
5517        days: i32,
5518        text: String,
5519    },
5520    Bool(bool),
5521    Null,
5522    /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
5523    Vector(Vec<f32>),
5524    /// TEXT[] value carried through the prepared-bind path
5525    /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
5526    /// text form, so the array rides the AST natively).
5527    TextArray(Vec<Option<String>>),
5528    /// INT[] value carried through the prepared-bind path.
5529    IntArray(Vec<Option<i32>>),
5530    /// BIGINT[] value carried through the prepared-bind path.
5531    BigIntArray(Vec<Option<i64>>),
5532    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
5533    /// Three independent dimensions: `months` (variable-length;
5534    /// year/month), `days` (fixed 86400 seconds at non-DST, but
5535    /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
5536    /// stays distinguishable), and `micros` (sub-day; can carry).
5537    /// `text` keeps the original spelling so Display round-trips
5538    /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
5539    Interval {
5540        months: i32,
5541        days: i32,
5542        micros: i64,
5543        text: String,
5544    },
5545}
5546
5547#[derive(Debug, Clone, PartialEq, Eq)]
5548pub struct ColumnName {
5549    pub qualifier: Option<String>,
5550    pub name: String,
5551}
5552
5553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5554pub enum BinOp {
5555    Or,
5556    And,
5557    Eq,
5558    NotEq,
5559    /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
5560    /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
5561    /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
5562    /// non-NULL behaviour matches `<>` / `=` exactly. Common in
5563    /// PG-style JOIN ON predicates and pg_dump output.
5564    IsDistinctFrom,
5565    IsNotDistinctFrom,
5566    /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
5567    /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
5568    /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
5569    /// is a real division (round 351).
5570    IntDiv,
5571    Lt,
5572    LtEq,
5573    Gt,
5574    GtEq,
5575    Add,
5576    Sub,
5577    Mul,
5578    Div,
5579    /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
5580    /// precedence as Mul/Div; result type follows left operand.
5581    Mod,
5582    /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
5583    /// operands of equal dimension; engine returns `Value::Float(d)`.
5584    L2Distance,
5585    /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
5586    GeomParallel,
5587    /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
5588    OverLeft,
5589    OverRight,
5590    /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
5591    GeomPerp,
5592    /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
5593    GeomSameAs,
5594    /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
5595    /// object to the left-hand one.
5596    ClosestPoint,
5597    /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
5598    GeomHoriz,
5599    /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
5600    /// more similar" remains true (matches pgvector's published convention).
5601    InnerProduct,
5602    /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
5603    CosineDistance,
5604    /// SQL string concatenation `||`. NULL propagates.
5605    Concat,
5606    /// Bitwise OR `|` on integers.
5607    BitOr,
5608    /// Bitwise AND `&` on integers.
5609    BitAnd,
5610    /// Bitwise XOR `#` on integers and equal-length bit strings.
5611    BitXor,
5612    /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
5613    /// sides as truth values and returns their exclusive-or (`1 XOR 0`
5614    /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
5615    /// MySQL dialect produces it; PG has no logical XOR. Its precedence
5616    /// sits between OR (loosest) and AND.
5617    LogicalXor,
5618    /// v4.14 `json -> key` — element access by string key (object)
5619    /// or integer index (array). Returns a JSON value.
5620    JsonGet,
5621    /// v4.14 `json ->> key` — same access, returns the result as
5622    /// TEXT (unwraps a top-level JSON string; renders other scalars
5623    /// as their canonical text).
5624    JsonGetText,
5625    /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
5626    /// text array literal like `'{a,0,b}'`. Returns JSON.
5627    JsonGetPath,
5628    /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
5629    JsonGetPathText,
5630    /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
5631    /// when every key/value in `sub_json` is structurally present in
5632    /// the left side. Matches PG semantics (top-level + recursive).
5633    JsonContains,
5634    /// `@?` — jsonb path existence (jsonb_path_exists).
5635    JsonPathExists,
5636    /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
5637    /// `a <@ b` is defined as `b @> a` (same semantics, swapped
5638    /// sides). Eval dispatch reuses `JsonContains` with swapped args.
5639    JsonContainedBy,
5640    /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
5641    /// returns BOOL. For an object, true if `key` is an existing
5642    /// member name; for an array, true if any element is the string
5643    /// `key` (PG semantics).
5644    JsonKeyExists,
5645    /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
5646    /// returns BOOL.
5647    JsonKeysAny,
5648    /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
5649    /// returns BOOL.
5650    JsonKeysAll,
5651    /// `jsonb #- path_text[]` — delete the value at a nested path.
5652    /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
5653    JsonDeletePath,
5654    /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
5655    /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
5656    /// tsvector` and engine eval normalises either ordering.
5657    TsMatch,
5658    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
5659    /// `<<`. LHS network is strictly inside RHS network (no equality).
5660    InetContainedBy,
5661    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
5662    /// `<<=`. LHS network ⊆ RHS network.
5663    InetContainedByEq,
5664    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
5665    /// LHS network strictly contains RHS network.
5666    InetContains,
5667    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
5668    /// LHS network ⊇ RHS network.
5669    InetContainsEq,
5670    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
5671    /// True iff either network contains any address of the other.
5672    InetOverlap,
5673    /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
5674    /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
5675    Intersects,
5676    /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
5677    /// (point, box).
5678    IsBelow,
5679    IsAbove,
5680    /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
5681    /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
5682    /// where `'A' < 'a'` is false under a non-C collation, which is the
5683    /// whole reason the operator family exists — it is what makes a LIKE
5684    /// prefix index-usable. pg_dump writes these into index definitions.
5685    PatternLt,
5686    PatternLtEq,
5687    PatternGt,
5688    PatternGtEq,
5689}
5690
5691#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5692pub enum UnOp {
5693    Not,
5694    Neg,
5695    /// Bitwise NOT `~` on integers.
5696    BitNot,
5697    /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
5698    /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
5699    /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
5700    /// while PG18 and MariaDB accept every one of them.
5701    ///
5702    /// It is not a no-op to drop at parse time — PG refuses it on
5703    /// non-numeric operands ("operator does not exist: + boolean"), so the
5704    /// operand's type has to be seen at eval.
5705    Plus,
5706}
5707
5708// --- Display impls (round-trip-safe) --------------------------------------
5709
5710impl Statement {
5711    /// v7.18 — classify whether the statement is read-only at
5712    /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
5713    /// route SELECT-shaped traffic through the fan-out
5714    /// `AsyncReadHandle` (no writer-lock contention) while
5715    /// keeping DML / DDL / TX-control on the single-writer path.
5716    ///
5717    /// The classification matches what
5718    /// `Engine::execute_readonly_with_cancel` accepts: anything
5719    /// that does NOT mutate catalog, statistics, session state,
5720    /// or transaction state. WaitForWalPosition is included
5721    /// (engine returns `Unsupported`, but the classification is
5722    /// semantically read-only — no mutation). Empty is excluded
5723    /// out of an abundance of caution — the no-op routes
5724    /// through the writer so any future side effect lands
5725    /// uniformly.
5726    ///
5727    /// **Not connection-state aware**. `SET LOCAL` / `RESET`
5728    /// affect session parameters and must run on the writer
5729    /// engine that owns the session state; they classify as
5730    /// writer-path here. Same for `BEGIN` / `COMMIT` /
5731    /// `ROLLBACK` / `SAVEPOINT` — transaction control is
5732    /// always writer-path.
5733    /// v7.39 (round 435) — does this statement implicitly COMMIT an open
5734    /// transaction under MySQL?
5735    ///
5736    /// PG runs DDL inside the transaction; MySQL commits before (and after)
5737    /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
5738    /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
5739    /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
5740    /// nested START TRANSACTION; and measured NOT to fire for `CREATE
5741    /// TEMPORARY TABLE`, `SET`, or a SELECT.
5742    ///
5743    /// A positive list, not "everything that is not DML": a statement
5744    /// wrongly listed here commits a client's data early, which is as bad as
5745    /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
5746    /// COMPACT) are left out — a MySQL session never sends them.
5747    #[must_use]
5748    pub fn mysql_implicit_commit(&self) -> bool {
5749        match self {
5750            // MySQL's documented exception, measured on MariaDB 11: a
5751            // TEMPORARY table is not DDL for this purpose and does not
5752            // commit. (Round 435 got this for free because the parser then
5753            // lowered that spelling to `Statement::Empty`; round 436 made it
5754            // a real CREATE TABLE, and the round-435 pin caught it.)
5755            Self::CreateTable(c) => !c.temporary,
5756            // MySQL commits the open transaction and opens a fresh one.
5757            Self::Begin { .. }
5758            | Self::DropTable { .. }
5759            | Self::DropIndex { .. }
5760            | Self::CreateIndex(_)
5761            | Self::AlterIndex { .. }
5762            | Self::AlterTable(_)
5763            | Self::Truncate { .. }
5764            | Self::Analyze { .. }
5765            | Self::CreateStatistics { .. }
5766            | Self::DropStatistics { .. }
5767            | Self::CreateView { .. }
5768            | Self::DropView { .. }
5769            | Self::CreateMaterializedView { .. }
5770            | Self::RefreshMaterializedView { .. }
5771            | Self::DropMaterializedView { .. }
5772            | Self::CreateSequence(_)
5773            | Self::AlterSequence { .. }
5774            | Self::DropSequence { .. }
5775            | Self::CreateFunction(_)
5776            | Self::DropFunction { .. }
5777            | Self::CreateTrigger(_)
5778            | Self::DropTrigger { .. }
5779            | Self::CreateRule(_)
5780            | Self::DropRule { .. }
5781            | Self::CreateType(_)
5782            | Self::DropType { .. }
5783            | Self::AlterTypeAddValue { .. }
5784            | Self::AlterTypeRenameValue { .. }
5785            | Self::CreateDomain(_)
5786            | Self::AlterDomain { .. }
5787            | Self::DropDomain { .. }
5788            | Self::CreateSchema { .. }
5789            | Self::DropSchema { .. }
5790            | Self::CreateUser { .. }
5791            | Self::DropUser { .. }
5792            | Self::Grant { .. }
5793            | Self::Revoke { .. }
5794            | Self::CreatePolicy(_)
5795            | Self::AlterPolicy(_)
5796            | Self::DropPolicy { .. }
5797            | Self::CommentOn { .. }
5798            | Self::CreateExtension { .. } => true,
5799            _ => false,
5800        }
5801    }
5802
5803    #[must_use]
5804    pub fn is_readonly(&self) -> bool {
5805        match self {
5806            Statement::RenameTables(_) => false,
5807            // v7.39 (round 288) — SET CONSTRAINTS changes transaction
5808            // state, and IMMEDIATE can run the deferred checks there and
5809            // then; writer-path.
5810            Statement::SetConstraints { .. } => false,
5811            // v7.39 (round 695) — it writes nothing (SPG has no
5812            // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
5813            // writer and a read-only session refuses it there too.
5814            Statement::AlterSystem { .. } => false,
5815            // Same shape: a no-op here, a writer to PG, so a read-only
5816            // session refuses it as PG's would.
5817            Statement::NoOpPreventedInTransaction { .. } => false,
5818            Statement::DropDatabase { .. } => false,
5819            // v7.39 (round 696) — they perform nothing, so nothing is
5820            // written; PG classes LOCK and the OWNED BY pair as writers and
5821            // a read-only session refuses them there.
5822            Statement::ValidateOnly { .. } => false,
5823            // v7.39 (round 750) — a credential rotation persists.
5824            Statement::AlterRolePassword { .. } => true,
5825            Statement::DropAggregate { .. } => false,
5826            // v7.39 (round 547) — records a GUC default in the catalog.
5827            Statement::SetDbRoleSetting(_) => false,
5828            // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5829            // but they name a relation and PG refuses one that is not
5830            // there, so they are not read-only in the sense this asks.
5831            Statement::Maintain { .. } => false,
5832            // v7.39 (round 277) — the prepared-statement surface is
5833            // session state, like SET; writer-path so it lands on the
5834            // engine that owns the session. EXECUTE may also run a
5835            // write, and its body is only known at execution time.
5836            Statement::Prepare { .. }
5837            | Statement::Execute { .. }
5838            | Statement::Deallocate(_)
5839            | Statement::Call(_)
5840            | Statement::PrepareTransaction(_)
5841            | Statement::CreateStatistics { .. }
5842            | Statement::DropStatistics { .. }
5843            // v7.39 (round 318, V51) — KILL signals another connection;
5844            // it must run on the writer path that owns the registry hook.
5845            | Statement::Kill { .. }
5846            // v7.39 (round 320, V53) — DISCARD throws session state away;
5847            // writer path, like SET / RESET.
5848            | Statement::Discard(_)
5849            // v7.39.2 — `USE <db>` writes session state, the same way
5850            // SET does, and takes the same path.
5851            | Statement::UseDatabase(_) => false,
5852            // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5853            // locks MUTATES the lock table, so it is not a read. Left as
5854            // a read it went to the read-only executor and the locking
5855            // pre-pass never ran at all — the clause was honoured only
5856            // inside an explicit transaction, and silently ignored in
5857            // autocommit, which is where a queue worker runs it.
5858            Statement::Select(s) if s.locking.is_some() => false,
5859            Statement::Select(_)
5860            | Statement::CopyTo { .. }
5861            | Statement::CopyToFile { .. }
5862            | Statement::Explain(_)
5863            | Statement::ShowTables
5864            | Statement::ShowDatabases
5865            | Statement::ShowCreateTable(_)
5866            | Statement::ShowIndexes(_)
5867            | Statement::ShowStatus
5868            | Statement::ShowVariables
5869            | Statement::ShowVariablesLike(_)
5870            | Statement::ShowProcesslist
5871            | Statement::ShowColumns(_)
5872            | Statement::ShowUsers
5873            | Statement::ShowPublications
5874            | Statement::ShowSubscriptions
5875            | Statement::WaitForWalPosition { .. } => true,
5876            // Everything else mutates catalog, statistics,
5877            // session state, or transaction state — writer path.
5878            // Listed explicitly so a new Statement variant fails
5879            // the match exhaustiveness check and forces a
5880            // classification decision at add-site.
5881            Statement::Empty
5882            // v7.39 (round 169) — VACUUM mutates storage (reclaims
5883            // tombstoned versions): writer path.
5884            | Statement::Vacuum { .. }
5885            | Statement::DropTable { .. }
5886            | Statement::DropIndex { .. }
5887            | Statement::CreateTable(_)
5888            | Statement::CreateExtension(_)
5889            | Statement::DoBlock(_)
5890            | Statement::CreateIndex(_)
5891            | Statement::Insert(_)
5892            | Statement::Update(_)
5893            | Statement::Delete(_)
5894            | Statement::Merge(_)
5895            | Statement::Begin(_)
5896            | Statement::Commit
5897            | Statement::Rollback
5898            | Statement::Savepoint(_)
5899            | Statement::RollbackToSavepoint(_)
5900            | Statement::ReleaseSavepoint(_)
5901            | Statement::CreateUser(_)
5902            | Statement::DropUser { .. }
5903            | Statement::SetRole(_)
5904            | Statement::Grant(_)
5905            | Statement::Revoke(_)
5906            | Statement::CreatePolicy(_)
5907            | Statement::AlterPolicy(_)
5908            | Statement::DropPolicy(_)
5909            | Statement::AlterIndex(_)
5910            | Statement::AlterTable(_)
5911            | Statement::CreatePublication(_)
5912            | Statement::DropPublication { .. }
5913            | Statement::CreateSubscription(_)
5914            | Statement::DropSubscription { .. }
5915            | Statement::Analyze(_)
5916            | Statement::Truncate { .. }
5917            | Statement::CompactColdSegments
5918            | Statement::SetParameter { .. }
5919            | Statement::SetParameterList(_)
5920            | Statement::SetUserVars(..)
5921            | Statement::SetTransaction { .. }
5922            | Statement::ShowParameter(_)
5923            | Statement::ResetParameter(_)
5924            | Statement::CreateFunction(_)
5925            | Statement::CreateTrigger(_)
5926            | Statement::DropTrigger { .. }
5927            | Statement::CreateRule(_)
5928            | Statement::DropRule { .. }
5929            | Statement::DropFunction { .. }
5930            | Statement::CreateSequence(_)
5931            | Statement::AlterSequence(_)
5932            | Statement::DropSequence { .. }
5933            | Statement::CreateView(_)
5934            | Statement::DropView { .. }
5935            | Statement::CreateMaterializedView(_)
5936            | Statement::RefreshMaterializedView { .. }
5937            | Statement::DropMaterializedView { .. }
5938            | Statement::CreateType(_)
5939            | Statement::AlterTypeAddValue { .. }
5940            | Statement::AlterTypeRenameValue { .. }
5941            | Statement::CommentOn { .. }
5942            | Statement::DropType { .. }
5943            | Statement::CreateDomain(_)
5944            | Statement::DropDomain { .. }
5945            | Statement::CreateSchema { .. }
5946            | Statement::DropSchema { .. }
5947            // v7.39 (round 218) — cursors mutate per-session cursor state
5948            // (open/position/close) on the writer engine: writer path.
5949            | Statement::DeclareCursor { .. }
5950            | Statement::FetchCursor { .. }
5951            | Statement::MoveCursor { .. }
5952            | Statement::CloseCursor { .. }
5953            // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5954            // state / the notification queue: writer path.
5955            | Statement::Listen(_)
5956            | Statement::Notify { .. }
5957            | Statement::Unlisten(_)
5958            | Statement::CopyFromFile { .. }
5959            | Statement::AlterDomain { .. } => false,
5960        }
5961    }
5962}
5963
5964/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5965/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5966#[derive(Debug, Clone, PartialEq, Eq)]
5967pub struct GrantStatement {
5968    /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5969    /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5970    /// is why they keep the case the user typed.
5971    pub privileges: Vec<GrantPriv>,
5972    /// What the privileges are on.
5973    pub object: GrantObject,
5974    /// The roles granted to / revoked from. An empty string entry = PUBLIC.
5975    pub grantees: Vec<String>,
5976    /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
5977    /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
5978    /// privilege itself).
5979    pub grant_option: bool,
5980}
5981
5982/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
5983/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
5984/// An empty column list means the privilege is table-wide.
5985#[derive(Debug, Clone, PartialEq, Eq)]
5986pub struct GrantPriv {
5987    pub word: String,
5988    pub columns: Vec<String>,
5989}
5990
5991/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
5992/// privileges; every other object class parses and is accepted as a no-op, so
5993/// a pg_dump that grants on schemas / sequences / functions still restores.
5994#[derive(Debug, Clone, PartialEq, Eq)]
5995pub enum GrantObject {
5996    /// `ON [TABLE] a, b` — the enforced case.
5997    Tables(Vec<String>),
5998    /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
5999    /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
6000    /// granted roles; the grantees are the members.
6001    Roles(Vec<String>),
6002    /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
6003    /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
6004    Sequences(Vec<String>),
6005    /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
6006    Schemas(Vec<String>),
6007    /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
6008    Databases(Vec<String>),
6009    /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
6010    /// (SPG keys functions by name); the argument list parses and is dropped.
6011    Functions(Vec<(String, Option<Vec<String>>)>),
6012    /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
6013    /// every table at GRANT time, exactly like PG.
6014    AllTablesInSchema,
6015    /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
6016    /// message.
6017    Other(String),
6018}
6019
6020impl GrantStatement {
6021    /// Round-trip text. `grant = false` renders the REVOKE form.
6022    fn render(&self, grant: bool) -> alloc::string::String {
6023        use core::fmt::Write as _;
6024        let mut s = alloc::string::String::new();
6025        let privs = if self.privileges.is_empty() {
6026            alloc::string::String::from("ALL")
6027        } else {
6028            let parts: Vec<_> = self
6029                .privileges
6030                .iter()
6031                .map(|p| {
6032                    if p.columns.is_empty() {
6033                        p.word.clone()
6034                    } else {
6035                        let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
6036                        alloc::format!("{} ({})", p.word, cols.join(", "))
6037                    }
6038                })
6039                .collect();
6040            parts.join(", ")
6041        };
6042        let obj = match &self.object {
6043            GrantObject::Tables(t) => {
6044                let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
6045                alloc::format!("TABLE {}", names.join(", "))
6046            }
6047            GrantObject::Roles(r) => {
6048                let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
6049                names.join(", ")
6050            }
6051            GrantObject::Sequences(n) => {
6052                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6053                alloc::format!("SEQUENCE {}", names.join(", "))
6054            }
6055            GrantObject::Schemas(n) => {
6056                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6057                alloc::format!("SCHEMA {}", names.join(", "))
6058            }
6059            GrantObject::Databases(n) => {
6060                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6061                alloc::format!("DATABASE {}", names.join(", "))
6062            }
6063            GrantObject::Functions(n) => {
6064                let names: Vec<_> = n
6065                    .iter()
6066                    .map(|(name, args)| match args {
6067                        Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
6068                        None => quote_ident(name),
6069                    })
6070                    .collect();
6071                alloc::format!("FUNCTION {}", names.join(", "))
6072            }
6073            GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
6074            GrantObject::Other(k) => k.clone(),
6075        };
6076        let who: Vec<_> = self
6077            .grantees
6078            .iter()
6079            .map(|g| {
6080                if g.is_empty() {
6081                    "PUBLIC".into()
6082                } else {
6083                    quote_ident(g)
6084                }
6085            })
6086            .collect();
6087        if let GrantObject::Roles(_) = &self.object {
6088            let _ = if grant {
6089                write!(s, "GRANT {obj} TO {}", who.join(", "))
6090            } else {
6091                write!(s, "REVOKE {obj} FROM {}", who.join(", "))
6092            };
6093            return s;
6094        }
6095        if grant {
6096            let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
6097            if self.grant_option {
6098                s.push_str(" WITH GRANT OPTION");
6099            }
6100        } else {
6101            s.push_str("REVOKE ");
6102            if self.grant_option {
6103                s.push_str("GRANT OPTION FOR ");
6104            }
6105            let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
6106        }
6107        s
6108    }
6109}
6110
6111impl fmt::Display for Statement {
6112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6113        match self {
6114            Self::Empty => Ok(()),
6115            // v7.39 (round 695) — deparsed the way PG writes it.
6116            // v7.39 (round 696) — never deparsed into a dump (nothing is
6117            // stored), so the shortest faithful spelling of what it was.
6118            Self::DropAggregate { if_exists, items } => {
6119                f.write_str("DROP AGGREGATE ")?;
6120                if *if_exists {
6121                    f.write_str("IF EXISTS ")?;
6122                }
6123                for (i, (name, args)) in items.iter().enumerate() {
6124                    if i > 0 {
6125                        f.write_str(", ")?;
6126                    }
6127                    match args {
6128                        Some(a) => write!(f, "{name}({})", a.join(", "))?,
6129                        None => write!(f, "{name}(*)")?,
6130                    }
6131                }
6132                Ok(())
6133            }
6134            Self::AlterRolePassword { name, password } => {
6135                write!(f, "ALTER ROLE {}", quote_ident(name))?;
6136                match password {
6137                    Some(_) => f.write_str(" PASSWORD '<redacted>'"),
6138                    None => f.write_str(" PASSWORD NULL"),
6139                }
6140            }
6141            Self::ValidateOnly { kind, names } => match kind {
6142                ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
6143                ValidateOnlyKind::RoleName => {
6144                    write!(f, "DROP OWNED BY {}", names.join(", "))
6145                }
6146                ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
6147                ValidateOnlyKind::ExtensionAvailable => {
6148                    write!(f, "CREATE EXTENSION {}", names.join(", "))
6149                }
6150                ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
6151                ValidateOnlyKind::CollationName => {
6152                    write!(f, "DROP COLLATION {}", names.join(", "))
6153                }
6154                ValidateOnlyKind::TsConfigName => {
6155                    write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
6156                }
6157                ValidateOnlyKind::EventTriggerName => {
6158                    write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
6159                }
6160                ValidateOnlyKind::TablespaceName => {
6161                    write!(f, "DROP TABLESPACE {}", names.join(", "))
6162                }
6163                ValidateOnlyKind::LargeObjectOid => {
6164                    write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
6165                }
6166                ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
6167                ValidateOnlyKind::AggregateName => {
6168                    write!(f, "ALTER AGGREGATE {}", names.join(", "))
6169                }
6170                ValidateOnlyKind::ConversionName => {
6171                    write!(f, "DROP CONVERSION {}", names.join(", "))
6172                }
6173                ValidateOnlyKind::LanguageName => {
6174                    write!(f, "DROP LANGUAGE {}", names.join(", "))
6175                }
6176                ValidateOnlyKind::ExtensionInstalled => {
6177                    write!(f, "DROP EXTENSION {}", names.join(", "))
6178                }
6179            },
6180            Self::AlterSystem { parameter } => match parameter {
6181                Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
6182                None => f.write_str("ALTER SYSTEM RESET ALL"),
6183            },
6184            // v7.39 (round 547) — round-trips as PG writes it.
6185            Self::SetDbRoleSetting(st) => {
6186                match (&st.database, &st.role) {
6187                    (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
6188                    (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
6189                    (None, None) => f.write_str("ALTER ROLE ALL")?,
6190                }
6191                if let (Some(d), Some(_)) = (&st.database, &st.role) {
6192                    write!(f, " IN DATABASE {d}")?;
6193                }
6194                match (&st.param, &st.value) {
6195                    (None, _) => f.write_str(" RESET ALL"),
6196                    (Some(p), None) => write!(f, " RESET {p}"),
6197                    (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
6198                }
6199            }
6200            Self::Maintain {
6201                kind,
6202                concurrently,
6203                target,
6204            } => {
6205                f.write_str(match kind {
6206                    crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
6207                    _ => "REINDEX ",
6208                })?;
6209                if *concurrently {
6210                    f.write_str("CONCURRENTLY ")?;
6211                }
6212                if let Some(t) = target {
6213                    f.write_str(t)?;
6214                }
6215                Ok(())
6216            }
6217            Self::DropDatabase { name, if_exists } => {
6218                f.write_str("DROP DATABASE ")?;
6219                if *if_exists {
6220                    f.write_str("IF EXISTS ")?;
6221                }
6222                f.write_str(name)
6223            }
6224            Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
6225            Self::SetConstraints { names, deferred } => {
6226                f.write_str("SET CONSTRAINTS ")?;
6227                if names.is_empty() {
6228                    f.write_str("ALL")?;
6229                } else {
6230                    for (i, n) in names.iter().enumerate() {
6231                        if i > 0 {
6232                            f.write_str(", ")?;
6233                        }
6234                        f.write_str(n)?;
6235                    }
6236                }
6237                f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
6238            }
6239            // v7.39 (round 277) — the source text is kept verbatim so
6240            // `pg_prepared_statements.statement` can report it the way
6241            // PG does (the whole PREPARE statement, not just the body).
6242            Self::Prepare { source, .. } => f.write_str(source),
6243            Self::Execute { name, args } => {
6244                write!(f, "EXECUTE {}", quote_ident(name))?;
6245                if !args.is_empty() {
6246                    f.write_str("(")?;
6247                    for (i, a) in args.iter().enumerate() {
6248                        if i > 0 {
6249                            f.write_str(", ")?;
6250                        }
6251                        write!(f, "{a}")?;
6252                    }
6253                    f.write_str(")")?;
6254                }
6255                Ok(())
6256            }
6257            Self::CreateStatistics {
6258                name,
6259                if_not_exists,
6260                kinds,
6261                columns,
6262                table,
6263            } => {
6264                f.write_str("CREATE STATISTICS ")?;
6265                if *if_not_exists {
6266                    f.write_str("IF NOT EXISTS ")?;
6267                }
6268                write!(f, "{}", quote_ident(name))?;
6269                if !kinds.is_empty() {
6270                    write!(f, " ({})", kinds.join(", "))?;
6271                }
6272                write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
6273            }
6274            Self::DropStatistics { name, if_exists } => {
6275                f.write_str("DROP STATISTICS ")?;
6276                if *if_exists {
6277                    f.write_str("IF EXISTS ")?;
6278                }
6279                write!(f, "{}", quote_ident(name))
6280            }
6281            Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
6282            Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
6283            Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
6284            Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
6285            Self::DeclareCursor {
6286                name,
6287                scroll,
6288                hold,
6289                query,
6290            } => {
6291                write!(f, "DECLARE {} ", quote_ident(name))?;
6292                match scroll {
6293                    Some(true) => f.write_str("SCROLL ")?,
6294                    Some(false) => f.write_str("NO SCROLL ")?,
6295                    None => {}
6296                }
6297                f.write_str("CURSOR ")?;
6298                if *hold {
6299                    f.write_str("WITH HOLD ")?;
6300                }
6301                write!(f, "FOR {query}")
6302            }
6303            Self::FetchCursor { name, direction } => {
6304                write!(f, "FETCH {direction} FROM {}", quote_ident(name))
6305            }
6306            Self::MoveCursor { name, direction } => {
6307                write!(f, "MOVE {direction} FROM {}", quote_ident(name))
6308            }
6309            Self::CloseCursor { name } => match name {
6310                Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
6311                None => f.write_str("CLOSE ALL"),
6312            },
6313            Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
6314            Self::Notify { channel, payload } => {
6315                write!(f, "NOTIFY {}", quote_ident(channel))?;
6316                if let Some(p) = payload {
6317                    write!(f, ", '{}'", p.replace('\'', "''"))?;
6318                }
6319                Ok(())
6320            }
6321            Self::Unlisten(ch) => match ch {
6322                Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
6323                None => f.write_str("UNLISTEN *"),
6324            },
6325            Self::CopyTo {
6326                table,
6327                columns,
6328                query,
6329                options,
6330            } => {
6331                if let Some(q) = query {
6332                    write!(f, "COPY ({q})")?;
6333                } else {
6334                    write!(f, "COPY {table}")?;
6335                    if let Some(cols) = columns {
6336                        write!(f, " ({})", cols.join(", "))?;
6337                    }
6338                }
6339                write!(f, " TO STDOUT")?;
6340                let mut parts: Vec<String> = Vec::new();
6341                if options.format == CopyFormat::Csv {
6342                    parts.push("FORMAT csv".to_string());
6343                }
6344                if options.header {
6345                    parts.push("HEADER true".to_string());
6346                }
6347                if let Some(d) = options.delimiter {
6348                    parts.push(alloc::format!("DELIMITER '{d}'"));
6349                }
6350                if let Some(n) = &options.null_str {
6351                    parts.push(alloc::format!("NULL '{n}'"));
6352                }
6353                if let Some(q) = options.quote {
6354                    parts.push(alloc::format!("QUOTE '{q}'"));
6355                }
6356                if !parts.is_empty() {
6357                    write!(f, " WITH ({})", parts.join(", "))?;
6358                }
6359                Ok(())
6360            }
6361            Self::CopyFromFile {
6362                table,
6363                columns,
6364                path,
6365                options,
6366            } => {
6367                write!(f, "COPY {table}")?;
6368                if let Some(cols) = columns {
6369                    write!(f, " ({})", cols.join(", "))?;
6370                }
6371                write!(f, " FROM '{path}'")?;
6372                let mut parts: Vec<String> = Vec::new();
6373                if options.format == CopyFormat::Csv {
6374                    parts.push("FORMAT csv".to_string());
6375                }
6376                if options.header {
6377                    parts.push("HEADER true".to_string());
6378                }
6379                if let Some(d) = options.delimiter {
6380                    parts.push(alloc::format!("DELIMITER '{d}'"));
6381                }
6382                if let Some(n) = &options.null_str {
6383                    parts.push(alloc::format!("NULL '{n}'"));
6384                }
6385                if let Some(q) = options.quote {
6386                    parts.push(alloc::format!("QUOTE '{q}'"));
6387                }
6388                if !parts.is_empty() {
6389                    write!(f, " WITH ({})", parts.join(", "))?;
6390                }
6391                Ok(())
6392            }
6393            Self::CopyToFile {
6394                table,
6395                columns,
6396                query,
6397                path,
6398                options,
6399            } => {
6400                if let Some(q) = query {
6401                    write!(f, "COPY ({q})")?;
6402                } else {
6403                    write!(f, "COPY {table}")?;
6404                    if let Some(cols) = columns {
6405                        write!(f, " ({})", cols.join(", "))?;
6406                    }
6407                }
6408                write!(f, " TO '{path}'")?;
6409                let mut parts: Vec<String> = Vec::new();
6410                if options.format == CopyFormat::Csv {
6411                    parts.push("FORMAT csv".to_string());
6412                }
6413                if options.header {
6414                    parts.push("HEADER true".to_string());
6415                }
6416                if let Some(d) = options.delimiter {
6417                    parts.push(alloc::format!("DELIMITER '{d}'"));
6418                }
6419                if let Some(n) = &options.null_str {
6420                    parts.push(alloc::format!("NULL '{n}'"));
6421                }
6422                if let Some(q) = options.quote {
6423                    parts.push(alloc::format!("QUOTE '{q}'"));
6424                }
6425                if !parts.is_empty() {
6426                    write!(f, " WITH ({})", parts.join(", "))?;
6427                }
6428                Ok(())
6429            }
6430            Self::AlterDomain { name, action } => {
6431                write!(f, "ALTER DOMAIN {name} ")?;
6432                match action {
6433                    AlterDomainAction::AddConstraint { name: cn, check } => match cn {
6434                        Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
6435                        None => write!(f, "ADD CHECK ({check})"),
6436                    },
6437                    AlterDomainAction::DropConstraint {
6438                        name: cn,
6439                        if_exists,
6440                    } => {
6441                        if *if_exists {
6442                            write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
6443                        } else {
6444                            write!(f, "DROP CONSTRAINT {cn}")
6445                        }
6446                    }
6447                    AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
6448                    AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
6449                    AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
6450                    AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
6451                    AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
6452                }
6453            }
6454            Self::Truncate {
6455                tables,
6456                restart_identity,
6457                cascade,
6458                only,
6459            } => {
6460                f.write_str("TRUNCATE TABLE ")?;
6461                if *only {
6462                    f.write_str("ONLY ")?;
6463                }
6464                for (i, t) in tables.iter().enumerate() {
6465                    if i > 0 {
6466                        f.write_str(", ")?;
6467                    }
6468                    f.write_str(t)?;
6469                }
6470                if *restart_identity {
6471                    f.write_str(" RESTART IDENTITY")?;
6472                }
6473                if *cascade {
6474                    f.write_str(" CASCADE")?;
6475                }
6476                Ok(())
6477            }
6478            Self::DropTable { names, if_exists } => {
6479                f.write_str("DROP TABLE ")?;
6480                if *if_exists {
6481                    f.write_str("IF EXISTS ")?;
6482                }
6483                for (i, n) in names.iter().enumerate() {
6484                    if i > 0 {
6485                        f.write_str(", ")?;
6486                    }
6487                    write!(f, "{}", quote_ident(n))?;
6488                }
6489                Ok(())
6490            }
6491            Self::DropIndex {
6492                name,
6493                if_exists,
6494                table,
6495            } => {
6496                f.write_str("DROP INDEX ")?;
6497                if *if_exists {
6498                    f.write_str("IF EXISTS ")?;
6499                }
6500                write!(f, "{}", quote_ident(name))?;
6501                if let Some(t) = table {
6502                    write!(f, " ON {}", quote_ident(t))?;
6503                }
6504                Ok(())
6505            }
6506            Self::Select(s) => s.fmt(f),
6507            Self::CreateTable(s) => s.fmt(f),
6508            Self::CreateIndex(s) => s.fmt(f),
6509            Self::Insert(s) => s.fmt(f),
6510            Self::Update(s) => s.fmt(f),
6511            Self::Delete(s) => s.fmt(f),
6512            Self::Merge(s) => s.fmt(f),
6513            Self::Vacuum { table, analyze } => {
6514                f.write_str("VACUUM")?;
6515                if *analyze {
6516                    f.write_str(" ANALYZE")?;
6517                }
6518                if let Some(t) = table {
6519                    write!(f, " {}", quote_ident(t))?;
6520                }
6521                Ok(())
6522            }
6523            Self::Begin(modes) => {
6524                f.write_str("BEGIN")?;
6525                if let Some(level) = modes.isolation {
6526                    write!(f, " ISOLATION LEVEL {level}")?;
6527                }
6528                match modes.read_only {
6529                    Some(true) => f.write_str(" READ ONLY")?,
6530                    Some(false) => f.write_str(" READ WRITE")?,
6531                    None => {}
6532                }
6533                Ok(())
6534            }
6535            Self::Commit => f.write_str("COMMIT"),
6536            Self::Rollback => f.write_str("ROLLBACK"),
6537            Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
6538            Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
6539            Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
6540            Self::ShowTables => f.write_str("SHOW TABLES"),
6541            Self::ShowDatabases => f.write_str("SHOW DATABASES"),
6542            Self::UseDatabase(n) => write!(f, "USE {}", quote_ident(n)),
6543            Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
6544            Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
6545            Self::ShowStatus => f.write_str("SHOW STATUS"),
6546            Self::ShowVariables => f.write_str("SHOW VARIABLES"),
6547            Self::ShowVariablesLike(p) => {
6548                write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
6549            }
6550            Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
6551            Self::Discard(t) => write!(f, "DISCARD {t}"),
6552            Self::Kill { query_only, id } => {
6553                if *query_only {
6554                    write!(f, "KILL QUERY {id}")
6555                } else {
6556                    write!(f, "KILL CONNECTION {id}")
6557                }
6558            }
6559            Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
6560            Self::CreateUser(s) => write!(
6561                f,
6562                "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
6563                quote_ident(&s.name),
6564                s.role
6565            ),
6566            Self::DropUser { name, if_exists } => {
6567                let ie = if *if_exists { "IF EXISTS " } else { "" };
6568                write!(f, "DROP USER {ie}{}", quote_ident(name))
6569            }
6570            Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
6571            Self::SetRole(None) => f.write_str("RESET ROLE"),
6572            Self::Grant(g) => write!(f, "{}", g.render(true)),
6573            Self::Revoke(g) => write!(f, "{}", g.render(false)),
6574            Self::CreatePolicy(s) => {
6575                write!(
6576                    f,
6577                    "CREATE POLICY {} ON {}",
6578                    quote_ident(&s.name),
6579                    quote_ident(&s.table)
6580                )?;
6581                if !s.permissive {
6582                    f.write_str(" AS RESTRICTIVE")?;
6583                }
6584                if !matches!(s.cmd, PolicyCmd::All) {
6585                    let w = match s.cmd {
6586                        PolicyCmd::Select => "SELECT",
6587                        PolicyCmd::Insert => "INSERT",
6588                        PolicyCmd::Update => "UPDATE",
6589                        PolicyCmd::Delete => "DELETE",
6590                        PolicyCmd::All => unreachable!(),
6591                    };
6592                    write!(f, " FOR {w}")?;
6593                }
6594                if !s.roles.is_empty() {
6595                    write!(f, " TO {}", s.roles.join(", "))?;
6596                }
6597                if let Some(u) = &s.using {
6598                    write!(f, " USING ({u})")?;
6599                }
6600                if let Some(c) = &s.with_check {
6601                    write!(f, " WITH CHECK ({c})")?;
6602                }
6603                Ok(())
6604            }
6605            Self::AlterPolicy(s) => {
6606                write!(
6607                    f,
6608                    "ALTER POLICY {} ON {}",
6609                    quote_ident(&s.name),
6610                    quote_ident(&s.table)
6611                )?;
6612                if let Some(nn) = &s.rename_to {
6613                    return write!(f, " RENAME TO {}", quote_ident(nn));
6614                }
6615                if let Some(roles) = &s.roles {
6616                    write!(f, " TO {}", roles.join(", "))?;
6617                }
6618                if let Some(u) = &s.using {
6619                    write!(f, " USING ({u})")?;
6620                }
6621                if let Some(c) = &s.with_check {
6622                    write!(f, " WITH CHECK ({c})")?;
6623                }
6624                Ok(())
6625            }
6626            Self::DropPolicy(s) => {
6627                f.write_str("DROP POLICY ")?;
6628                if s.if_exists {
6629                    f.write_str("IF EXISTS ")?;
6630                }
6631                write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
6632            }
6633            Self::ShowUsers => f.write_str("SHOW USERS"),
6634            Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
6635            Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
6636            Self::CreateSubscription(s) => {
6637                write!(
6638                    f,
6639                    "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
6640                    quote_ident(&s.name),
6641                    s.conn_str.replace('\'', "''")
6642                )?;
6643                for (i, p) in s.publications.iter().enumerate() {
6644                    if i > 0 {
6645                        f.write_str(", ")?;
6646                    }
6647                    write!(f, "{}", quote_ident(p))?;
6648                }
6649                Ok(())
6650            }
6651            Self::DropSubscription { name, if_exists } => {
6652                let opt = if *if_exists { "IF EXISTS " } else { "" };
6653                write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
6654            }
6655            Self::WaitForWalPosition { pos, timeout_ms } => {
6656                write!(f, "WAIT FOR WAL POSITION {pos}")?;
6657                if let Some(ms) = timeout_ms {
6658                    write!(f, " WITH TIMEOUT {ms}")?;
6659                }
6660                Ok(())
6661            }
6662            Self::RenameTables(pairs) => {
6663                f.write_str("RENAME TABLE ")?;
6664                for (i, (from, to)) in pairs.iter().enumerate() {
6665                    if i > 0 {
6666                        f.write_str(", ")?;
6667                    }
6668                    write!(f, "{} TO {}", quote_ident(from), quote_ident(to))?;
6669                }
6670                Ok(())
6671            }
6672            Self::Analyze(None) => f.write_str("ANALYZE"),
6673            Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
6674            Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
6675            Self::Explain(e) => {
6676                if e.suggest {
6677                    write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
6678                } else if e.analyze {
6679                    write!(f, "EXPLAIN ANALYZE {}", e.inner)
6680                } else {
6681                    write!(f, "EXPLAIN {}", e.inner)
6682                }
6683            }
6684            Self::AlterIndex(a) => {
6685                write!(f, "ALTER INDEX ")?;
6686                match &a.target {
6687                    // Parameters are consumed, not stored; the shortest
6688                    // faithful spelling.
6689                    AlterIndexTarget::StorageParams => {
6690                        write!(f, "{} SET ()", quote_ident(&a.name))
6691                    }
6692                    AlterIndexTarget::Rebuild { encoding } => {
6693                        write!(f, "{} REBUILD", quote_ident(&a.name))?;
6694                        if let Some(enc) = encoding {
6695                            write!(f, " WITH (encoding = {enc})")?;
6696                        }
6697                        Ok(())
6698                    }
6699                    AlterIndexTarget::Rename { new, if_exists } => {
6700                        if *if_exists {
6701                            f.write_str("IF EXISTS ")?;
6702                        }
6703                        write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
6704                    }
6705                }
6706            }
6707            Self::AlterTable(a) => {
6708                write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
6709                for (i, t) in a.targets.iter().enumerate() {
6710                    if i > 0 {
6711                        f.write_str(", ")?;
6712                    }
6713                    fmt_alter_target(f, t)?;
6714                }
6715                Ok(())
6716            }
6717            Self::CreatePublication(p) => {
6718                write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
6719                match &p.scope {
6720                    PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
6721                    PublicationScope::ForTables(ts) => {
6722                        f.write_str(" FOR TABLE ")?;
6723                        for (i, t) in ts.iter().enumerate() {
6724                            if i > 0 {
6725                                f.write_str(", ")?;
6726                            }
6727                            write!(f, "{}", quote_ident(t))?;
6728                        }
6729                        Ok(())
6730                    }
6731                    PublicationScope::TablesInSchema(schema) => {
6732                        write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
6733                        Ok(())
6734                    }
6735                    PublicationScope::AllTablesExcept(ts) => {
6736                        f.write_str(" FOR ALL TABLES EXCEPT ")?;
6737                        for (i, t) in ts.iter().enumerate() {
6738                            if i > 0 {
6739                                f.write_str(", ")?;
6740                            }
6741                            write!(f, "{}", quote_ident(t))?;
6742                        }
6743                        Ok(())
6744                    }
6745                }
6746            }
6747            Self::CreateExtension(name) => {
6748                write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
6749            }
6750            Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
6751            Self::DropPublication { name, if_exists } => {
6752                let opt = if *if_exists { "IF EXISTS " } else { "" };
6753                write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
6754            }
6755            Self::SetParameter { name, value, local } => {
6756                write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
6757                match value {
6758                    SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
6759                    SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
6760                    SetValue::Default => f.write_str("DEFAULT"),
6761                    SetValue::Null => f.write_str("NULL"),
6762                }
6763            }
6764            Self::SetTransaction { modes } => {
6765                f.write_str("SET TRANSACTION")?;
6766                if let Some(isolation) = modes.isolation {
6767                    let name = match isolation {
6768                        IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
6769                        IsolationLevel::ReadCommitted => "READ COMMITTED",
6770                        IsolationLevel::RepeatableRead => "REPEATABLE READ",
6771                        IsolationLevel::Serializable => "SERIALIZABLE",
6772                    };
6773                    write!(f, " ISOLATION LEVEL {name}")?;
6774                }
6775                match modes.read_only {
6776                    Some(true) => f.write_str(" READ ONLY")?,
6777                    Some(false) => f.write_str(" READ WRITE")?,
6778                    None => {}
6779                }
6780                Ok(())
6781            }
6782            Self::ShowParameter(name) => write!(f, "SHOW {name}"),
6783            Self::SetUserVars(assigns, _) => {
6784                f.write_str("SET ")?;
6785                for (i, (name, value)) in assigns.iter().enumerate() {
6786                    if i > 0 {
6787                        f.write_str(", ")?;
6788                    }
6789                    write!(f, "@{name} = {value}")?;
6790                }
6791                Ok(())
6792            }
6793            Self::SetParameterList(pairs) => {
6794                f.write_str("SET ")?;
6795                for (i, (name, value)) in pairs.iter().enumerate() {
6796                    if i > 0 {
6797                        f.write_str(", ")?;
6798                    }
6799                    write!(f, "{name} = ")?;
6800                    match value {
6801                        SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
6802                        SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
6803                        SetValue::Default => f.write_str("DEFAULT")?,
6804                        SetValue::Null => f.write_str("NULL")?,
6805                    }
6806                }
6807                Ok(())
6808            }
6809            Self::ResetParameter(None) => f.write_str("RESET ALL"),
6810            Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
6811            Self::CreateFunction(s) => s.fmt(f),
6812            Self::CreateTrigger(s) => s.fmt(f),
6813            Self::DropTrigger {
6814                name,
6815                table,
6816                if_exists,
6817            } => {
6818                f.write_str("DROP TRIGGER ")?;
6819                if *if_exists {
6820                    f.write_str("IF EXISTS ")?;
6821                }
6822                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6823            }
6824            Self::DropFunction {
6825                name,
6826                args,
6827                if_exists,
6828            } => {
6829                f.write_str("DROP FUNCTION ")?;
6830                if *if_exists {
6831                    f.write_str("IF EXISTS ")?;
6832                }
6833                write!(f, "{}", quote_ident(name))?;
6834                if let Some(a) = args {
6835                    write!(f, "({})", a.join(", "))?;
6836                }
6837                Ok(())
6838            }
6839            Self::CreateSequence(s) => s.fmt(f),
6840            Self::AlterSequence(s) => s.fmt(f),
6841            Self::DropSequence { names, if_exists } => {
6842                f.write_str("DROP SEQUENCE ")?;
6843                if *if_exists {
6844                    f.write_str("IF EXISTS ")?;
6845                }
6846                for (i, n) in names.iter().enumerate() {
6847                    if i > 0 {
6848                        f.write_str(", ")?;
6849                    }
6850                    write!(f, "{}", quote_ident(n))?;
6851                }
6852                Ok(())
6853            }
6854            Self::CreateView(v) => v.fmt(f),
6855            Self::DropView { names, if_exists } => {
6856                f.write_str("DROP VIEW ")?;
6857                if *if_exists {
6858                    f.write_str("IF EXISTS ")?;
6859                }
6860                for (i, n) in names.iter().enumerate() {
6861                    if i > 0 {
6862                        f.write_str(", ")?;
6863                    }
6864                    write!(f, "{}", quote_ident(n))?;
6865                }
6866                Ok(())
6867            }
6868            Self::CreateMaterializedView(v) => v.fmt(f),
6869            Self::RefreshMaterializedView { name, with_data } => {
6870                write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6871                if !*with_data {
6872                    f.write_str(" WITH NO DATA")?;
6873                }
6874                Ok(())
6875            }
6876            Self::DropMaterializedView { names, if_exists } => {
6877                f.write_str("DROP MATERIALIZED VIEW ")?;
6878                if *if_exists {
6879                    f.write_str("IF EXISTS ")?;
6880                }
6881                for (i, n) in names.iter().enumerate() {
6882                    if i > 0 {
6883                        f.write_str(", ")?;
6884                    }
6885                    write!(f, "{}", quote_ident(n))?;
6886                }
6887                Ok(())
6888            }
6889            Self::CreateType(t) => t.fmt(f),
6890            Self::CommentOn {
6891                kind,
6892                name,
6893                comment,
6894            } => {
6895                let body = match comment {
6896                    Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6897                    None => "NULL".into(),
6898                };
6899                write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6900            }
6901            Self::AlterTypeRenameValue {
6902                type_name,
6903                old,
6904                new,
6905            } => write!(
6906                f,
6907                "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6908                quote_ident(type_name),
6909                old.replace('\'', "''"),
6910                new.replace('\'', "''")
6911            ),
6912            Self::AlterTypeAddValue {
6913                type_name,
6914                label,
6915                if_not_exists,
6916                position,
6917            } => {
6918                write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6919                if *if_not_exists {
6920                    write!(f, "IF NOT EXISTS ")?;
6921                }
6922                write!(f, "'{label}'")?;
6923                if let Some((is_before, anchor)) = position {
6924                    write!(
6925                        f,
6926                        " {} '{anchor}'",
6927                        if *is_before { "BEFORE" } else { "AFTER" }
6928                    )?;
6929                }
6930                Ok(())
6931            }
6932            Self::DropType { names, if_exists } => {
6933                f.write_str("DROP TYPE ")?;
6934                if *if_exists {
6935                    f.write_str("IF EXISTS ")?;
6936                }
6937                for (i, n) in names.iter().enumerate() {
6938                    if i > 0 {
6939                        f.write_str(", ")?;
6940                    }
6941                    write!(f, "{}", quote_ident(n))?;
6942                }
6943                Ok(())
6944            }
6945            Self::CreateDomain(d) => d.fmt(f),
6946            Self::DropDomain { names, if_exists } => {
6947                f.write_str("DROP DOMAIN ")?;
6948                if *if_exists {
6949                    f.write_str("IF EXISTS ")?;
6950                }
6951                for (i, n) in names.iter().enumerate() {
6952                    if i > 0 {
6953                        f.write_str(", ")?;
6954                    }
6955                    write!(f, "{}", quote_ident(n))?;
6956                }
6957                Ok(())
6958            }
6959            Self::CreateSchema {
6960                name,
6961                if_not_exists,
6962            } => {
6963                f.write_str("CREATE SCHEMA ")?;
6964                if *if_not_exists {
6965                    f.write_str("IF NOT EXISTS ")?;
6966                }
6967                write!(f, "{}", quote_ident(name))
6968            }
6969            Self::DropSchema { names, if_exists } => {
6970                f.write_str("DROP SCHEMA ")?;
6971                if *if_exists {
6972                    f.write_str("IF EXISTS ")?;
6973                }
6974                for (i, n) in names.iter().enumerate() {
6975                    if i > 0 {
6976                        f.write_str(", ")?;
6977                    }
6978                    write!(f, "{}", quote_ident(n))?;
6979                }
6980                Ok(())
6981            }
6982            Self::CreateRule(r) => {
6983                f.write_str("CREATE ")?;
6984                if r.or_replace {
6985                    f.write_str("OR REPLACE ")?;
6986                }
6987                write!(
6988                    f,
6989                    "RULE {} AS ON {} TO {}",
6990                    quote_ident(&r.name),
6991                    r.event,
6992                    quote_ident(&r.table)
6993                )?;
6994                if let Some(w) = &r.when_condition {
6995                    write!(f, " WHERE {w}")?;
6996                }
6997                f.write_str(if r.instead {
6998                    " DO INSTEAD "
6999                } else {
7000                    " DO ALSO "
7001                })?;
7002                if r.commands.is_empty() {
7003                    f.write_str("NOTHING")?;
7004                } else if r.commands.len() == 1 {
7005                    write!(f, "{}", r.commands[0])?;
7006                } else {
7007                    f.write_str("(")?;
7008                    for (i, c) in r.commands.iter().enumerate() {
7009                        if i > 0 {
7010                            f.write_str("; ")?;
7011                        }
7012                        write!(f, "{c}")?;
7013                    }
7014                    f.write_str(")")?;
7015                }
7016                Ok(())
7017            }
7018            Self::DropRule {
7019                name,
7020                table,
7021                if_exists,
7022            } => {
7023                f.write_str("DROP RULE ")?;
7024                if *if_exists {
7025                    f.write_str("IF EXISTS ")?;
7026                }
7027                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
7028            }
7029        }
7030    }
7031}
7032
7033impl fmt::Display for CreateDomainStatement {
7034    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7035        write!(
7036            f,
7037            "CREATE DOMAIN {} AS {}",
7038            quote_ident(&self.name),
7039            self.base_type
7040        )?;
7041        if let Some(d) = &self.default {
7042            write!(f, " DEFAULT {d}")?;
7043        }
7044        if self.not_null {
7045            f.write_str(" NOT NULL")?;
7046        }
7047        for c in &self.checks {
7048            write!(f, " CHECK ({c})")?;
7049        }
7050        Ok(())
7051    }
7052}
7053
7054impl fmt::Display for CreateTypeStatement {
7055    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7056        write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
7057        match &self.kind {
7058            TypeKind::Enum { labels } => {
7059                f.write_str("ENUM (")?;
7060                for (i, l) in labels.iter().enumerate() {
7061                    if i > 0 {
7062                        f.write_str(", ")?;
7063                    }
7064                    write!(f, "'{}'", l.replace('\'', "''"))?;
7065                }
7066                f.write_str(")")
7067            }
7068            TypeKind::Composite { fields, .. } => {
7069                f.write_str("(")?;
7070                for (i, (n, t)) in fields.iter().enumerate() {
7071                    if i > 0 {
7072                        f.write_str(", ")?;
7073                    }
7074                    write!(f, "{} {}", quote_ident(n), t)?;
7075                }
7076                f.write_str(")")
7077            }
7078        }
7079    }
7080}
7081
7082impl fmt::Display for CreateMaterializedViewStatement {
7083    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7084        f.write_str("CREATE MATERIALIZED VIEW ")?;
7085        if self.if_not_exists {
7086            f.write_str("IF NOT EXISTS ")?;
7087        }
7088        write!(f, "{}", quote_ident(&self.name))?;
7089        if !self.columns.is_empty() {
7090            f.write_str(" (")?;
7091            for (i, c) in self.columns.iter().enumerate() {
7092                if i > 0 {
7093                    f.write_str(", ")?;
7094                }
7095                write!(f, "{}", quote_ident(c))?;
7096            }
7097            f.write_str(")")?;
7098        }
7099        write!(f, " AS {}", self.body)?;
7100        if !self.with_data {
7101            f.write_str(" WITH NO DATA")?;
7102        }
7103        Ok(())
7104    }
7105}
7106
7107impl fmt::Display for CreateViewStatement {
7108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7109        f.write_str("CREATE ")?;
7110        if self.or_replace {
7111            f.write_str("OR REPLACE ")?;
7112        }
7113        if self.temporary {
7114            f.write_str("TEMPORARY ")?;
7115        }
7116        f.write_str("VIEW ")?;
7117        if self.if_not_exists {
7118            f.write_str("IF NOT EXISTS ")?;
7119        }
7120        write!(f, "{}", quote_ident(&self.name))?;
7121        if !self.columns.is_empty() {
7122            f.write_str(" (")?;
7123            for (i, c) in self.columns.iter().enumerate() {
7124                if i > 0 {
7125                    f.write_str(", ")?;
7126                }
7127                write!(f, "{}", quote_ident(c))?;
7128            }
7129            f.write_str(")")?;
7130        }
7131        write!(f, " AS {}", self.body)?;
7132        match self.check_option {
7133            Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
7134            Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
7135            None => Ok(()),
7136        }
7137    }
7138}
7139
7140impl fmt::Display for CreateSequenceStatement {
7141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7142        f.write_str("CREATE ")?;
7143        if self.temporary {
7144            f.write_str("TEMPORARY ")?;
7145        }
7146        f.write_str("SEQUENCE ")?;
7147        if self.if_not_exists {
7148            f.write_str("IF NOT EXISTS ")?;
7149        }
7150        write!(f, "{}", quote_ident(&self.name))?;
7151        if let Some(dt) = self.data_type {
7152            write!(f, " AS {dt}")?;
7153        }
7154        write_sequence_options(f, &self.options)
7155    }
7156}
7157
7158impl fmt::Display for AlterSequenceStatement {
7159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7160        f.write_str("ALTER SEQUENCE ")?;
7161        if self.if_exists {
7162            f.write_str("IF EXISTS ")?;
7163        }
7164        write!(f, "{}", quote_ident(&self.name))?;
7165        write_sequence_options(f, &self.options)
7166    }
7167}
7168
7169impl fmt::Display for SequenceDataType {
7170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7171        f.write_str(match self {
7172            Self::SmallInt => "smallint",
7173            Self::Int => "integer",
7174            Self::BigInt => "bigint",
7175        })
7176    }
7177}
7178
7179fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
7180    if let Some(n) = o.increment {
7181        write!(f, " INCREMENT BY {n}")?;
7182    }
7183    match o.min_value {
7184        Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
7185        Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
7186        None => {}
7187    }
7188    match o.max_value {
7189        Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
7190        Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
7191        None => {}
7192    }
7193    if let Some(n) = o.start {
7194        write!(f, " START WITH {n}")?;
7195    }
7196    match o.restart {
7197        Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
7198        Some(None) => f.write_str(" RESTART")?,
7199        None => {}
7200    }
7201    if let Some(n) = o.cache {
7202        write!(f, " CACHE {n}")?;
7203    }
7204    match o.cycle {
7205        Some(true) => f.write_str(" CYCLE")?,
7206        Some(false) => f.write_str(" NO CYCLE")?,
7207        None => {}
7208    }
7209    if let Some(ob) = &o.owned_by {
7210        match ob {
7211            SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
7212            SequenceOwnedBy::Column { table, column } => {
7213                write!(
7214                    f,
7215                    " OWNED BY {}.{}",
7216                    quote_ident(table),
7217                    quote_ident(column)
7218                )?;
7219            }
7220        }
7221    }
7222    Ok(())
7223}
7224
7225impl fmt::Display for CreateFunctionStatement {
7226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7227        f.write_str("CREATE ")?;
7228        if self.or_replace {
7229            f.write_str("OR REPLACE ")?;
7230        }
7231        write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
7232        for (i, arg) in self.args.iter().enumerate() {
7233            if i > 0 {
7234                f.write_str(", ")?;
7235            }
7236            match arg.mode {
7237                FunctionArgMode::In => {}
7238                FunctionArgMode::Out => f.write_str("OUT ")?,
7239                FunctionArgMode::InOut => f.write_str("INOUT ")?,
7240            }
7241            if let Some(name) = &arg.name {
7242                write!(f, "{} ", quote_ident(name))?;
7243            }
7244            match &arg.ty {
7245                FunctionArgType::Typed(t) => write!(f, "{t}")?,
7246                FunctionArgType::Raw(s) => f.write_str(s)?,
7247            }
7248        }
7249        f.write_str(") RETURNS ")?;
7250        match &self.returns {
7251            FunctionReturn::Trigger => f.write_str("TRIGGER")?,
7252            FunctionReturn::Void => f.write_str("VOID")?,
7253            FunctionReturn::Type(t) => write!(f, "{t}")?,
7254            FunctionReturn::Other(s) => f.write_str(s)?,
7255        }
7256        write!(f, " LANGUAGE {} AS $$", self.language)?;
7257        match &self.body {
7258            FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
7259            FunctionBody::Raw(s) => f.write_str(s)?,
7260        }
7261        f.write_str("$$")
7262    }
7263}
7264
7265impl fmt::Display for PlPgSqlBlock {
7266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7267        if !self.declarations.is_empty() {
7268            f.write_str("DECLARE\n")?;
7269            for d in &self.declarations {
7270                write!(f, "  {} ", quote_ident(&d.name))?;
7271                match &d.ty {
7272                    FunctionArgType::Typed(t) => write!(f, "{t}")?,
7273                    FunctionArgType::Raw(s) => f.write_str(s)?,
7274                }
7275                if let Some(e) = &d.default {
7276                    write!(f, " := {e}")?;
7277                }
7278                f.write_str(";\n")?;
7279            }
7280        }
7281        f.write_str("BEGIN\n")?;
7282        for stmt in &self.statements {
7283            writeln!(f, "  {stmt};")?;
7284        }
7285        // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
7286        // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
7287        // parsed block through it — so every exception handler a function
7288        // declared was thrown away AT STORE TIME. The block executed fine while
7289        // it was still an AST (a DO block never round-trips through text), which
7290        // is why only functions and triggers lost theirs.
7291        if !self.exception_handlers.is_empty() {
7292            f.write_str("EXCEPTION\n")?;
7293            for h in &self.exception_handlers {
7294                writeln!(f, "  WHEN {} THEN", h.conditions.join(" OR "))?;
7295                for stmt in &h.body {
7296                    writeln!(f, "    {stmt};")?;
7297                }
7298            }
7299        }
7300        f.write_str("END")
7301    }
7302}
7303
7304impl fmt::Display for PlPgSqlStmt {
7305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7306        match self {
7307            Self::Assign { target, value } => write!(f, "{target} := {value}"),
7308            Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
7309            Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
7310            Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
7311            Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
7312            Self::Return(t) => match t {
7313                ReturnTarget::New => f.write_str("RETURN NEW"),
7314                ReturnTarget::Old => f.write_str("RETURN OLD"),
7315                ReturnTarget::Null => f.write_str("RETURN NULL"),
7316                ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
7317            },
7318            Self::If {
7319                branches,
7320                else_branch,
7321            } => {
7322                for (i, (cond, body)) in branches.iter().enumerate() {
7323                    if i == 0 {
7324                        write!(f, "IF {cond} THEN ")?;
7325                    } else {
7326                        write!(f, " ELSIF {cond} THEN ")?;
7327                    }
7328                    for (j, s) in body.iter().enumerate() {
7329                        if j > 0 {
7330                            f.write_str("; ")?;
7331                        }
7332                        write!(f, "{s}")?;
7333                    }
7334                }
7335                if !else_branch.is_empty() {
7336                    f.write_str(" ELSE ")?;
7337                    for (j, s) in else_branch.iter().enumerate() {
7338                        if j > 0 {
7339                            f.write_str("; ")?;
7340                        }
7341                        write!(f, "{s}")?;
7342                    }
7343                }
7344                f.write_str(" END IF")
7345            }
7346            Self::Raise {
7347                level,
7348                message,
7349                args,
7350            } => {
7351                let lvl = match level {
7352                    RaiseLevel::Notice => "NOTICE",
7353                    RaiseLevel::Warning => "WARNING",
7354                    RaiseLevel::Info => "INFO",
7355                    RaiseLevel::Log => "LOG",
7356                    RaiseLevel::Debug => "DEBUG",
7357                    RaiseLevel::Exception => "EXCEPTION",
7358                };
7359                write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
7360                for a in args {
7361                    write!(f, ", {a}")?;
7362                }
7363                Ok(())
7364            }
7365            Self::EmbeddedSql(s) => write!(f, "{s}"),
7366            Self::Assert { condition, message } => {
7367                write!(f, "ASSERT {condition}")?;
7368                if let Some(m) = message {
7369                    write!(f, ", {m}")?;
7370                }
7371                Ok(())
7372            }
7373            Self::While { condition, body } => {
7374                writeln!(f, "WHILE {condition} LOOP")?;
7375                for s in body {
7376                    writeln!(f, "  {s};")?;
7377                }
7378                f.write_str("END LOOP")
7379            }
7380            Self::ForRange {
7381                var,
7382                start,
7383                end,
7384                reverse,
7385                body,
7386            } => {
7387                write!(f, "FOR {var} IN ")?;
7388                if *reverse {
7389                    f.write_str("REVERSE ")?;
7390                }
7391                writeln!(f, "{start}..{end} LOOP")?;
7392                for s in body {
7393                    writeln!(f, "  {s};")?;
7394                }
7395                f.write_str("END LOOP")
7396            }
7397            Self::Loop { body } => {
7398                writeln!(f, "LOOP")?;
7399                for s in body {
7400                    writeln!(f, "  {s};")?;
7401                }
7402                f.write_str("END LOOP")
7403            }
7404            Self::Exit { when } => {
7405                f.write_str("EXIT")?;
7406                if let Some(c) = when {
7407                    write!(f, " WHEN {c}")?;
7408                }
7409                Ok(())
7410            }
7411            Self::Continue { when } => {
7412                f.write_str("CONTINUE")?;
7413                if let Some(c) = when {
7414                    write!(f, " WHEN {c}")?;
7415                }
7416                Ok(())
7417            }
7418            Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
7419            Self::ForQuery { var, query, body } => {
7420                writeln!(f, "FOR {var} IN ({query}) LOOP")?;
7421                for s in body {
7422                    writeln!(f, "  {s};")?;
7423                }
7424                f.write_str("END LOOP")
7425            }
7426            Self::ForExecute {
7427                var,
7428                sql_expr,
7429                body,
7430            } => {
7431                writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
7432                for s in body {
7433                    writeln!(f, "  {s};")?;
7434                }
7435                f.write_str("END LOOP")
7436            }
7437        }
7438    }
7439}
7440
7441impl fmt::Display for AssignTarget {
7442    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7443        match self {
7444            Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
7445            Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
7446            Self::Local(n) => f.write_str(n),
7447        }
7448    }
7449}
7450
7451impl fmt::Display for CreateTriggerStatement {
7452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7453        f.write_str("CREATE ")?;
7454        if self.or_replace {
7455            f.write_str("OR REPLACE ")?;
7456        }
7457        write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
7458        match self.timing {
7459            TriggerTiming::Before => f.write_str("BEFORE")?,
7460            TriggerTiming::After => f.write_str("AFTER")?,
7461            TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
7462        }
7463        for (i, e) in self.events.iter().enumerate() {
7464            if i == 0 {
7465                f.write_str(" ")?;
7466            } else {
7467                f.write_str(" OR ")?;
7468            }
7469            match e {
7470                TriggerEvent::Insert => f.write_str("INSERT")?,
7471                TriggerEvent::Update => {
7472                    f.write_str("UPDATE")?;
7473                    if !self.update_columns.is_empty() {
7474                        f.write_str(" OF ")?;
7475                        for (j, col) in self.update_columns.iter().enumerate() {
7476                            if j > 0 {
7477                                f.write_str(", ")?;
7478                            }
7479                            f.write_str(&quote_ident(col))?;
7480                        }
7481                    }
7482                }
7483                TriggerEvent::Delete => f.write_str("DELETE")?,
7484                TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
7485            }
7486        }
7487        write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
7488        match self.for_each {
7489            TriggerForEach::Row => f.write_str("ROW")?,
7490            TriggerForEach::Statement => f.write_str("STATEMENT")?,
7491        }
7492        write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
7493    }
7494}
7495
7496impl fmt::Display for CreateIndexStatement {
7497    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7498        if self.is_unique {
7499            f.write_str("CREATE UNIQUE INDEX ")?;
7500        } else {
7501            f.write_str("CREATE INDEX ")?;
7502        }
7503        if self.if_not_exists {
7504            f.write_str("IF NOT EXISTS ")?;
7505        }
7506        write!(
7507            f,
7508            "{} ON {} ",
7509            quote_ident(&self.name),
7510            quote_ident(&self.table)
7511        )?;
7512        match self.method {
7513            IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
7514            IndexMethod::Brin => f.write_str("USING brin ")?,
7515            IndexMethod::Gin => f.write_str("USING gin ")?,
7516            IndexMethod::BTree => {}
7517        }
7518        if let Some(expr) = &self.expression {
7519            write!(f, "({})", expr)?;
7520        } else if self.extra_columns.is_empty() {
7521            // v7.15.0 — preserve operator class on round-trip
7522            // (`(col opclass)`) so WAL replay reconstructs the
7523            // engine-routing intent (e.g. `gin_trgm_ops` →
7524            // trigram-GIN build path).
7525            if let Some(op) = &self.opclass {
7526                write!(f, "({} {})", quote_ident(&self.column), op)?;
7527            } else {
7528                write!(f, "({})", quote_ident(&self.column))?;
7529            }
7530        } else {
7531            // v7.9.14 — multi-column key. Emit each column quoted
7532            // so the round-tripped form re-parses to identical AST.
7533            f.write_str("(")?;
7534            write!(f, "{}", quote_ident(&self.column))?;
7535            for c in &self.extra_columns {
7536                write!(f, ", {}", quote_ident(c))?;
7537            }
7538            f.write_str(")")?;
7539        }
7540        if !self.included_columns.is_empty() {
7541            f.write_str(" INCLUDE (")?;
7542            for (i, c) in self.included_columns.iter().enumerate() {
7543                if i > 0 {
7544                    f.write_str(", ")?;
7545                }
7546                write!(f, "{}", quote_ident(c))?;
7547            }
7548            f.write_str(")")?;
7549        }
7550        if let Some(pred) = &self.partial_predicate {
7551            write!(f, " WHERE {}", pred)?;
7552        }
7553        Ok(())
7554    }
7555}
7556
7557impl fmt::Display for CreateTableStatement {
7558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7559        f.write_str("CREATE TABLE ")?;
7560        if self.if_not_exists {
7561            f.write_str("IF NOT EXISTS ")?;
7562        }
7563        write!(f, "{}", quote_ident(&self.name))?;
7564        // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
7565        // no column list and no constraints; the table inherits its
7566        // columns from the parent at engine-DDL time.
7567        if let Some(spec) = &self.partition_of {
7568            write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
7569            return match &spec.bounds {
7570                PartitionOfBoundsAst::Range { lower, upper } => {
7571                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7572                }
7573                PartitionOfBoundsAst::List { values } => {
7574                    f.write_str("FOR VALUES IN (")?;
7575                    for (i, v) in values.iter().enumerate() {
7576                        if i > 0 {
7577                            f.write_str(", ")?;
7578                        }
7579                        write!(f, "{}", v)?;
7580                    }
7581                    f.write_str(")")
7582                }
7583                PartitionOfBoundsAst::Hash { modulus, remainder } => {
7584                    write!(
7585                        f,
7586                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7587                        modulus, remainder
7588                    )
7589                }
7590                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7591            };
7592        }
7593        f.write_str(" (")?;
7594        for (i, col) in self.columns.iter().enumerate() {
7595            if i > 0 {
7596                f.write_str(", ")?;
7597            }
7598            write!(f, "{col}")?;
7599        }
7600        // v7.6.0 — render FK constraints in table-level form, after
7601        // the column list. WAL replay round-trips through Display, so
7602        // every FK must serialise here for replay to reconstruct the
7603        // schema bit-for-bit.
7604        for fk in &self.foreign_keys {
7605            f.write_str(", ")?;
7606            write!(f, "{fk}")?;
7607        }
7608        // v7.13.0 — render table-level constraints (PRIMARY KEY /
7609        // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
7610        // column-level UNIQUE / CHECK get lifted to this list at
7611        // parse time, so emitting only here avoids double-counting.
7612        for tc in &self.table_constraints {
7613            f.write_str(", ")?;
7614            write!(f, "{tc}")?;
7615        }
7616        f.write_str(")")?;
7617        // v7.37.6-B — partition-parent suffix renders after the
7618        // closing column-list paren, before the optional MySQL
7619        // table-options tail (which Display doesn't currently emit).
7620        if let Some(spec) = &self.partition_by {
7621            f.write_str(" PARTITION BY ")?;
7622            match spec.kind {
7623                PartitionKindAst::Range => f.write_str("RANGE ")?,
7624                PartitionKindAst::List => f.write_str("LIST ")?,
7625                PartitionKindAst::Hash => f.write_str("HASH ")?,
7626            }
7627            f.write_str("(")?;
7628            for (i, col) in spec.key_columns.iter().enumerate() {
7629                if i > 0 {
7630                    f.write_str(", ")?;
7631                }
7632                f.write_str(&quote_ident(col))?;
7633            }
7634            f.write_str(")")?;
7635        }
7636        Ok(())
7637    }
7638}
7639
7640fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
7641    match t {
7642        AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
7643        AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
7644            write!(f, "REPLICA IDENTITY USING INDEX {index}")
7645        }
7646        AlterTableTarget::Inherit { parent, detach } => {
7647            if *detach {
7648                write!(f, "NO INHERIT {parent}")
7649            } else {
7650                write!(f, "INHERIT {parent}")
7651            }
7652        }
7653        AlterTableTarget::SetHotTierBytes(n) => {
7654            write!(f, "SET hot_tier_bytes = {n}")
7655        }
7656        AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
7657        AlterTableTarget::DropForeignKey { name, if_exists } => {
7658            f.write_str("DROP CONSTRAINT ")?;
7659            if *if_exists {
7660                f.write_str("IF EXISTS ")?;
7661            }
7662            write!(f, "{}", quote_ident(name))
7663        }
7664        AlterTableTarget::DropIndex { name, if_exists } => {
7665            f.write_str("DROP INDEX ")?;
7666            if *if_exists {
7667                f.write_str("IF EXISTS ")?;
7668            }
7669            write!(f, "{}", quote_ident(name))
7670        }
7671        AlterTableTarget::ModifyColumn {
7672            column,
7673            rename_to,
7674            definition,
7675            position,
7676        } => {
7677            if let Some(new) = rename_to {
7678                write!(
7679                    f,
7680                    "CHANGE COLUMN {} {} {}",
7681                    quote_ident(column),
7682                    quote_ident(new),
7683                    definition.ty
7684                )?;
7685            } else {
7686                write!(f, "MODIFY COLUMN {} {}", quote_ident(column), definition.ty)?;
7687            }
7688            if !definition.nullable {
7689                f.write_str(" NOT NULL")?;
7690            }
7691            write_column_position(f, position.as_ref())
7692        }
7693        AlterTableTarget::RenameIndex { old, new } => {
7694            write!(
7695                f,
7696                "RENAME INDEX {} TO {}",
7697                quote_ident(old),
7698                quote_ident(new)
7699            )
7700        }
7701        AlterTableTarget::SetTableAutoIncrement(n) => write!(f, "AUTO_INCREMENT = {n}"),
7702        AlterTableTarget::SetEngine(name) => write!(f, "ENGINE = {name}"),
7703        AlterTableTarget::ConvertToCharacterSet { charset, collate } => {
7704            write!(f, "CONVERT TO CHARACTER SET {charset}")?;
7705            if let Some(c) = collate {
7706                write!(f, " COLLATE {c}")?;
7707            }
7708            Ok(())
7709        }
7710        AlterTableTarget::AddColumn {
7711            column,
7712            if_not_exists,
7713            position,
7714        } => {
7715            f.write_str("ADD COLUMN ")?;
7716            if *if_not_exists {
7717                f.write_str("IF NOT EXISTS ")?;
7718            }
7719            write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
7720            if !column.nullable {
7721                f.write_str(" NOT NULL")?;
7722            }
7723            if let Some(d) = &column.default {
7724                write!(f, " DEFAULT {d}")?;
7725            }
7726            if column.auto_increment {
7727                f.write_str(" AUTO_INCREMENT")?;
7728            }
7729            if column.is_primary_key {
7730                f.write_str(" PRIMARY KEY")?;
7731            }
7732            Ok(())
7733        }
7734        AlterTableTarget::AlterColumnType {
7735            column,
7736            new_type,
7737            using,
7738            collation,
7739        } => {
7740            write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
7741            if let Some((_, name)) = collation {
7742                write!(f, " COLLATE {}", quote_ident(name))?;
7743            }
7744            if let Some(u) = using {
7745                write!(f, " USING {u}")?;
7746            }
7747            Ok(())
7748        }
7749        AlterTableTarget::DropColumn {
7750            column,
7751            if_exists,
7752            cascade,
7753        } => {
7754            f.write_str("DROP COLUMN ")?;
7755            if *if_exists {
7756                f.write_str("IF EXISTS ")?;
7757            }
7758            write!(f, "{}", quote_ident(column))?;
7759            if *cascade {
7760                f.write_str(" CASCADE")?;
7761            }
7762            Ok(())
7763        }
7764        AlterTableTarget::AddTableConstraint(tc) => {
7765            write!(f, "ADD {tc}")
7766        }
7767        AlterTableTarget::ValidateConstraint { name } => {
7768            write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
7769        }
7770        AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
7771        AlterTableTarget::ClusterOn { index } => match index {
7772            Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
7773            None => f.write_str("SET WITHOUT CLUSTER"),
7774        },
7775        AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
7776            // Round-trip-safe spelling: re-parsing this form lowers
7777            // back to SetColumnAutoIncrement (the nextval default is
7778            // how pg_dump says "serial").
7779            let seq = seq_name
7780                .clone()
7781                .unwrap_or_else(|| alloc::format!("{column}_seq"));
7782            write!(
7783                f,
7784                "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
7785                quote_ident(column)
7786            )
7787        }
7788        AlterTableTarget::RenameColumn { old, new } => {
7789            write!(
7790                f,
7791                "RENAME COLUMN {} TO {}",
7792                quote_ident(old),
7793                quote_ident(new)
7794            )
7795        }
7796        AlterTableTarget::RenameConstraint { old, new } => {
7797            write!(
7798                f,
7799                "RENAME CONSTRAINT {} TO {}",
7800                quote_ident(old),
7801                quote_ident(new)
7802            )
7803        }
7804        AlterTableTarget::RenameTable { new } => {
7805            write!(f, "RENAME TO {}", quote_ident(new))
7806        }
7807        AlterTableTarget::SetTriggerEnabled { which, enabled } => {
7808            f.write_str(if *enabled {
7809                "ENABLE TRIGGER "
7810            } else {
7811                "DISABLE TRIGGER "
7812            })?;
7813            match which {
7814                TriggerSelector::All => f.write_str("ALL"),
7815                TriggerSelector::Named(n) => f.write_str(&quote_ident(n)),
7816            }
7817        }
7818        AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
7819            (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
7820            (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
7821            (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
7822            (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
7823            (None, None) => Ok(()),
7824        },
7825        AlterTableTarget::AttachPartition { child, bounds } => {
7826            write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
7827            match bounds {
7828                PartitionOfBoundsAst::Range { lower, upper } => {
7829                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7830                }
7831                PartitionOfBoundsAst::List { values } => {
7832                    f.write_str("FOR VALUES IN (")?;
7833                    for (i, v) in values.iter().enumerate() {
7834                        if i > 0 {
7835                            f.write_str(", ")?;
7836                        }
7837                        write!(f, "{}", v)?;
7838                    }
7839                    f.write_str(")")
7840                }
7841                PartitionOfBoundsAst::Hash { modulus, remainder } => {
7842                    write!(
7843                        f,
7844                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7845                        modulus, remainder
7846                    )
7847                }
7848                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7849            }
7850        }
7851        AlterTableTarget::DetachPartition {
7852            child,
7853            concurrently,
7854            finalize,
7855        } => {
7856            write!(f, "DETACH PARTITION {}", quote_ident(child))?;
7857            if *concurrently {
7858                f.write_str(" CONCURRENTLY")?;
7859            }
7860            if *finalize {
7861                f.write_str(" FINALIZE")?;
7862            }
7863            Ok(())
7864        }
7865        AlterTableTarget::AlterColumnSetDefault {
7866            column,
7867            default_expr,
7868        } => write!(
7869            f,
7870            "ALTER COLUMN {} SET DEFAULT {}",
7871            quote_ident(column),
7872            default_expr
7873        ),
7874        AlterTableTarget::AlterColumnDropDefault { column } => {
7875            write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
7876        }
7877        AlterTableTarget::AlterColumnSetNotNull { column } => {
7878            write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
7879        }
7880        AlterTableTarget::AlterColumnDropNotNull { column } => {
7881            write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
7882        }
7883        AlterTableTarget::AlterColumnRestart { column, with } => {
7884            write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
7885            if let Some(n) = with {
7886                write!(f, " WITH {n}")?;
7887            }
7888            Ok(())
7889        }
7890        AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
7891            write!(
7892                f,
7893                "ALTER COLUMN {} DROP EXPRESSION{}",
7894                quote_ident(column),
7895                if *if_exists { " IF EXISTS" } else { "" }
7896            )
7897        }
7898        AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7899            write!(
7900                f,
7901                "ALTER COLUMN {} DROP IDENTITY{}",
7902                quote_ident(column),
7903                if *if_exists { " IF EXISTS" } else { "" }
7904            )
7905        }
7906        AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7907            write!(
7908                f,
7909                "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7910                quote_ident(column)
7911            )
7912        }
7913    }
7914}
7915
7916impl fmt::Display for TableConstraint {
7917    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7918        match self {
7919            Self::PrimaryKey { name, columns, .. } => {
7920                if let Some(n) = name {
7921                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7922                }
7923                f.write_str("PRIMARY KEY (")?;
7924                for (i, c) in columns.iter().enumerate() {
7925                    if i > 0 {
7926                        f.write_str(", ")?;
7927                    }
7928                    f.write_str(&quote_ident(c))?;
7929                }
7930                f.write_str(")")
7931            }
7932            Self::Unique {
7933                name,
7934                columns,
7935                nulls_not_distinct,
7936                ..
7937            } => {
7938                if let Some(n) = name {
7939                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7940                }
7941                f.write_str("UNIQUE ")?;
7942                if *nulls_not_distinct {
7943                    f.write_str("NULLS NOT DISTINCT ")?;
7944                }
7945                f.write_str("(")?;
7946                for (i, c) in columns.iter().enumerate() {
7947                    if i > 0 {
7948                        f.write_str(", ")?;
7949                    }
7950                    f.write_str(&quote_ident(c))?;
7951                }
7952                f.write_str(")")
7953            }
7954            Self::Check {
7955                name,
7956                expr,
7957                not_valid,
7958            } => {
7959                if let Some(n) = name {
7960                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7961                }
7962                write!(f, "CHECK ({expr})")?;
7963                if *not_valid {
7964                    write!(f, " NOT VALID")?;
7965                }
7966                Ok(())
7967            }
7968            Self::Index {
7969                name,
7970                columns,
7971                prefix_lengths,
7972            } => {
7973                f.write_str("KEY ")?;
7974                if let Some(n) = name {
7975                    write!(f, "{} ", quote_ident(n))?;
7976                }
7977                f.write_str("(")?;
7978                for (i, c) in columns.iter().enumerate() {
7979                    if i > 0 {
7980                        f.write_str(", ")?;
7981                    }
7982                    f.write_str(&quote_ident(c))?;
7983                    // v7.40.0 — the declared prefix rounds back with it.
7984                    if let Some(Some(p)) = prefix_lengths.get(i) {
7985                        write!(f, "({p})")?;
7986                    }
7987                }
7988                f.write_str(")")
7989            }
7990            Self::FulltextIndex { name, columns } => {
7991                // Mysqldump emits `FULLTEXT KEY name (cols)` —
7992                // Display rounds back to that shape so dump
7993                // replay reproduces the input verbatim.
7994                f.write_str("FULLTEXT KEY ")?;
7995                if let Some(n) = name {
7996                    write!(f, "{} ", quote_ident(n))?;
7997                }
7998                f.write_str("(")?;
7999                for (i, c) in columns.iter().enumerate() {
8000                    if i > 0 {
8001                        f.write_str(", ")?;
8002                    }
8003                    f.write_str(&quote_ident(c))?;
8004                }
8005                f.write_str(")")
8006            }
8007            Self::Exclude {
8008                name,
8009                method,
8010                elements,
8011            } => {
8012                if let Some(n) = name {
8013                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
8014                }
8015                f.write_str("EXCLUDE ")?;
8016                if let Some(m) = method {
8017                    write!(f, "USING {m} ")?;
8018                }
8019                f.write_str("(")?;
8020                for (i, (col, op)) in elements.iter().enumerate() {
8021                    if i > 0 {
8022                        f.write_str(", ")?;
8023                    }
8024                    write!(f, "{} WITH {op}", quote_ident(col))?;
8025                }
8026                f.write_str(")")
8027            }
8028        }
8029    }
8030}
8031
8032impl fmt::Display for ForeignKeyConstraint {
8033    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8034        if let Some(name) = &self.name {
8035            write!(f, "CONSTRAINT {} ", quote_ident(name))?;
8036        }
8037        f.write_str("FOREIGN KEY (")?;
8038        for (i, c) in self.columns.iter().enumerate() {
8039            if i > 0 {
8040                f.write_str(", ")?;
8041            }
8042            f.write_str(&quote_ident(c))?;
8043        }
8044        write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
8045        if !self.parent_columns.is_empty() {
8046            f.write_str(" (")?;
8047            for (i, c) in self.parent_columns.iter().enumerate() {
8048                if i > 0 {
8049                    f.write_str(", ")?;
8050                }
8051                f.write_str(&quote_ident(c))?;
8052            }
8053            f.write_str(")")?;
8054        }
8055        // Only render non-default actions to keep Display output
8056        // close to user input. SPG's default is RESTRICT (matches
8057        // SQL spec).
8058        if self.on_delete != FkAction::Restrict {
8059            write!(f, " ON DELETE {}", self.on_delete)?;
8060        }
8061        if self.on_update != FkAction::Restrict {
8062            write!(f, " ON UPDATE {}", self.on_update)?;
8063        }
8064        Ok(())
8065    }
8066}
8067
8068impl fmt::Display for FkAction {
8069    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8070        match self {
8071            Self::Restrict => f.write_str("RESTRICT"),
8072            Self::Cascade => f.write_str("CASCADE"),
8073            Self::SetNull => f.write_str("SET NULL"),
8074            Self::SetDefault => f.write_str("SET DEFAULT"),
8075            Self::NoAction => f.write_str("NO ACTION"),
8076        }
8077    }
8078}
8079
8080impl fmt::Display for ColumnDef {
8081    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8082        // v7.30.1 (mailrs round-24 class audit) — the type position
8083        // must re-parse to the same ColumnDef: a user-defined type
8084        // reference and the MySQL inline ENUM / SET value lists all
8085        // lower `ty` to Text, so rendering `ty` lost them.
8086        write!(f, "{}", quote_ident(&self.name))?;
8087        if let Some(ut) = &self.user_type_ref {
8088            write!(f, " {}", quote_ident(ut))?;
8089        } else if let Some(variants) = &self.inline_enum_variants {
8090            write_variant_list(f, "ENUM", variants)?;
8091        } else if let Some(variants) = &self.inline_set_variants {
8092            write_variant_list(f, "SET", variants)?;
8093        } else {
8094            write!(f, " {}", self.ty)?;
8095        }
8096        if self.is_unsigned {
8097            f.write_str(" UNSIGNED")?;
8098        }
8099        // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
8100        // DDL. Only emits when non-default so the typical output
8101        // stays unchanged.
8102        match self.collation {
8103            Collation::Binary => {}
8104            Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
8105        }
8106        if let Some(d) = &self.default {
8107            write!(f, " DEFAULT {d}")?;
8108        }
8109        if self.auto_increment {
8110            f.write_str(" AUTO_INCREMENT")?;
8111        }
8112        if !self.nullable {
8113            f.write_str(" NOT NULL")?;
8114        }
8115        // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
8116        // is NOT lifted to a table-level constraint at parse time
8117        // (unlike UNIQUE / CHECK), so the WAL round trip of a
8118        // prepared CREATE TABLE silently dropped the primary key.
8119        if self.is_primary_key {
8120            f.write_str(" PRIMARY KEY")?;
8121        }
8122        // The parser accepts only CURRENT_TIMESTAMP here (stored as
8123        // now()), so that spelling is the lossless round trip.
8124        if self.on_update_runtime.is_some() {
8125            f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
8126        }
8127        // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
8128        // replay reconstructs the computed-column declaration. The
8129        // expression sits inside a single set of parens; STORED is
8130        // the only variant the parser accepts.
8131        if let Some(gen_expr) = &self.generated_stored_expr {
8132            write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
8133        }
8134        Ok(())
8135    }
8136}
8137
8138/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
8139/// types (MySQL flavour; `ty` is Text underneath).
8140fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
8141    write!(f, " {kw}(")?;
8142    for (i, v) in variants.iter().enumerate() {
8143        if i > 0 {
8144            f.write_str(", ")?;
8145        }
8146        write!(f, "'{}'", v.replace('\'', "''"))?;
8147    }
8148    f.write_str(")")
8149}
8150
8151impl fmt::Display for InsertStatement {
8152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8153        write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
8154        if let Some(cols) = &self.columns {
8155            f.write_str(" (")?;
8156            for (i, c) in cols.iter().enumerate() {
8157                if i > 0 {
8158                    f.write_str(", ")?;
8159                }
8160                f.write_str(&quote_ident(c))?;
8161            }
8162            f.write_str(")")?;
8163        }
8164        // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
8165        // skipping the VALUES list (mailrs round-5 G4).
8166        if let Some(sel) = &self.select_source {
8167            write!(f, " {sel}")?;
8168        } else {
8169            f.write_str(" VALUES ")?;
8170            for (ri, row) in self.rows.iter().enumerate() {
8171                if ri > 0 {
8172                    f.write_str(", ")?;
8173                }
8174                f.write_str("(")?;
8175                for (i, v) in row.iter().enumerate() {
8176                    if i > 0 {
8177                        f.write_str(", ")?;
8178                    }
8179                    write!(f, "{v}")?;
8180                }
8181                f.write_str(")")?;
8182            }
8183        }
8184        // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
8185        // Display round trip: WAL persistence renders the bind-final
8186        // AST through this impl, and a replayed bare INSERT turns a
8187        // legal upsert no-op into a UNIQUE violation that refuses to
8188        // open the catalog.
8189        if let Some(oc) = &self.on_conflict {
8190            write!(f, " {oc}")?;
8191        }
8192        write_returning(self.returning.as_deref(), f)?;
8193        Ok(())
8194    }
8195}
8196
8197/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
8198/// parser produced, so the AST→SQL round trip preserves upsert
8199/// semantics (WAL replay depends on it).
8200impl fmt::Display for OnConflictClause {
8201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8202        f.write_str("ON CONFLICT")?;
8203        if let Some(name) = &self.constraint_name {
8204            write!(f, " ON CONSTRAINT {name}")?;
8205        }
8206        if !self.target_columns.is_empty() {
8207            f.write_str(" (")?;
8208            for (i, c) in self.target_columns.iter().enumerate() {
8209                if i > 0 {
8210                    f.write_str(", ")?;
8211                }
8212                f.write_str(&quote_ident(c))?;
8213            }
8214            f.write_str(")")?;
8215        }
8216        if let Some(w) = &self.index_where {
8217            write!(f, " WHERE {w}")?;
8218        }
8219        match &self.action {
8220            OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
8221            OnConflictAction::Update {
8222                assignments,
8223                where_,
8224            } => {
8225                f.write_str(" DO UPDATE SET ")?;
8226                for (i, (col, expr)) in assignments.iter().enumerate() {
8227                    if i > 0 {
8228                        f.write_str(", ")?;
8229                    }
8230                    write!(f, "{} = {expr}", quote_ident(col))?;
8231                }
8232                if let Some(w) = where_ {
8233                    write!(f, " WHERE {w}")?;
8234                }
8235                Ok(())
8236            }
8237        }
8238    }
8239}
8240
8241/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
8242/// tail for the three DML Display impls.
8243fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8244    let Some(items) = ret else {
8245        return Ok(());
8246    };
8247    f.write_str(" RETURNING ")?;
8248    for (i, item) in items.iter().enumerate() {
8249        if i > 0 {
8250            f.write_str(", ")?;
8251        }
8252        write!(f, "{item}")?;
8253    }
8254    Ok(())
8255}
8256
8257impl fmt::Display for UpdateStatement {
8258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8259        write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
8260        for (i, (col, expr)) in self.assignments.iter().enumerate() {
8261            if i > 0 {
8262                f.write_str(", ")?;
8263            }
8264            write!(f, "{} = {expr}", quote_ident(col))?;
8265        }
8266        if let Some(w) = &self.where_ {
8267            write!(f, " WHERE {w}")?;
8268        }
8269        // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
8270        if let Some(ol) = self.order_limit.as_deref() {
8271            if !ol.order_by.is_empty() {
8272                f.write_str(" ORDER BY ")?;
8273                for (i, o) in ol.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            if let Some(n) = ol.limit {
8289                write!(f, " LIMIT {n}")?;
8290            }
8291        }
8292        write_returning(self.returning.as_deref(), f)?;
8293        Ok(())
8294    }
8295}
8296
8297impl fmt::Display for DeleteStatement {
8298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8299        write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
8300        if let Some(w) = &self.where_ {
8301            write!(f, " WHERE {w}")?;
8302        }
8303        write_returning(self.returning.as_deref(), f)?;
8304        Ok(())
8305    }
8306}
8307
8308impl fmt::Display for CteBody {
8309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8310        match self {
8311            Self::Select(s) => write!(f, "{s}"),
8312            Self::Insert(s) => write!(f, "{s}"),
8313            Self::Update(s) => write!(f, "{s}"),
8314            Self::Delete(s) => write!(f, "{s}"),
8315            Self::Merge(s) => write!(f, "{s}"),
8316        }
8317    }
8318}
8319
8320impl fmt::Display for MergeStatement {
8321    // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
8322    // (it round-trips for the cases tests cover, not for
8323    // round-tripping every edge of the surface).
8324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8325        fmt_with_clause(&self.ctes, f)?;
8326        f.write_str("MERGE INTO ")?;
8327        write!(f, "{}", quote_ident(&self.target))?;
8328        if let Some(a) = &self.target_alias {
8329            write!(f, " {}", quote_ident(a))?;
8330        }
8331        f.write_str(" USING ")?;
8332        if let Some(sub) = &self.source_select {
8333            write!(f, "({sub})")?;
8334        } else {
8335            write!(f, "{}", quote_ident(&self.source))?;
8336        }
8337        if let Some(a) = &self.source_alias {
8338            write!(f, " {}", quote_ident(a))?;
8339        }
8340        if !self.source_column_aliases.is_empty() {
8341            f.write_str("(")?;
8342            for (i, c) in self.source_column_aliases.iter().enumerate() {
8343                if i > 0 {
8344                    f.write_str(", ")?;
8345                }
8346                write!(f, "{}", quote_ident(c))?;
8347            }
8348            f.write_str(")")?;
8349        }
8350        write!(f, " ON {}", self.on)?;
8351        for clause in &self.clauses {
8352            f.write_str(" WHEN ")?;
8353            f.write_str(match clause.matched {
8354                MergeMatched::Matched => "MATCHED",
8355                MergeMatched::NotMatched => "NOT MATCHED",
8356                MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
8357            })?;
8358            if let Some(c) = &clause.condition {
8359                write!(f, " AND {c}")?;
8360            }
8361            f.write_str(" THEN ")?;
8362            match &clause.action {
8363                MergeAction::Insert { columns, values } => {
8364                    f.write_str("INSERT ")?;
8365                    // A column list is optional (round 146): the bare
8366                    // `INSERT VALUES (…)` form maps positionally.
8367                    if !columns.is_empty() {
8368                        f.write_str("(")?;
8369                        for (i, c) in columns.iter().enumerate() {
8370                            if i > 0 {
8371                                f.write_str(", ")?;
8372                            }
8373                            write!(f, "{}", quote_ident(c))?;
8374                        }
8375                        f.write_str(") ")?;
8376                    }
8377                    f.write_str("VALUES (")?;
8378                    for (i, v) in values.iter().enumerate() {
8379                        if i > 0 {
8380                            f.write_str(", ")?;
8381                        }
8382                        write!(f, "{v}")?;
8383                    }
8384                    f.write_str(")")?;
8385                }
8386                MergeAction::Update { assignments } => {
8387                    f.write_str("UPDATE SET ")?;
8388                    for (i, (c, e)) in assignments.iter().enumerate() {
8389                        if i > 0 {
8390                            f.write_str(", ")?;
8391                        }
8392                        write!(f, "{} = {e}", quote_ident(c))?;
8393                    }
8394                }
8395                MergeAction::Delete => f.write_str("DELETE")?,
8396                MergeAction::DoNothing => f.write_str("DO NOTHING")?,
8397            }
8398        }
8399        if let Some(items) = &self.returning {
8400            f.write_str(" RETURNING ")?;
8401            for (i, it) in items.iter().enumerate() {
8402                if i > 0 {
8403                    f.write_str(", ")?;
8404                }
8405                write!(f, "{it}")?;
8406            }
8407        }
8408        Ok(())
8409    }
8410}
8411
8412/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
8413/// carry a CTE list and must round-trip it identically.
8414fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
8415    if ctes.is_empty() {
8416        return Ok(());
8417    }
8418    f.write_str("WITH ")?;
8419    if ctes.iter().any(|c| c.recursive) {
8420        f.write_str("RECURSIVE ")?;
8421    }
8422    for (i, cte) in ctes.iter().enumerate() {
8423        if i > 0 {
8424            f.write_str(", ")?;
8425        }
8426        f.write_str(&quote_ident(&cte.name))?;
8427        if !cte.column_overrides.is_empty() {
8428            f.write_str(" (")?;
8429            for (ci, c) in cte.column_overrides.iter().enumerate() {
8430                if ci > 0 {
8431                    f.write_str(", ")?;
8432                }
8433                f.write_str(&quote_ident(c))?;
8434            }
8435            f.write_str(")")?;
8436        }
8437        write!(f, " AS ({})", cte.body)?;
8438    }
8439    f.write_str(" ")
8440}
8441
8442impl fmt::Display for SelectStatement {
8443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8444        // v7.30.1 (mailrs round-24 class audit) — the WITH clause
8445        // must survive the round trip; a CTE-using statement
8446        // re-parsed without it references undefined tables.
8447        fmt_with_clause(&self.ctes, f)?;
8448        write_bare_select(self, f)?;
8449        for (kind, peer) in &self.unions {
8450            f.write_str(match kind {
8451                UnionKind::Distinct => " UNION ",
8452                UnionKind::All => " UNION ALL ",
8453                UnionKind::Intersect => " INTERSECT ",
8454                UnionKind::IntersectAll => " INTERSECT ALL ",
8455                UnionKind::Except => " EXCEPT ",
8456                UnionKind::ExceptAll => " EXCEPT ALL ",
8457            })?;
8458            write_bare_select(peer, f)?;
8459        }
8460        if !self.order_by.is_empty() {
8461            f.write_str(" ORDER BY ")?;
8462            for (i, o) in self.order_by.iter().enumerate() {
8463                if i > 0 {
8464                    f.write_str(", ")?;
8465                }
8466                write!(f, "{}", o.expr)?;
8467                if o.desc {
8468                    f.write_str(" DESC")?;
8469                }
8470                match o.nulls_first {
8471                    Some(true) => f.write_str(" NULLS FIRST")?,
8472                    Some(false) => f.write_str(" NULLS LAST")?,
8473                    None => {}
8474                }
8475            }
8476        }
8477        // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
8478        // exists in the FETCH FIRST spelling; rendering it as LIMIT
8479        // dropped the tie-extension semantics on replay. The parser
8480        // accepts OFFSET before FETCH, so keep that order here.
8481        if self.limit_with_ties {
8482            if let Some(o) = &self.offset {
8483                write!(f, " OFFSET {o}")?;
8484            }
8485            if let Some(n) = &self.limit {
8486                write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
8487            }
8488        } else {
8489            if let Some(n) = &self.limit {
8490                write!(f, " LIMIT {n}")?;
8491            }
8492            if let Some(o) = &self.offset {
8493                write!(f, " OFFSET {o}")?;
8494            }
8495        }
8496        Ok(())
8497    }
8498}
8499
8500fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8501    f.write_str("SELECT ")?;
8502    if s.distinct {
8503        f.write_str("DISTINCT ")?;
8504    }
8505    write_bare_select_body(s, f)
8506}
8507
8508fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8509    for (i, item) in s.items.iter().enumerate() {
8510        if i > 0 {
8511            f.write_str(", ")?;
8512        }
8513        write!(f, "{item}")?;
8514    }
8515    if let Some(t) = &s.from {
8516        write!(f, " FROM {t}")?;
8517    }
8518    if let Some(e) = &s.where_ {
8519        write!(f, " WHERE {e}")?;
8520    }
8521    if let Some(gs) = &s.group_by {
8522        f.write_str(" GROUP BY ")?;
8523        for (i, g) in gs.iter().enumerate() {
8524            if i > 0 {
8525                f.write_str(", ")?;
8526            }
8527            write!(f, "{g}")?;
8528        }
8529    } else if s.group_by_all {
8530        // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
8531        // shortcut parses to group_by: None + this flag; dropping
8532        // it turned an aggregate query into a bare projection on
8533        // re-parse.
8534        f.write_str(" GROUP BY ALL")?;
8535    }
8536    if let Some(h) = &s.having {
8537        write!(f, " HAVING {h}")?;
8538    }
8539    Ok(())
8540}
8541
8542impl fmt::Display for SelectItem {
8543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8544        match self {
8545            Self::Wildcard => f.write_str("*"),
8546            Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
8547            Self::Expr { expr, alias } => {
8548                write!(f, "{expr}")?;
8549                if let Some(a) = alias {
8550                    write!(f, " AS {}", quote_ident(a))?;
8551                }
8552                Ok(())
8553            }
8554        }
8555    }
8556}
8557
8558impl fmt::Display for FromClause {
8559    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8560        write!(f, "{}", self.primary)?;
8561        for j in &self.joins {
8562            match j.kind {
8563                JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
8564                JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
8565                JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
8566                JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
8567                JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
8568                JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
8569            }
8570            if let Some(on) = &j.on {
8571                write!(f, " ON {on}")?;
8572            }
8573        }
8574        Ok(())
8575    }
8576}
8577
8578/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
8579/// for NESTED). Kept close to the parser's grammar so it re-parses.
8580fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
8581    for (i, c) in cols.iter().enumerate() {
8582        if i > 0 {
8583            f.write_str(", ")?;
8584        }
8585        match c {
8586            JsonTableColumn::Ordinality { name } => {
8587                write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
8588            }
8589            JsonTableColumn::Nested { path, columns } => {
8590                write!(f, "NESTED PATH '{path}' COLUMNS (")?;
8591                fmt_json_table_columns(f, columns)?;
8592                f.write_str(")")?;
8593            }
8594            JsonTableColumn::Regular {
8595                name,
8596                ty,
8597                path,
8598                exists,
8599                format_json,
8600                wrapper,
8601                on_empty,
8602                on_error,
8603            } => {
8604                write!(f, "{} {ty}", quote_ident(name))?;
8605                if *format_json {
8606                    f.write_str(" FORMAT JSON")?;
8607                }
8608                if *exists {
8609                    write!(f, " EXISTS PATH '{path}'")?;
8610                } else {
8611                    write!(f, " PATH '{path}'")?;
8612                }
8613                if *wrapper {
8614                    f.write_str(" WITH WRAPPER")?;
8615                }
8616                if let JsonTableOnBehavior::Error = on_empty {
8617                    f.write_str(" ERROR ON EMPTY")?;
8618                } else if let JsonTableOnBehavior::Default(e) = on_empty {
8619                    write!(f, " DEFAULT {e} ON EMPTY")?;
8620                }
8621                if let JsonTableOnBehavior::Error = on_error {
8622                    f.write_str(" ERROR ON ERROR")?;
8623                } else if let JsonTableOnBehavior::Default(e) = on_error {
8624                    write!(f, " DEFAULT {e} ON ERROR")?;
8625                }
8626            }
8627        }
8628    }
8629    Ok(())
8630}
8631
8632impl fmt::Display for TableRef {
8633    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8634        // v7.30.1 (mailrs round-24 class audit) — the dynamic
8635        // table-ref shapes must round-trip: rendering only the
8636        // (synthetic) name turned LATERAL / unnest() /
8637        // generate_series() into references to nonexistent tables
8638        // on re-parse.
8639        // v7.39 (round 205) — JSON_TABLE round-trips through Display
8640        // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
8641        if let Some(jt) = &self.json_table {
8642            write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
8643            if !jt.passing.is_empty() {
8644                f.write_str(" PASSING ")?;
8645                for (i, (n, e)) in jt.passing.iter().enumerate() {
8646                    if i > 0 {
8647                        f.write_str(", ")?;
8648                    }
8649                    write!(f, "{e} AS {}", quote_ident(n))?;
8650                }
8651            }
8652            f.write_str(" COLUMNS (")?;
8653            fmt_json_table_columns(f, &jt.columns)?;
8654            f.write_str(")")?;
8655            if let Some(a) = &self.alias {
8656                write!(f, " AS {}", quote_ident(a))?;
8657            }
8658            return Ok(());
8659        }
8660        if let Some(inner) = &self.lateral_subquery {
8661            write!(f, "LATERAL ({inner})")?;
8662            if let Some(a) = &self.alias {
8663                write!(f, " AS {}", quote_ident(a))?;
8664                // v7.37 D.28 — a derived table on the lateral_subquery channel
8665                // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
8666                // lowers here). Rendering the alias without the column list lost
8667                // the column names on re-parse (a view body round-trips through
8668                // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
8669                if !self.unnest_column_aliases.is_empty() {
8670                    f.write_str(" (")?;
8671                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8672                        if i > 0 {
8673                            f.write_str(", ")?;
8674                        }
8675                        f.write_str(&quote_ident(c))?;
8676                    }
8677                    f.write_str(")")?;
8678                }
8679            }
8680            return Ok(());
8681        }
8682        if let Some(expr) = &self.unnest_expr {
8683            write!(f, "UNNEST({expr})")?;
8684            if let Some(a) = &self.alias {
8685                write!(f, " AS {}", quote_ident(a))?;
8686                if !self.unnest_column_aliases.is_empty() {
8687                    f.write_str(" (")?;
8688                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8689                        if i > 0 {
8690                            f.write_str(", ")?;
8691                        }
8692                        f.write_str(&quote_ident(c))?;
8693                    }
8694                    f.write_str(")")?;
8695                }
8696            }
8697            return Ok(());
8698        }
8699        // 7.38.1 S5.1 — a FROM-position table function must re-render
8700        // as the CALL, not its bare name: ARRAY(subquery) desugars by
8701        // re-parsing the subquery's canonical text, and a dropped
8702        // argument list turned `pg_options_to_table(x)` into a
8703        // relation lookup that does not exist.
8704        if let Some(call) = &self.table_fn_call {
8705            let (fn_name, args) = call.as_ref();
8706            write!(f, "{fn_name}(")?;
8707            for (i, a) in args.iter().enumerate() {
8708                if i > 0 {
8709                    f.write_str(", ")?;
8710                }
8711                write!(f, "{a}")?;
8712            }
8713            f.write_str(")")?;
8714            if let Some(a) = &self.alias {
8715                write!(f, " AS {}", quote_ident(a))?;
8716                if !self.unnest_column_aliases.is_empty() {
8717                    f.write_str("(")?;
8718                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8719                        if i > 0 {
8720                            f.write_str(", ")?;
8721                        }
8722                        write!(f, "{}", quote_ident(c))?;
8723                    }
8724                    f.write_str(")")?;
8725                }
8726            }
8727            return Ok(());
8728        }
8729        if let Some(args) = &self.generate_series_args {
8730            f.write_str("generate_series(")?;
8731            for (i, a) in args.iter().enumerate() {
8732                if i > 0 {
8733                    f.write_str(", ")?;
8734                }
8735                write!(f, "{a}")?;
8736            }
8737            f.write_str(")")?;
8738            if let Some(a) = &self.alias {
8739                write!(f, " AS {}", quote_ident(a))?;
8740            }
8741            return Ok(());
8742        }
8743        write!(f, "{}", quote_ident(&self.name))?;
8744        if let Some(seg) = self.as_of_segment {
8745            write!(f, " AS OF SEGMENT {seg}")?;
8746        }
8747        if let Some(a) = &self.alias {
8748            write!(f, " AS {}", quote_ident(a))?;
8749        }
8750        Ok(())
8751    }
8752}
8753
8754impl fmt::Display for ColumnName {
8755    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8756        if let Some(q) = &self.qualifier {
8757            write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
8758        } else {
8759            write!(f, "{}", quote_ident(&self.name))
8760        }
8761    }
8762}
8763
8764/// v7.39 (round 311) — render the left spine of an AND / OR chain
8765/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
8766/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
8767/// SAME operator flattens; anything else is an ordinary operand.
8768fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
8769    if let Expr::Binary {
8770        lhs,
8771        op: inner,
8772        rhs,
8773    } = e
8774        && *inner == op
8775    {
8776        write_bool_chain(f, lhs, op)?;
8777        return write!(f, " {op} {rhs}");
8778    }
8779    write!(f, "{e}")
8780}
8781
8782/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
8783/// form `pg_get_constraintdef(oid, true)` and friends return.
8784///
8785/// The default [`fmt::Display`] parenthesises every operator node, which
8786/// is what PG's non-pretty deparse does and what makes the text
8787/// round-trip. Pretty drops the pairs the grammar can put back, and the
8788/// rule is NOT plain precedence minimisation — measured against PG 18.4
8789/// across 37 shapes:
8790///
8791///   * the boolean layer follows precedence (NOT > AND > OR): an OR
8792///     under an AND keeps its parens, an AND under an OR does not, and a
8793///     comparison under any of them does not (`NOT a > 1`);
8794///   * an associative chain flattens completely, even where the source
8795///     nested it to the right (`a AND (b AND c)` prints as one chain);
8796///   * but an operand of a comparison or arithmetic operator keeps its
8797///     parens whenever it is itself an operator expression — so
8798///     `(a + b) > 10` and `(- a) + b`, even though precedence alone
8799///     would not require either. A cast, function call, column or
8800///     literal in that position does not (`a::text = t`,
8801///     `length(code) > 2`); a cast counts as compound exactly when the
8802///     thing it casts is (`((a + b)::text) = t`).
8803///
8804/// Anything outside that layer defers to `Display`, which is never
8805/// wrong — only more parenthesised than PG would print.
8806#[must_use]
8807pub fn pretty_expr(e: &Expr) -> String {
8808    let mut out = String::new();
8809    write_pretty(&mut out, e, PrettyParent::None, false, false);
8810    out
8811}
8812
8813/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
8814/// writes it.
8815///
8816/// MariaDB names the offending expression in its out-of-range message
8817/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
8818/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
8819/// MySQL client, for a cast the client had just written the other way.
8820#[must_use]
8821pub fn pretty_expr_mysql(e: &Expr) -> String {
8822    let mut out = String::new();
8823    write_pretty(&mut out, e, PrettyParent::None, false, true);
8824    out
8825}
8826
8827/// v7.39 (round 505) — how strongly an expression suggests its own column
8828/// name. A cast keeps its argument's name only when that name is STRONG;
8829/// otherwise the cast reports the type it casts to.
8830///
8831/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
8832/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
8833/// itself `text` — so `case` and a function name cannot be the same kind of
8834/// answer, even though a bare `CASE …` does report `case`.
8835#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8836enum NameStrength {
8837    /// Nothing to go on — PG reports `?column?`.
8838    None,
8839    /// A name, but one a cast overrides: `case`, or a type name.
8840    Weak,
8841    /// A name a cast keeps: a column, or the function that produced it.
8842    Strong,
8843}
8844
8845/// v7.39 (round 505) — the column name PG18 gives a projected expression
8846/// that carries no `AS` alias. `None` means `?column?`.
8847///
8848/// SPG used to print the parsed expression back out, which matched neither
8849/// oracle and made name-keyed row access miss on both wires:
8850///
8851/// | query        | PG18       | SPG (before) |
8852/// |--------------|------------|--------------|
8853/// | `upper(s)`   | `upper`    | `upper(s)`   |
8854/// | `a+b`        | `?column?` | `(a + b)`    |
8855/// | `'lit'`      | `?column?` | `'lit'`      |
8856/// | `CASE …`     | `case`     | `CASE WHEN (a = 1) THEN …` |
8857///
8858/// Every rule below is one of those measurements, taken with `\gdesc`
8859/// against PG18: a call is named for its function, a cast recurses into its
8860/// argument and falls back to the type, a scalar subquery takes the name of
8861/// the column it selects, and operators have no name at all.
8862#[must_use]
8863pub fn figure_column_name(expr: &Expr) -> Option<String> {
8864    let (name, _) = figure_name_inner(expr);
8865    name
8866}
8867
8868/// The name a function reports, which is not always the name SPG parsed it
8869/// under: `count(*)` is held as `count_star` so the star arity survives the
8870/// AST, and that internal spelling must not reach a client. PG18 reports
8871/// `count`.
8872/// v7.39.13 — public, because Describe was naming the same call from a
8873/// second map that did not have this entry.
8874///
8875/// `count(*)` is held as `count_star` so the star arity survives the
8876/// AST. The projection mapped it back and the extended protocol's
8877/// Describe did not, so `SELECT count(*) OVER ()` answered `count` in
8878/// the row stream and `count_star` to `\gdesc` — an ORM-visible column
8879/// name, and two answers to one question. Reported by sentori against
8880/// 7.39.12.
8881#[must_use]
8882pub fn canonical_function_name(name: &str) -> String {
8883    match name {
8884        "count_star" => "count".to_string(),
8885        other => other.to_ascii_lowercase(),
8886    }
8887}
8888
8889/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
8890/// reports when its operand has none of its own. Only the spellings that
8891/// differ from what the user writes need an entry; everything else is
8892/// already its own typname.
8893fn cast_target_typname(target: &CastTarget) -> String {
8894    let written = target.to_string().to_ascii_lowercase();
8895    let base = written.strip_suffix("[]").unwrap_or(&written);
8896    let mapped = match base {
8897        "bigint" => "int8",
8898        "integer" | "int" => "int4",
8899        "smallint" => "int2",
8900        "boolean" => "bool",
8901        "double precision" => "float8",
8902        "real" => "float4",
8903        "character varying" => "varchar",
8904        "character" => "bpchar",
8905        "timestamp with time zone" => "timestamptz",
8906        "timestamp without time zone" => "timestamp",
8907        "time without time zone" => "time",
8908        "decimal" => "numeric",
8909        other => other,
8910    };
8911    if written.ends_with("[]") {
8912        alloc::format!("_{mapped}")
8913    } else {
8914        String::from(mapped)
8915    }
8916}
8917
8918fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8919    let strong = |n: String| (Some(n), NameStrength::Strong);
8920    match expr {
8921        // A column keeps its own name, qualifier and all discarded:
8922        // `lbl.a` reports `a`.
8923        Expr::Column(c) => strong(c.name.clone()),
8924        // Calls are named for the function. This covers the shapes that
8925        // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8926        // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8927        // because PG resolves them to functions before naming them.
8928        Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8929            strong(canonical_function_name(name))
8930        }
8931        Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8932        Expr::Extract { .. } => strong("extract".to_string()),
8933        Expr::Exists { .. } => strong("exists".to_string()),
8934        Expr::Array(_) => strong("array".to_string()),
8935        // `(expr).field` is named for the field, as a column would be.
8936        Expr::FieldAccess { field, .. } => strong(field.clone()),
8937        // v7.39.12 — PostgreSQL names a subscript after its operand, so
8938        // `arr[1]` is `arr`. There was no arm, so it fell through to
8939        // `?column?`. Reported by sentori against 7.39.11 — the same
8940        // naming defect v7.38.20 closed, reached through a different
8941        // expression. Weak, like the field access above it: an outer
8942        // cast or function still names the column.
8943        Expr::ArraySubscript { target, .. } => (figure_name_inner(target).0, NameStrength::Weak),
8944        // A cast prefers its argument's name and settles for the type:
8945        // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8946        Expr::Cast {
8947            expr: inner,
8948            target,
8949        } => match figure_name_inner(inner) {
8950            (Some(n), NameStrength::Strong) => strong(n),
8951            // v7.38.7 — the fallback is the target type's INTERNAL name,
8952            // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8953            // the `bigint` the user typed. Measured on PG18 alongside
8954            // `CAST(7 AS bigint)`, which answers `int8` too.
8955            _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8956        },
8957        // A scalar subquery reports whatever its single output column
8958        // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8959        Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8960        // `CASE …` names itself, but weakly — a cast around it wins.
8961        Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8962        // A literal that carries its own type names itself for that type:
8963        // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8964        // reports nothing. Weak, like any other type name.
8965        Expr::Literal(Literal::Interval { .. }) => {
8966            (Some("interval".to_string()), NameStrength::Weak)
8967        }
8968        // A wrapper that adds no name of its own.
8969        Expr::Variadic(inner) => figure_name_inner(inner),
8970        Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
8971        // Everything else — operators, comparisons, IS NULL, LIKE, IN,
8972        // literals, placeholders — reports `?column?`.
8973        _ => (None, NameStrength::None),
8974    }
8975}
8976
8977/// The name a scalar subquery's single projected column reports.
8978fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
8979    match sel.items.as_slice() {
8980        [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
8981        [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
8982        _ => (None, NameStrength::None),
8983    }
8984}
8985
8986/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
8987fn pretty_prec(e: &Expr) -> u8 {
8988    match e {
8989        Expr::Binary { op, .. } => match op {
8990            // v7.39 (round 407) — this deparse ladder mirrors the parser's:
8991            // OR < XOR < AND < NOT < comparison < additive < multiplicative.
8992            // XOR (MySQL-only) sits between OR and AND, so AND and everything
8993            // above shifted +1 to open rung 2 for it.
8994            BinOp::Or => 1,
8995            BinOp::LogicalXor => 2,
8996            BinOp::And => 3,
8997            BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
8998            BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
8999            // Everything else in this enum is a comparison-shaped
9000            // operator; they share one level, as in the grammar.
9001            _ => 5,
9002        },
9003        Expr::Unary { op, .. } => match op {
9004            UnOp::Not => 4,
9005            UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
9006        },
9007        _ => u8::MAX,
9008    }
9009}
9010
9011/// Is this node an operator expression — the thing an arithmetic or
9012/// comparison parent keeps parentheses around? A cast inherits the
9013/// answer from what it casts.
9014fn pretty_is_compound(e: &Expr) -> bool {
9015    match e {
9016        Expr::Binary { .. } | Expr::Unary { .. } => true,
9017        Expr::Cast { expr, .. } => pretty_is_compound(expr),
9018        _ => false,
9019    }
9020}
9021
9022/// `parent` describes the enclosing operator: its binding power, and
9023/// whether it is a comparison (which keeps parens around any operator
9024/// operand) or a NOT (which keeps them at equal power too).
9025#[derive(Clone, Copy, PartialEq)]
9026enum PrettyParent {
9027    /// Nothing encloses this node.
9028    None,
9029    /// A comparison-shaped operator: an operator operand always keeps
9030    /// its parens, whatever precedence would allow.
9031    Comparison,
9032    /// Arithmetic / concatenation: precedence decides.
9033    Arith(u8),
9034    /// A boolean connective: precedence decides.
9035    Bool(u8),
9036    /// `NOT`: precedence decides, but equal power still needs parens so
9037    /// `NOT (NOT a > 1)` does not collapse.
9038    Not,
9039}
9040
9041fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
9042    let prec = pretty_prec(e);
9043    let is_unary_sign = matches!(
9044        e,
9045        Expr::Unary {
9046            op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
9047            ..
9048        }
9049    );
9050    let needs = match parent {
9051        PrettyParent::None => false,
9052        PrettyParent::Comparison => pretty_is_compound(e),
9053        // A sign always keeps its parens under an operator — PG writes
9054        // `(- a) + b` even though precedence would not require it.
9055        PrettyParent::Arith(p) => {
9056            is_unary_sign
9057                || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
9058                    && (prec < p || (prec == p && is_rhs)))
9059        }
9060        PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
9061        PrettyParent::Not => {
9062            matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
9063        }
9064    };
9065    if needs {
9066        out.push('(');
9067    }
9068    match e {
9069        Expr::Binary { lhs, op, rhs } => {
9070            let child = match op {
9071                BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
9072                BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
9073                    PrettyParent::Arith(prec)
9074                }
9075                _ => PrettyParent::Comparison,
9076            };
9077            write_pretty(out, lhs, child, false, mysql);
9078            out.push(' ');
9079            out.push_str(&alloc::format!("{op}"));
9080            out.push(' ');
9081            // AND / OR are associative, so an explicitly right-nested
9082            // chain still prints as one chain.
9083            let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
9084            write_pretty(out, rhs, child, rhs_is_rhs, mysql);
9085        }
9086        Expr::Unary { op, expr } => match op {
9087            UnOp::Not => {
9088                out.push_str("NOT ");
9089                write_pretty(out, expr, PrettyParent::Not, false, mysql);
9090            }
9091            UnOp::Neg => {
9092                out.push_str("- ");
9093                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9094            }
9095            UnOp::Plus => {
9096                out.push_str("+ ");
9097                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9098            }
9099            UnOp::BitNot => {
9100                out.push('~');
9101                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9102            }
9103        },
9104        Expr::Cast { expr, target } => {
9105            if mysql {
9106                // MySQL's own spelling, which is what its error messages
9107                // quote back.
9108                out.push_str("cast(");
9109                write_pretty(out, expr, PrettyParent::None, false, mysql);
9110                out.push_str(&alloc::format!(
9111                    " as {})",
9112                    target.to_string().to_lowercase()
9113                ));
9114            } else {
9115                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9116                out.push_str(&alloc::format!("::{target}"));
9117            }
9118        }
9119        Expr::IsNull { expr, negated } => {
9120            write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9121            out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
9122        }
9123        other => out.push_str(&alloc::format!("{other}")),
9124    }
9125    if needs {
9126        out.push(')');
9127    }
9128}
9129
9130const fn pretty_prec_not() -> u8 {
9131    // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
9132    // when the XOR insertion shifted the deparse ladder up by one).
9133    4
9134}
9135
9136impl fmt::Display for Expr {
9137    #[allow(clippy::too_many_lines)]
9138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9139        match self {
9140            Self::Literal(l) => write!(f, "{l}"),
9141            Self::Column(c) => write!(f, "{c}"),
9142            Self::Placeholder(n) => write!(f, "${n}"),
9143            // Round-trips as the spelling PG's docs lead with.
9144            Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
9145            Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
9146            // Round-trips with the name quoted, which is how PG spells a
9147            // collation everywhere: `"en_US.utf8"`, `"C"`.
9148            Self::Collate { expr, collation } => {
9149                write!(f, "{expr} COLLATE {}", quote_ident(collation))
9150            }
9151            // v7.39 (round 311) — an AND / OR chain that nests to the
9152            // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
9153            // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
9154            // its parentheses, because that is a different grouping as
9155            // written. Both halves measured against PG 18.4's deparse,
9156            // which flattens a same-operator left chain at parse time and
9157            // leaves `a AND (b AND c)` alone.
9158            Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
9159                f.write_str("(")?;
9160                write_bool_chain(f, lhs, *op)?;
9161                write!(f, " {op} {rhs}")?;
9162                f.write_str(")")
9163            }
9164            Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
9165            Self::Unary { op, expr } => match op {
9166                UnOp::Not => write!(f, "(NOT {expr})"),
9167                // A space after the sign, as PG's deparse writes it.
9168                UnOp::Neg => write!(f, "(- {expr})"),
9169                UnOp::Plus => write!(f, "(+ {expr})"),
9170                UnOp::BitNot => write!(f, "(~{expr})"),
9171            },
9172            // The OPERAND carries the parentheses, not the cast:
9173            // `(a)::text`, `((a + b))::text`. PG words it this way, and
9174            // it is what keeps `a::text = t` from reading as a cast of
9175            // the comparison.
9176            Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
9177            Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
9178            Self::AggregateOrdered {
9179                call,
9180                order_by,
9181                distinct,
9182                filter,
9183            } => {
9184                let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
9185                    for (i, o) in order_by.iter().enumerate() {
9186                        if i > 0 {
9187                            f.write_str(", ")?;
9188                        }
9189                        write!(f, "{}", o.expr)?;
9190                        if o.desc {
9191                            f.write_str(" DESC")?;
9192                        }
9193                        match o.nulls_first {
9194                            Some(true) => f.write_str(" NULLS FIRST")?,
9195                            Some(false) => f.write_str(" NULLS LAST")?,
9196                            None => {}
9197                        }
9198                    }
9199                    Ok(())
9200                };
9201                // Ordered-set aggregates (`percentile_cont(f) WITHIN
9202                // GROUP (ORDER BY x)`) render the in-parens args as the
9203                // direct argument and the sort spec under WITHIN GROUP —
9204                // not as an in-argument ORDER BY.
9205                let ordered_set = matches!(
9206                    call.as_ref(),
9207                    Expr::FunctionCall { name, .. }
9208                        if matches!(
9209                            name.to_ascii_lowercase().as_str(),
9210                            "percentile_cont" | "percentile_disc" | "mode"
9211                        )
9212                );
9213                if ordered_set {
9214                    write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
9215                    fmt_order_by(f)?;
9216                    f.write_str(")")?;
9217                } else {
9218                    // `name([DISTINCT ]args [ORDER BY …])` — peel the
9219                    // inner call's parens to splice modifiers.
9220                    let inner = alloc::format!("{call}");
9221                    let body = inner.strip_suffix(')').unwrap_or(&inner);
9222                    let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
9223                    write!(f, "{head}(")?;
9224                    if *distinct {
9225                        f.write_str("DISTINCT ")?;
9226                    }
9227                    write!(f, "{args_part}")?;
9228                    if !order_by.is_empty() {
9229                        f.write_str(" ORDER BY ")?;
9230                        fmt_order_by(f)?;
9231                    }
9232                    f.write_str(")")?;
9233                }
9234                if let Some(cond) = filter {
9235                    write!(f, " FILTER (WHERE {cond})")?;
9236                }
9237                Ok(())
9238            }
9239            Self::IsNull { expr, negated } => {
9240                if *negated {
9241                    write!(f, "({expr} IS NOT NULL)")
9242                } else {
9243                    write!(f, "({expr} IS NULL)")
9244                }
9245            }
9246            Self::BoolTest {
9247                expr,
9248                value,
9249                negated,
9250            } => {
9251                let word = match value {
9252                    Some(true) => "TRUE",
9253                    Some(false) => "FALSE",
9254                    None => "UNKNOWN",
9255                };
9256                if *negated {
9257                    write!(f, "({expr} IS NOT {word})")
9258                } else {
9259                    write!(f, "({expr} IS {word})")
9260                }
9261            }
9262            Self::FunctionCall { name, args } => {
9263                write!(f, "{name}(")?;
9264                for (i, a) in args.iter().enumerate() {
9265                    if i > 0 {
9266                        f.write_str(", ")?;
9267                    }
9268                    write!(f, "{a}")?;
9269                }
9270                f.write_str(")")
9271            }
9272            Self::Like {
9273                expr,
9274                pattern,
9275                negated,
9276                case_insensitive,
9277            } => {
9278                let op = match (negated, case_insensitive) {
9279                    (false, false) => "LIKE",
9280                    (true, false) => "NOT LIKE",
9281                    (false, true) => "ILIKE",
9282                    (true, true) => "NOT ILIKE",
9283                };
9284                write!(f, "({expr} {op} {pattern})")
9285            }
9286            Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
9287            Self::WindowFunction {
9288                name,
9289                args,
9290                partition_by,
9291                order_by,
9292                frame,
9293                null_treatment,
9294                filter,
9295            } => {
9296                write!(f, "{name}(")?;
9297                for (i, a) in args.iter().enumerate() {
9298                    if i > 0 {
9299                        f.write_str(", ")?;
9300                    }
9301                    write!(f, "{a}")?;
9302                }
9303                f.write_str(")")?;
9304                // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
9305                // OVER; it round-trips so a window body's Display re-parses.
9306                if let Some(cond) = filter {
9307                    write!(f, " FILTER (WHERE {cond})")?;
9308                }
9309                // v7.30.1 (mailrs round-24 class audit) — IGNORE
9310                // NULLS sits between the arg list and OVER; dropping
9311                // it reverted replayed queries to RESPECT NULLS.
9312                if matches!(null_treatment, NullTreatment::Ignore) {
9313                    f.write_str(" IGNORE NULLS")?;
9314                }
9315                f.write_str(" OVER (")?;
9316                if !partition_by.is_empty() {
9317                    f.write_str("PARTITION BY ")?;
9318                    for (i, p) in partition_by.iter().enumerate() {
9319                        if i > 0 {
9320                            f.write_str(", ")?;
9321                        }
9322                        write!(f, "{p}")?;
9323                    }
9324                }
9325                if !order_by.is_empty() {
9326                    if !partition_by.is_empty() {
9327                        f.write_str(" ")?;
9328                    }
9329                    f.write_str("ORDER BY ")?;
9330                    for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
9331                        if i > 0 {
9332                            f.write_str(", ")?;
9333                        }
9334                        write!(f, "{e}")?;
9335                        if *desc {
9336                            f.write_str(" DESC")?;
9337                        }
9338                        match nulls_first {
9339                            Some(true) => f.write_str(" NULLS FIRST")?,
9340                            Some(false) => f.write_str(" NULLS LAST")?,
9341                            None => {}
9342                        }
9343                    }
9344                }
9345                if let Some(fr) = frame {
9346                    if !partition_by.is_empty() || !order_by.is_empty() {
9347                        f.write_str(" ")?;
9348                    }
9349                    let k = match fr.kind {
9350                        FrameKind::Rows => "ROWS",
9351                        FrameKind::Range => "RANGE",
9352                        FrameKind::Groups => "GROUPS",
9353                    };
9354                    if let Some(end) = &fr.end {
9355                        write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
9356                    } else {
9357                        write!(f, "{k} {}", fr.start)?;
9358                    }
9359                }
9360                f.write_str(")")
9361            }
9362            Self::ScalarSubquery(s) => write!(f, "({s})"),
9363            Self::Exists { subquery, negated } => {
9364                if *negated {
9365                    write!(f, "NOT EXISTS ({subquery})")
9366                } else {
9367                    write!(f, "EXISTS ({subquery})")
9368                }
9369            }
9370            Self::InSubquery {
9371                expr,
9372                subquery,
9373                negated,
9374            } => {
9375                if *negated {
9376                    write!(f, "({expr} NOT IN ({subquery}))")
9377                } else {
9378                    write!(f, "({expr} IN ({subquery}))")
9379                }
9380            }
9381            Self::RowInSubquery {
9382                row,
9383                subquery,
9384                negated,
9385            } => {
9386                write!(f, "(")?;
9387                for (i, e) in row.iter().enumerate() {
9388                    if i > 0 {
9389                        write!(f, ", ")?;
9390                    }
9391                    write!(f, "{e}")?;
9392                }
9393                let kw = if *negated { ") NOT IN (" } else { ") IN (" };
9394                write!(f, "{kw}{subquery})")
9395            }
9396            Self::RowCmpSubquery { row, op, subquery } => {
9397                write!(f, "(")?;
9398                for (i, e) in row.iter().enumerate() {
9399                    if i > 0 {
9400                        write!(f, ", ")?;
9401                    }
9402                    write!(f, "{e}")?;
9403                }
9404                write!(f, ") {op} ({subquery})")
9405            }
9406            Self::InList {
9407                expr,
9408                list,
9409                negated,
9410            } => {
9411                let kw = if *negated { " NOT IN (" } else { " IN (" };
9412                write!(f, "({expr}{kw}")?;
9413                for (i, e) in list.iter().enumerate() {
9414                    if i > 0 {
9415                        f.write_str(", ")?;
9416                    }
9417                    write!(f, "{e}")?;
9418                }
9419                f.write_str("))")
9420            }
9421            Self::Array(items) => {
9422                f.write_str("ARRAY[")?;
9423                for (i, e) in items.iter().enumerate() {
9424                    if i > 0 {
9425                        f.write_str(", ")?;
9426                    }
9427                    write!(f, "{e}")?;
9428                }
9429                f.write_str("]")
9430            }
9431            Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
9432            Self::ArraySlice { target, lo, hi } => {
9433                write!(f, "({target}[")?;
9434                if let Some(l) = lo {
9435                    write!(f, "{l}")?;
9436                }
9437                write!(f, ":")?;
9438                if let Some(h) = hi {
9439                    write!(f, "{h}")?;
9440                }
9441                write!(f, "])")
9442            }
9443            Self::AnyAll {
9444                expr,
9445                op,
9446                array,
9447                is_any,
9448            } => {
9449                let kw = if *is_any { "ANY" } else { "ALL" };
9450                write!(f, "({expr} {op} {kw}({array}))")
9451            }
9452            Self::Case {
9453                operand,
9454                branches,
9455                else_branch,
9456            } => {
9457                f.write_str("CASE")?;
9458                if let Some(op) = operand {
9459                    write!(f, " {op}")?;
9460                }
9461                for (w, t) in branches {
9462                    write!(f, " WHEN {w} THEN {t}")?;
9463                }
9464                if let Some(e) = else_branch {
9465                    write!(f, " ELSE {e}")?;
9466                }
9467                f.write_str(" END")
9468            }
9469        }
9470    }
9471}
9472
9473/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
9474/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
9475pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
9476    use alloc::string::ToString;
9477    if scale == 0 {
9478        return alloc::format!("{unscaled}");
9479    }
9480    let neg = unscaled < 0;
9481    let digits = alloc::format!("{}", unscaled.unsigned_abs());
9482    let scale = scale as usize;
9483    let (int_part, frac_part) = if digits.len() > scale {
9484        (
9485            digits[..digits.len() - scale].to_string(),
9486            digits[digits.len() - scale..].to_string(),
9487        )
9488    } else {
9489        ("0".to_string(), alloc::format!("{digits:0>scale$}"))
9490    };
9491    alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
9492}
9493
9494/// A single-quoted SQL string, with an embedded quote doubled.
9495fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
9496    f.write_str("'")?;
9497    for c in s.chars() {
9498        if c == '\'' {
9499            f.write_str("''")?;
9500        } else {
9501            write!(f, "{c}")?;
9502        }
9503    }
9504    f.write_str("'")
9505}
9506
9507impl fmt::Display for Literal {
9508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9509        match self {
9510            Self::Integer(n) => write!(f, "{n}"),
9511            Self::Float(x) => {
9512                let s = format!("{x}");
9513                // Default Display for an integral f64 (e.g. 1.0) emits "1",
9514                // which would round-trip back to Integer. Force a dot.
9515                if s.contains('.') || s.contains('e') || s.contains('E') {
9516                    f.write_str(&s)
9517                } else {
9518                    write!(f, "{s}.0")
9519                }
9520            }
9521            Self::Numeric { unscaled, scale } => {
9522                // Render the exact decimal `unscaled / 10^scale`, preserving
9523                // scale (trailing zeros) — round-trips to the same literal.
9524                f.write_str(&render_exact_decimal(*unscaled, *scale))
9525            }
9526            Self::NumericBig(s) => f.write_str(s),
9527            // Printed exactly as the text form was, so a reader cannot
9528            // tell whether the constant was decoded or not.
9529            Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
9530            Self::String(s) => write_quoted(f, s),
9531            Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
9532            Self::Null => f.write_str("NULL"),
9533            // PG external array form. Display round-trip re-enters
9534            // through the column-typed text coerce, same as pgwire.
9535            Self::TextArray(items) => {
9536                f.write_str("'{")?;
9537                for (i, it) in items.iter().enumerate() {
9538                    if i > 0 {
9539                        f.write_str(",")?;
9540                    }
9541                    match it {
9542                        None => f.write_str("NULL")?,
9543                        Some(s) => {
9544                            f.write_str("\"")?;
9545                            for c in s.chars() {
9546                                match c {
9547                                    // array-element escapes
9548                                    '"' | '\\' => write!(f, "\\{c}")?,
9549                                    // the OUTER wrapper is a SQL string
9550                                    // literal — embedded quotes must
9551                                    // double, or the rendered form
9552                                    // (WAL replay parses it back) is
9553                                    // invalid SQL
9554                                    '\'' => f.write_str("''")?,
9555                                    _ => write!(f, "{c}")?,
9556                                }
9557                            }
9558                            f.write_str("\"")?;
9559                        }
9560                    }
9561                }
9562                f.write_str("}'")
9563            }
9564            Self::IntArray(items) => {
9565                f.write_str("'{")?;
9566                for (i, it) in items.iter().enumerate() {
9567                    if i > 0 {
9568                        f.write_str(",")?;
9569                    }
9570                    match it {
9571                        None => f.write_str("NULL")?,
9572                        Some(n) => write!(f, "{n}")?,
9573                    }
9574                }
9575                f.write_str("}'")
9576            }
9577            Self::BigIntArray(items) => {
9578                f.write_str("'{")?;
9579                for (i, it) in items.iter().enumerate() {
9580                    if i > 0 {
9581                        f.write_str(",")?;
9582                    }
9583                    match it {
9584                        None => f.write_str("NULL")?,
9585                        Some(n) => write!(f, "{n}")?,
9586                    }
9587                }
9588                f.write_str("}'")
9589            }
9590            Self::Vector(v) => {
9591                f.write_str("[")?;
9592                for (i, x) in v.iter().enumerate() {
9593                    if i > 0 {
9594                        f.write_str(", ")?;
9595                    }
9596                    let s = format!("{x}");
9597                    // Mirror Float Display: force a dot so re-parse stays
9598                    // numerically literal.
9599                    if s.contains('.') || s.contains('e') || s.contains('E') {
9600                        f.write_str(&s)?;
9601                    } else {
9602                        write!(f, "{s}.0")?;
9603                    }
9604                }
9605                f.write_str("]")
9606            }
9607            Self::Interval { text, .. } => {
9608                f.write_str("INTERVAL '")?;
9609                for c in text.chars() {
9610                    if c == '\'' {
9611                        f.write_str("''")?;
9612                    } else {
9613                        write!(f, "{c}")?;
9614                    }
9615                }
9616                f.write_str("'")
9617            }
9618        }
9619    }
9620}
9621
9622impl fmt::Display for BinOp {
9623    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9624        f.write_str(match self {
9625            Self::Or => "OR",
9626            Self::And => "AND",
9627            Self::Eq => "=",
9628            Self::NotEq => "<>",
9629            Self::IsDistinctFrom => "IS DISTINCT FROM",
9630            Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
9631            Self::IntDiv => "DIV",
9632            Self::Lt => "<",
9633            Self::LtEq => "<=",
9634            Self::Gt => ">",
9635            Self::GtEq => ">=",
9636            Self::Add => "+",
9637            Self::Sub => "-",
9638            Self::Mul => "*",
9639            Self::Div => "/",
9640            Self::Mod => "%",
9641            Self::L2Distance => "<->",
9642            Self::GeomParallel => "?||",
9643            Self::OverLeft => "&<",
9644            Self::OverRight => "&>",
9645            Self::GeomPerp => "?-|",
9646            Self::GeomSameAs => "~=",
9647            Self::ClosestPoint => "##",
9648            Self::GeomHoriz => "?-",
9649            Self::InnerProduct => "<#>",
9650            Self::CosineDistance => "<=>",
9651            Self::Concat => "||",
9652            Self::BitOr => "|",
9653            Self::BitAnd => "&",
9654            Self::BitXor => "#",
9655            Self::LogicalXor => "xor",
9656            Self::JsonGet => "->",
9657            Self::JsonGetText => "->>",
9658            Self::JsonGetPath => "#>",
9659            Self::JsonGetPathText => "#>>",
9660            Self::JsonContains => "@>",
9661            Self::JsonPathExists => "@?",
9662            Self::JsonContainedBy => "<@",
9663            Self::JsonKeyExists => "?",
9664            Self::JsonKeysAny => "?|",
9665            Self::JsonKeysAll => "?&",
9666            Self::JsonDeletePath => "#-",
9667            Self::TsMatch => "@@",
9668            Self::InetContainedBy => "<<",
9669            Self::InetContainedByEq => "<<=",
9670            Self::InetContains => ">>",
9671            Self::InetContainsEq => ">>=",
9672            Self::InetOverlap => "&&",
9673            Self::Intersects => "?#",
9674            Self::IsBelow => "<^",
9675            Self::IsAbove => ">^",
9676            Self::PatternLt => "~<~",
9677            Self::PatternLtEq => "~<=~",
9678            Self::PatternGt => "~>~",
9679            Self::PatternGtEq => "~>=~",
9680        })
9681    }
9682}
9683
9684/// Quote `s` as a PG double-quoted identifier when required (keyword,
9685/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
9686/// Otherwise return it as-is. Returns an owned `String` to keep the call site
9687/// uniform.
9688pub(crate) fn quote_ident(s: &str) -> String {
9689    let needs_quote = match s.chars().next() {
9690        None => true,
9691        Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
9692        _ => {
9693            s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
9694                || s.chars().any(|c| c.is_ascii_uppercase())
9695                || is_keyword(s)
9696        }
9697    };
9698    if !needs_quote {
9699        return s.to_string();
9700    }
9701    let mut out = String::with_capacity(s.len() + 2);
9702    out.push('"');
9703    for c in s.chars() {
9704        if c == '"' {
9705            out.push_str("\"\"");
9706        } else {
9707            out.push(c);
9708        }
9709    }
9710    out.push('"');
9711    out
9712}
9713
9714fn is_keyword(s: &str) -> bool {
9715    matches!(
9716        &*s.to_ascii_lowercase(),
9717        "select"
9718            | "from"
9719            | "where"
9720            | "as"
9721            | "null"
9722            | "true"
9723            | "false"
9724            | "and"
9725            | "or"
9726            | "not"
9727            | "create"
9728            | "table"
9729            | "insert"
9730            | "into"
9731            | "values"
9732            | "index"
9733            | "on"
9734            | "begin"
9735            | "commit"
9736            | "rollback"
9737            | "is"
9738            | "between"
9739            | "in"
9740            | "like"
9741            | "group"
9742            | "distinct"
9743            | "union"
9744            | "all"
9745            | "join"
9746            | "inner"
9747            | "left"
9748            | "cross"
9749            | "outer"
9750            | "default"
9751            | "savepoint"
9752            | "release"
9753            | "to"
9754            | "having"
9755            | "show"
9756            | "extract"
9757            | "offset"
9758            | "asc"
9759            | "desc"
9760            | "interval"
9761    )
9762}
9763
9764#[cfg(test)]
9765mod tests {
9766    use super::*;
9767    use alloc::vec;
9768
9769    #[test]
9770    fn integer_literal_renders_without_dot() {
9771        assert_eq!(Literal::Integer(42).to_string(), "42");
9772    }
9773
9774    #[test]
9775    fn integral_float_keeps_dot() {
9776        assert_eq!(Literal::Float(1.0).to_string(), "1.0");
9777        assert_eq!(Literal::Float(1.5).to_string(), "1.5");
9778        assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
9779    }
9780
9781    #[test]
9782    fn string_literal_doubles_quote() {
9783        assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
9784    }
9785
9786    #[test]
9787    fn bool_and_null_render_uppercase() {
9788        assert_eq!(Literal::Bool(true).to_string(), "TRUE");
9789        assert_eq!(Literal::Bool(false).to_string(), "FALSE");
9790        assert_eq!(Literal::Null.to_string(), "NULL");
9791    }
9792
9793    #[test]
9794    fn binary_op_always_parenthesised() {
9795        let e = Expr::Binary {
9796            lhs: Box::new(Expr::Literal(Literal::Integer(1))),
9797            op: BinOp::Add,
9798            rhs: Box::new(Expr::Literal(Literal::Integer(2))),
9799        };
9800        assert_eq!(e.to_string(), "(1 + 2)");
9801    }
9802
9803    #[test]
9804    fn select_star_from_table() {
9805        let s = SelectStatement {
9806            locking: None,
9807            items: vec![SelectItem::Wildcard],
9808            from: Some(FromClause {
9809                primary: TableRef {
9810                    name: "users".into(),
9811                    alias: None,
9812                    only: false,
9813                    as_of_segment: None,
9814                    unnest_expr: None,
9815                    unnest_column_aliases: Vec::new(),
9816                    with_ordinality: false,
9817                    generate_series_args: None,
9818                    lateral_subquery: None,
9819                    jsonb_each_text_arg: None,
9820                    table_fn_call: None,
9821                    rows_from: None,
9822                    json_table: None,
9823                    scalar_fn_item: false,
9824                },
9825                joins: vec![],
9826            }),
9827            where_: None,
9828            group_by: None,
9829            group_by_all: false,
9830            having: None,
9831            unions: vec![],
9832            order_by: Vec::new(),
9833            limit: None,
9834            offset: None,
9835            limit_with_ties: false,
9836            window_check_exprs: Vec::new(),
9837            distinct: false,
9838            distinct_on: Vec::new(),
9839            ctes: vec![],
9840        };
9841        assert_eq!(s.to_string(), "SELECT * FROM users");
9842    }
9843
9844    #[test]
9845    fn quote_ident_for_uppercase_and_keyword() {
9846        assert_eq!(quote_ident("foo"), "foo");
9847        assert_eq!(quote_ident("Foo"), "\"Foo\"");
9848        assert_eq!(quote_ident("select"), "\"select\"");
9849        assert_eq!(quote_ident(""), "\"\"");
9850        assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
9851    }
9852}