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