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