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