Skip to main content

spg_sql/
ast.rs

1//! AST for the PG-dialect subset SPG accepts in v0.2.
2//!
3//! `Display` is implemented so that for any AST `a` produced by [`crate::parser`],
4//! re-parsing `format!("{a}")` yields a structurally equal AST. Binary and
5//! unary operators always emit parentheses to remove any precedence
6//! ambiguity — round-trip safety wins over prettiness.
7
8use alloc::boxed::Box;
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use core::fmt;
13
14/// `COPY … TO STDOUT` output format. `text` is PG's default
15/// (tab-separated, `\N` nulls, backslash escapes); `csv` follows
16/// RFC-4180-style quoting.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum CopyFormat {
19    #[default]
20    Text,
21    Csv,
22}
23
24/// Options for `COPY … TO STDOUT [WITH] (…)`. Defaults reproduce the
25/// bare `COPY … TO STDOUT` text-format behaviour, so an empty option
26/// list is a no-op. `delimiter` / `null_str` / `quote` fall back to the
27/// per-format defaults (text: `\t` / `\N`; csv: `,` / `` / `"`) when
28/// unset.
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct CopyOptions {
31    pub format: CopyFormat,
32    pub header: bool,
33    pub delimiter: Option<char>,
34    pub null_str: Option<String>,
35    pub quote: Option<char>,
36    /// v7.39 (round 247) — CSV `ESCAPE`: the character that precedes a
37    /// quote (or itself) inside a quoted cell. Defaults to the quote
38    /// character (PG's doubling behavior).
39    pub escape: Option<char>,
40    /// v7.39 (round 247) — CSV `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`:
41    /// columns whose non-NULL cells always quote. `Some(vec![])` is the
42    /// `*` spelling (every column).
43    pub force_quote: Option<Vec<String>>,
44    /// v7.39 (round 265) — CSV `FORCE_NOT_NULL (col, …)`: for these
45    /// columns an UNQUOTED empty field reads as the empty string rather
46    /// than NULL (probed). COPY FROM only.
47    pub force_not_null: Option<Vec<String>>,
48    /// v7.39 (round 265) — CSV `FORCE_NULL (col, …)`: for these columns
49    /// a QUOTED empty field (`""`) also reads as NULL (probed). COPY
50    /// FROM only.
51    pub force_null: Option<Vec<String>>,
52}
53
54/// v7.39 (round 218) — FETCH / MOVE cursor direction. PG grammar: single-row
55/// forms (NEXT / PRIOR / FIRST / LAST / ABSOLUTE n / RELATIVE n) return at
56/// most one row; multi-row forms (bare n / ALL / FORWARD [n|ALL] /
57/// BACKWARD [n|ALL]) stream a run. A negative bare/FORWARD count means
58/// BACKWARD (normalized at execution).
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum CursorDirection {
61    Next,
62    Prior,
63    First,
64    Last,
65    Absolute(i64),
66    Relative(i64),
67    /// Bare `FETCH n` / `FORWARD n` (negative = backward n).
68    Count(i64),
69    /// `ALL` / `FORWARD ALL`.
70    All,
71    Backward(i64),
72    BackwardAll,
73}
74
75impl fmt::Display for CursorDirection {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::Next => f.write_str("NEXT"),
79            Self::Prior => f.write_str("PRIOR"),
80            Self::First => f.write_str("FIRST"),
81            Self::Last => f.write_str("LAST"),
82            Self::Absolute(n) => write!(f, "ABSOLUTE {n}"),
83            Self::Relative(n) => write!(f, "RELATIVE {n}"),
84            Self::Count(n) => write!(f, "FORWARD {n}"),
85            Self::All => f.write_str("ALL"),
86            Self::Backward(n) => write!(f, "BACKWARD {n}"),
87            Self::BackwardAll => f.write_str("BACKWARD ALL"),
88        }
89    }
90}
91
92/// v7.39 (round 320, V53) — what a `DISCARD` throws away.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum DiscardTarget {
95    All,
96    Plans,
97    Sequences,
98    Temp,
99}
100
101impl fmt::Display for DiscardTarget {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.write_str(match self {
104            Self::All => "ALL",
105            Self::Plans => "PLANS",
106            Self::Sequences => "SEQUENCES",
107            Self::Temp => "TEMP",
108        })
109    }
110}
111
112/// v7.39 (round 535) — which maintenance statement, and therefore what
113/// its target names. Measured on PG18: INDEX / TABLE / CLUSTER name a
114/// relation, SCHEMA names a schema, and SYSTEM / DATABASE name neither
115/// in a way SPG can refuse.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum MaintainKind {
118    ReindexRelation,
119    ReindexSchema,
120    /// `REINDEX SYSTEM` / `REINDEX DATABASE`, and a bare `CLUSTER`.
121    Whole,
122    ClusterRelation,
123}
124
125/// v7.39 (round 547) — see [`Statement::SetDbRoleSetting`]. Boxed in the
126/// enum so the variant costs one pointer.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SetDbRoleSettingStatement {
129    pub database: Option<String>,
130    pub role: Option<String>,
131    pub param: Option<String>,
132    pub value: Option<String>,
133}
134
135/// v7.39 (round 696) — which operand a [`Statement::ValidateOnly`] names,
136/// and therefore which catalog answers whether it exists.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ValidateOnlyKind {
139    /// `LOCK TABLE <t> [, …]` — the relation must exist.
140    LockTable,
141    /// Every role named must exist: `DROP OWNED BY <r> [, …]`,
142    /// `REASSIGN OWNED BY <r> [, …] TO <r>`, and (round 697)
143    /// `SET SESSION AUTHORIZATION <r>`.
144    RoleName,
145    /// `SECURITY LABEL …` — PG refuses unconditionally, because no label
146    /// provider is loaded. SPG has none either.
147    SecurityLabel,
148    /// v7.39 (round 697) — `CREATE EXTENSION <e>`: the extension must be
149    /// AVAILABLE (PG: `extension "x" is not available`).
150    ExtensionAvailable,
151    /// v7.39 (round 708) — `ALTER TYPE <t> <any no-op form>`: the TYPE must
152    /// exist (PG: `type "x" does not exist`); the action itself stays a
153    /// no-op (PG genuinely renames; that residual is recorded).
154    TypeName,
155    /// v7.39 (round 708) — `ALTER AGGREGATE name(args) …`: names[0] is the
156    /// aggregate, the rest its argument type names (`*` = the `(*)` form).
157    /// Existence only; the action no-ops (PG really renames built-ins —
158    /// measured — and SPG does not model that).
159    AggregateName,
160    /// v7.39 (round 708) — `DROP CONVERSION <c>`: SPG ships no conversions,
161    /// so every name answers PG's `conversion "x" does not exist`.
162    ConversionName,
163    /// v7.39 (round 708) — `DROP LANGUAGE <l>`: an unknown language does
164    /// not exist; a shipped one is required (PG's two wordings, measured).
165    LanguageName,
166    /// v7.39 (round 709) — a collation name: performable or PG's
167    /// `collation "x" for encoding "UTF8" does not exist`.
168    CollationName,
169    /// v7.39 (round 709) — a text search configuration name.
170    TsConfigName,
171    /// v7.39 (round 709) — an event trigger name. SPG has none, so the
172    /// not-found answer is total.
173    EventTriggerName,
174    /// v7.39 (round 709) — a tablespace name. SPG has none beyond PG's two
175    /// built-ins, whose drop PG refuses with `permission denied` (measured).
176    TablespaceName,
177    /// v7.39 (round 709) — a large-object oid (names[0], decimal). The
178    /// registry is real (round 287), so the check is a lookup.
179    LargeObjectOid,
180    /// v7.39 (round 706) — `CREATE SERVER` / `CREATE FOREIGN TABLE` /
181    /// `CREATE FOREIGN DATA WRAPPER`. SPG has no foreign-data
182    /// infrastructure at all, so PG's refusals (`foreign-data wrapper "x"
183    /// does not exist`, `server "x" does not exist`) cannot be copied —
184    /// PG can refuse because the missing piece is installable there.
185    /// Accepted with a WARNING, the extension resolution (round 697):
186    /// refusing turns a dump that restores today into one that needs
187    /// editing, and silent acceptance was the actual defect.
188    ForeignInfra,
189    /// v7.39 (round 697) — `DROP EXTENSION <e>`: it must be installed
190    /// (PG: `extension "x" does not exist`).
191    ExtensionInstalled,
192}
193
194#[derive(Debug, Clone, PartialEq)]
195#[allow(clippy::large_enum_variant)] // Statement::Select dominates; Boxing would touch every match site
196pub enum Statement {
197    /// v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET <name>`.
198    ///
199    /// It used to be swallowed with the rest of the ALTER no-ops, which meant
200    /// `ALTER SYSTEM SET nosuch_guc = 1` was ACCEPTED where PG18 answers
201    /// `unrecognized configuration parameter`. SPG still applies nothing —
202    /// there is no postgresql.auto.conf to write — but a name it does not
203    /// know is now refused rather than swallowed.
204    ///
205    /// `None` is `RESET ALL`, which names no parameter.
206    AlterSystem {
207        parameter: Option<String>,
208    },
209    /// `DROP DATABASE [IF EXISTS] <name>`. SPG is single-database, so
210    /// this never succeeds; the name and the flag are carried so the
211    /// engine can answer with PG's wording for the two cases PG itself
212    /// has — an unknown name, or the database you are connected to.
213    DropDatabase {
214        name: String,
215        if_exists: bool,
216    },
217    /// A statement SPG accepts as a no-op but PG refuses inside a
218    /// transaction block — today `CREATE DATABASE` / `DROP DATABASE`,
219    /// which are no-ops here because SPG is single-database.
220    ///
221    /// The no-op path they used to share (`Statement::Empty`) also
222    /// carries CREATE ROLE, CREATE CAST and a dozen others that PG is
223    /// happy to run inside a transaction, so the object has to be named
224    /// to refuse the right ones.
225    NoOpPreventedInTransaction {
226        what: String,
227        /// v7.38.18 — `CREATE DATABASE … LC_COLLATE 'de_DE.utf8'` is in
228        /// every PostgreSQL bootstrap script there is, and SPG threw the
229        /// whole statement away. Being single-database makes the NAME a
230        /// no-op; it does not make the collation one, and a database
231        /// that quietly sorts by the container's `LANG` instead of the
232        /// one the script asked for is a silent difference in every
233        /// `ORDER BY` it will ever run.
234        ///
235        /// `LOCALE` and `LC_COLLATE` both land here; `LC_CTYPE` does
236        /// not, because SPG has no separate ctype.
237        collation: Option<String>,
238        /// v7.38.19 — the database's name, so `pg_database` can list one
239        /// that was created and can be connected to. It was thrown away
240        /// with the rest of the statement.
241        name: Option<String>,
242    },
243    /// v7.39 (round 696) — statements SPG performs nothing for, but whose
244    /// OPERAND PG validates before performing nothing either.
245    ///
246    /// All four used to be consumed whole by `is_dump_noise_statement`,
247    /// which meant `LOCK TABLE nosuch` and `DROP OWNED BY nosuchrole` were
248    /// ACCEPTED where PG18 errors. Accepting a statement that names
249    /// something that does not exist is the F29 shape: the caller is told
250    /// their intent was understood when the object it referred to is not
251    /// there.
252    ///
253    /// They share one variant because they share one rule — resolve the
254    /// name, refuse if absent, otherwise no-op — and four variants would be
255    /// four places for that rule to drift.
256    /// v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS] name(argtypes)[, …]`.
257    /// Consumed whole by the dump-noise list before, so `DROP AGGREGATE
258    /// nosuch(int)` reported success. PG validates every named aggregate's
259    /// EXISTENCE first (measured: a list with one unknown fails on the
260    /// unknown even when an earlier entry exists), renders the signature
261    /// with canonical type names (`int` → `integer`), and refuses to drop a
262    /// built-in (`cannot drop function sum(integer) because it is required
263    /// by the database system`). Every SPG aggregate is a built-in, so the
264    /// outcome is one of those two errors — or the IF EXISTS no-op.
265    ///
266    /// `args` holds the argument type names as written; `None` is the
267    /// `(*)` spelling.
268    DropAggregate {
269        if_exists: bool,
270        items: Vec<(String, Option<Vec<String>>)>,
271    },
272    /// v7.39 (round 750) — `ALTER ROLE|USER <name> … PASSWORD 'x' |
273    /// PASSWORD NULL`. The one attribute of the no-op family with a
274    /// SECURITY consequence: it was silently dropped (ledgered r710),
275    /// so a rotated credential never rotated. `None` = PASSWORD NULL
276    /// (the role keeps existing but can no longer password-auth).
277    AlterRolePassword {
278        name: String,
279        password: Option<String>,
280    },
281    ValidateOnly {
282        kind: ValidateOnlyKind,
283        /// The names the statement referred to. Empty means the form names
284        /// nothing (`SECURITY LABEL`, whose refusal is unconditional).
285        names: Vec<String>,
286    },
287
288    /// v7.39 (round 547) — `ALTER ROLE … SET/RESET` and
289    /// `ALTER DATABASE … SET/RESET`: the GUC defaults a session picks up
290    /// when it starts. Both used to land in the pg_dump no-op tail, so
291    /// the statement reported success and changed nothing.
292    ///
293    /// `database` / `role` are `None` for PG's oid 0 — `ALTER ROLE ALL`
294    /// sets both to None. `param` is `None` for RESET ALL. `value` is
295    /// `None` for RESET of one parameter.
296    SetDbRoleSetting(Box<SetDbRoleSettingStatement>),
297    /// v7.39 (round 288) — `SET CONSTRAINTS { ALL | <name>… }
298    /// { DEFERRED | IMMEDIATE }`. `deferred` carries the timing; the
299    /// name list is not yet honoured (ALL is what pg_dump emits and
300    /// what a circular-FK restore needs), so a named form applies to
301    /// all deferrable constraints too rather than silently doing
302    /// nothing.
303    /// v7.39 (round 308) — `SET CONSTRAINTS { ALL | name [, …] }
304    /// { DEFERRED | IMMEDIATE }`. An empty `names` is the ALL form;
305    /// otherwise the timing applies only to the constraints listed.
306    SetConstraints {
307        names: Vec<String>,
308        deferred: bool,
309    },
310
311    /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
312    /// [CASCADE | RESTRICT]`. Engine removes the matching tables
313    /// (each one) from the catalog; IF EXISTS makes the drop
314    /// idempotent. CASCADE / RESTRICT trailers parsed silently
315    /// (SPG always cascades index drops on table drop).
316    DropTable {
317        names: Vec<String>,
318        if_exists: bool,
319    },
320    /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
321    /// matching index across whichever table holds it.
322    DropIndex {
323        name: String,
324        if_exists: bool,
325    },
326    /// v7.14.0 — empty / comment-only statement. The lexer strips
327    /// `--` line comments and `/* … */` block comments (including
328    /// the MySQL conditional `/*!NNNNN … */` form) before the
329    /// parser ever sees them; a SQL chunk that contains nothing
330    /// else lands here. Engine returns CommandOk no-op so
331    /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
332    /// wrapped in conditional comments, etc.) load cleanly.
333    /// v7.39 (round 277) — SQL-level `PREPARE <name> [(type, …)] AS
334    /// <stmt>`. Session-scoped; the body keeps its `$N` placeholders
335    /// and is substituted at EXECUTE time.
336    Prepare {
337        name: String,
338        /// Declared parameter type names, in order. Empty when the
339        /// `(type, …)` list was omitted (PG infers them).
340        param_types: Vec<String>,
341        body: alloc::boxed::Box<Statement>,
342        /// The statement's own source text, which
343        /// `pg_prepared_statements.statement` reports verbatim.
344        source: String,
345    },
346    /// v7.39 (round 277) — `EXECUTE <name> [(arg, …)]`.
347    Execute {
348        name: String,
349        args: Vec<Expr>,
350    },
351    /// v7.39 (round 277) — `DEALLOCATE {<name> | ALL}`. `None` = ALL.
352    Deallocate(Option<String>),
353    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
354    /// [(kind, …)] ON <col>, … FROM <table>`. SPG records the object so
355    /// dumps restore and reflection is honest; the planner does not
356    /// consult it yet.
357    CreateStatistics {
358        name: String,
359        if_not_exists: bool,
360        /// Requested kinds as PG's single letters (`d` ndistinct,
361        /// `f` dependencies, `m` mcv). Empty = PG's default set.
362        kinds: Vec<String>,
363        columns: Vec<String>,
364        table: String,
365    },
366    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
367    DropStatistics {
368        name: String,
369        if_exists: bool,
370    },
371    /// v7.39 (round 278) — `CALL <proc>(…)`. Parses; the engine
372    /// reports that the procedure does not exist, because SPG has no
373    /// procedure catalog. Carried as a statement rather than raised at
374    /// parse time so the failure is a missing OBJECT (42883), not a
375    /// syntax error.
376    Call(String),
377    /// v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'`. Same shape:
378    /// 2PC is unavailable, which PG itself reports when
379    /// `max_prepared_transactions` is 0.
380    PrepareTransaction(String),
381    Empty,
382    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE]
383    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. The
384    /// canonical driver path for streaming large result sets (psycopg2
385    /// named cursors, JDBC setFetchSize).
386    DeclareCursor {
387        name: String,
388        /// `None` = neither keyword (PG default: backward allowed when the
389        /// plan supports it — always, for SPG's materialized cursors);
390        /// `Some(true)` = SCROLL; `Some(false)` = NO SCROLL (backward
391        /// fetch errors 55000).
392        scroll: Option<bool>,
393        /// `WITH HOLD` — survives the creating transaction's COMMIT.
394        hold: bool,
395        query: Box<Statement>,
396    },
397    /// v7.39 (round 218) — `FETCH [<direction>] [FROM|IN] <name>`.
398    FetchCursor {
399        name: String,
400        direction: CursorDirection,
401    },
402    /// v7.39 (round 218) — `MOVE [<direction>] [FROM|IN] <name>`: FETCH
403    /// without returning rows; the command tag carries the move count.
404    MoveCursor {
405        name: String,
406        direction: CursorDirection,
407    },
408    /// v7.39 (round 218) — `CLOSE <name>` / `CLOSE ALL` (`None` = ALL).
409    CloseCursor {
410        name: Option<String>,
411    },
412    /// v7.39 (round 222) — `LISTEN <channel>`: subscribe this session to
413    /// async notifications on the channel.
414    Listen(String),
415    /// v7.39 (round 222) — `NOTIFY <channel> [, '<payload>']`. Delivered at
416    /// COMMIT (PG semantics: transactional, deduplicated within the tx);
417    /// immediately under autocommit.
418    Notify {
419        channel: String,
420        payload: Option<String>,
421    },
422    /// v7.39 (round 222) — `UNLISTEN <channel>` / `UNLISTEN *` (`None` = *).
423    Unlisten(Option<String>),
424    /// `COPY table [(cols)] TO STDOUT` — the engine renders the
425    /// visible rows in COPY text format (tab-separated, `\N`
426    /// nulls, backslash escapes) as a single-text-column result
427    /// set; the wire layer streams CopyData from it.
428    CopyTo {
429        table: String,
430        columns: Option<Vec<String>>,
431        /// v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT`: an
432        /// arbitrary SELECT/VALUES/CTE (a whole [`Statement`], so set-ops and
433        /// VALUES ride through unchanged) whose result set is streamed in COPY
434        /// format. `Some` overrides `table`/`columns` (which are empty then);
435        /// `None` is the classic `COPY <table> …` shape.
436        query: Option<Box<Statement>>,
437        /// v7.37.x — `WITH (FORMAT csv, HEADER, DELIMITER, NULL, QUOTE)`
438        /// and the legacy `WITH CSV HEADER …` spelling. Default =
439        /// text format, no header (bare `COPY … TO STDOUT`).
440        options: CopyOptions,
441    },
442    /// v7.39 (round 249) — `COPY table [(cols)] FROM '<path>' [(opts)]`.
443    /// The engine is no_std and cannot read the file itself: the host
444    /// (embedded / server / tooling) reads the path and hands the bytes to
445    /// `Engine::copy_from_buffer`. Dispatching this statement straight to
446    /// the engine reports that contract.
447    CopyFromFile {
448        table: String,
449        columns: Option<Vec<String>>,
450        path: String,
451        options: CopyOptions,
452    },
453    /// v7.39 (round 249/252) — `COPY <table> [(cols)] TO '<file>'` (and
454    /// the `COPY (<query>) TO '<file>'` form). The engine is no_std and
455    /// cannot write the file itself: the host renders the payload via
456    /// `Engine::copy_to_buffer` and writes the path.
457    CopyToFile {
458        table: String,
459        columns: Option<Vec<String>>,
460        query: Option<Box<Statement>>,
461        path: String,
462        options: CopyOptions,
463    },
464    Select(SelectStatement),
465    CreateTable(CreateTableStatement),
466    /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
467    /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
468    /// no-op so PG dumps that include extension declarations
469    /// (notably `pgvector`) load against SPG without splitting
470    /// init scripts. mailrs migration follow-up F3.
471    CreateExtension(String),
472    /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
473    /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
474    /// the engine executes it at top level (mailrs round-10
475    /// A.2). Pre-v7.16.2 the parser discarded the body and the
476    /// engine returned CommandOk — a SEV-1 silent no-op that
477    /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
478    /// $$` idempotent migrations into invisible no-ops.
479    DoBlock(PlPgSqlBlock),
480    CreateIndex(CreateIndexStatement),
481    Insert(InsertStatement),
482    /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
483    Update(UpdateStatement),
484    /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
485    Delete(DeleteStatement),
486    /// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ `MERGE` statement.
487    /// `MERGE INTO target [alias] USING source [alias] ON cond
488    /// WHEN MATCHED [AND cond] THEN { UPDATE SET … | DELETE | DO NOTHING }
489    /// WHEN NOT MATCHED [AND cond] THEN { INSERT (cols) VALUES (vals) | DO NOTHING }
490    /// [WHEN …]`. SPG v7.17 supports table-based source (subquery
491    /// source is a follow-up); BY SOURCE / BY TARGET and RETURNING
492    /// are also follow-ups.
493    Merge(MergeStatement),
494    /// v7.39 (round 169) — `VACUUM [(opts)] [FULL|FREEZE|VERBOSE|ANALYZE]
495    /// [<table>]`. Was a parse-time no-op from the pre-MVCC era; with the
496    /// in-place MVCC gate ON, tombstoned versions are REAL bloat and a
497    /// customer's manual VACUUM must actually reclaim. `analyze` mirrors
498    /// the `VACUUM ANALYZE` spelling.
499    Vacuum {
500        table: Option<String>,
501        analyze: bool,
502    },
503    /// `BEGIN` / `START TRANSACTION` — with an optional explicit
504    /// `ISOLATION LEVEL …` mode (`None` = use the session default). PG
505    /// applies the level for the duration of this transaction only.
506    Begin(TransactionModes),
507    Commit,
508    Rollback,
509    /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
510    /// stack so a later `ROLLBACK TO <name>` can undo just the work
511    /// since this point.
512    Savepoint(String),
513    /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
514    /// named savepoint and discard later savepoints. Does not end the
515    /// transaction.
516    RollbackToSavepoint(String),
517    /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
518    /// rolling back. Keeps the work done since then.
519    ReleaseSavepoint(String),
520    /// `SHOW TABLES` — return the list of tables in the catalog.
521    ShowTables,
522    /// v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES` /
523    /// `SHOW SCHEMAS`. SPG is single-database; the executor
524    /// returns the canonical MySQL set so the mysql / MariaDB
525    /// client populates its database selector.
526    ShowDatabases,
527    /// v7.39.2 — MySQL `USE <db>`.
528    ///
529    /// It parsed as `Empty` and did nothing at all, so `USE myapp;
530    /// SELECT DATABASE()` answered the same constant it answered before
531    /// — measured against MySQL 9.7.2, which answers `myapp`. SPG serves
532    /// ONE database and answers to any name (see `CREATE DATABASE`), so
533    /// this does not switch catalogs; it records the NAME, which is the
534    /// half a client can observe and the half the PostgreSQL wire has
535    /// tracked since v7.39 (`current_database()` names what the startup
536    /// message asked for).
537    UseDatabase(String),
538    /// v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE TABLE <t>`
539    /// returns a 2-column row `(Table, "Create Table")` carrying
540    /// the synthesized DDL. mysqldump emits this for every
541    /// table at scrape time.
542    ShowCreateTable(String),
543    /// v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES FROM <t>`
544    /// (also `SHOW INDEX`, `SHOW KEYS`).
545    ShowIndexes(String),
546    /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS`.
547    ShowStatus,
548    /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW VARIABLES`.
549    ShowVariables,
550    /// r1067 — MySQL `SHOW VARIABLES LIKE 'pattern'` (sysbench-tpcc
551    /// probes isolation with it at connect).
552    ShowVariablesLike(String),
553    /// v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
554    ShowProcesslist,
555    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
556    /// pgbouncer sends `DISCARD ALL` between pooled client sessions to make
557    /// the connection look brand new to the next client; it used to be
558    /// swallowed as dump noise, so nothing was discarded.
559    Discard(DiscardTarget),
560    /// v7.39 (round 318, V51) — MySQL `KILL [CONNECTION | QUERY] <expr>`.
561    /// The id is an expression because MariaDB accepts one
562    /// (`KILL connection_id()` is the documented way to drop your own
563    /// connection). `query_only` is the `QUERY` form: stop the target's
564    /// running statement but leave it connected.
565    Kill {
566        query_only: bool,
567        id: Box<Expr>,
568    },
569    /// `SHOW COLUMNS FROM <table>` — return one row per column with
570    /// its declared name / type / nullability.
571    ShowColumns(String),
572    /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
573    /// Role is optional; defaults to `readonly` when omitted.
574    CreateUser(CreateUserStatement),
575    /// `DROP USER 'name'` (v4.1). v7.39 (read01 round 58) — `IF EXISTS` is
576    /// carried through: PG skips with a NOTICE rather than erroring.
577    DropUser {
578        name: String,
579        if_exists: bool,
580    },
581    /// v7.39 (RLS) — `SET ROLE { name | NONE | DEFAULT }` / `RESET ROLE`.
582    /// `Some(name)` switches the session's effective role (drives
583    /// `current_user` and RLS enforcement); `None` resets to the login
584    /// identity (the Admin superuser).
585    SetRole(Option<String>),
586    /// v7.39 (read01 round 57) — `GRANT <privs> ON <object> TO <roles>`.
587    Grant(GrantStatement),
588    /// v7.39 (read01 round 57) — `REVOKE [GRANT OPTION FOR] <privs> ON
589    /// <object> FROM <roles>`.
590    Revoke(GrantStatement),
591    /// v7.39 (RLS) — `CREATE POLICY name ON table …`.
592    CreatePolicy(CreatePolicyStatement),
593    /// v7.39 (RLS) — `ALTER POLICY name ON table …`.
594    AlterPolicy(AlterPolicyStatement),
595    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
596    DropPolicy(DropPolicyStatement),
597    /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
598    ShowUsers,
599    /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
600    /// single-column text table describing the rewritten plan tree
601    /// for `inner`. `analyze` triggers an actual exec to attach
602    /// observed row counts and elapsed micros to each node.
603    Explain(ExplainStatement),
604    /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
605    /// Synchronous rebuild of an NSW index. With the optional
606    /// encoding clause, every stored cell at the indexed column is
607    /// also re-encoded through `coerce_value` before the new graph
608    /// builds.
609    AlterIndex(AlterIndexStatement),
610    /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
611    /// The only setting in v6.7.2 is `hot_tier_bytes`, which
612    /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
613    /// for the named table.
614    AlterTable(AlterTableStatement),
615    /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
616    /// The catalog row lives in `spg_publications`. Publisher-side
617    /// WAL filtering arrives in v6.1.5.
618    CreatePublication(CreatePublicationStatement),
619    /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
620    /// no-op when the publication does not exist.
621    DropPublication {
622        name: String,
623        /// v7.39 (round 754, F31-B4) — `IF EXISTS` quietly skips a
624        /// missing publication; the bare form refuses with PG's
625        /// sentence (PG18-measured — the old "silent no-op" note on
626        /// the executor was wrong).
627        if_exists: bool,
628    },
629    /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
630    /// publication ordered by name with `(name, scope_summary,
631    /// table_count)` columns. The scope summary is the human-
632    /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
633    /// TABLES EXCEPT …`; `table_count` is `NULL` for the
634    /// `AllTables` scope and the table-list length otherwise.
635    ShowPublications,
636    /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
637    /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
638    /// in `spg_subscriptions`; when the subscription is
639    /// `enabled = true` (default) the server spawns a
640    /// background worker that connects to `conn` and drains the
641    /// requested publication(s) into the local engine.
642    CreateSubscription(CreateSubscriptionStatement),
643    /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
644    /// PUBLICATION, silent no-op when absent. Stops the
645    /// associated worker thread before removing the row.
646    DropSubscription {
647        name: String,
648        /// v7.39 (round 754, F31-B4) — same contract as
649        /// [`Statement::DropPublication`].
650        if_exists: bool,
651    },
652    /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
653    /// subscription ordered by name with `(name, conn_str,
654    /// publications, enabled, last_received_pos)`.
655    ShowSubscriptions,
656    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
657    /// Blocks until the local server's apply position reaches
658    /// `<pos>` or `<ms>` elapses. Server-layer command: the
659    /// engine refuses it (`EngineError::Unsupported`) since
660    /// `lag_state` lives in `spg-server`'s `ServerState`.
661    WaitForWalPosition {
662        pos: u64,
663        /// `None` → wait forever; `Some(ms)` → return after `ms`
664        /// milliseconds even if the target isn't reached.
665        timeout_ms: Option<u64>,
666    },
667    /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
668    /// table; `ANALYZE <name>` re-stats just one. Populates
669    /// `spg_statistic` with per-column null_frac + n_distinct +
670    /// 100-bucket equi-depth histogram.
671    Analyze(Option<String>),
672    /// v7.39 (round 535) — `REINDEX { INDEX | TABLE | SCHEMA | DATABASE
673    /// | SYSTEM } [CONCURRENTLY] <name>` and `CLUSTER [VERBOSE]
674    /// [<table> [USING <index>]]`.
675    ///
676    /// SPG has neither index bloat nor a clustering order to rebuild, so
677    /// the work is a no-op — but PG VALIDATES the target, and both were
678    /// swallowed at parse time, so `REINDEX TABLE typo` reported success.
679    /// The name is carried now so the engine can say what PG says.
680    Maintain {
681        kind: MaintainKind,
682        /// `REINDEX … CONCURRENTLY`. Carried for the same reason as
683        /// [`CreateIndexStatement::concurrently`]: PG bars the
684        /// CONCURRENTLY form inside a transaction block and allows the
685        /// plain one.
686        concurrently: bool,
687        /// `None` for the whole-database forms, which name nothing.
688        target: Option<String>,
689    },
690    /// v7.37.17 (17.6 sibling) — `TRUNCATE [TABLE] [ONLY] <name>
691    /// [, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE |
692    /// RESTRICT]`. Clears every row from each named table. SPG's
693    /// SEQUENCE identity is per-table; RESTART IDENTITY reinitializes
694    /// the associated sequence to its starting value. CASCADE
695    /// currently walks direct FK-referring tables and truncates
696    /// them too (PG's semantics). The ONLY modifier (skip partitions)
697    /// and RESTRICT (default) are accepted with no effect since
698    /// SPG's declarative partitions are always truncated together.
699    Truncate {
700        tables: Vec<String>,
701        restart_identity: bool,
702        cascade: bool,
703        /// v7.39 (round 647) — `TRUNCATE ONLY t`. Absorbed as a no-op
704        /// since v7.14 on the reasoning that SPG's children are separate
705        /// relations a truncate does not descend into. Same reasoning
706        /// round 621 applied to `FROM ONLY`, and it stopped being true
707        /// for the same reason: measured, `TRUNCATE <inheritance parent>`
708        /// leaves the children's rows where PG empties them, and
709        /// `TRUNCATE ONLY <partitioned parent>` is silently accepted
710        /// where PG refuses it outright.
711        only: bool,
712    },
713    /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
714    /// BTree-cold indices and merges small cold-tier segments
715    /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
716    /// 4 MiB) into a single larger segment per (table, index).
717    /// `WHERE` predicate filtering on which tables to compact is
718    /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
719    /// v6.7.3 only supports the bare form.
720    CompactColdSegments,
721    /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
722    /// parameter on the engine; v7.12.1 honours
723    /// `default_text_search_config` (consumed by `to_tsvector` /
724    /// `plainto_tsquery` family when called without an explicit
725    /// config arg). All other names are accepted as a no-op so PG
726    /// dumps with `SET client_encoding`, `SET search_path` etc.
727    /// load cleanly.
728    SetParameter {
729        name: String,
730        value: SetValue,
731        /// v7.38 (read01 P3.19) — `SET LOCAL` scopes the change to the
732        /// current transaction; the engine saves the prior value and
733        /// restores it at COMMIT / ROLLBACK. Plain `SET` (and `SET
734        /// SESSION`) leave this false and persist for the session.
735        local: bool,
736    },
737    /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
738    /// multi-assignment (mysqldump preamble uses
739    /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
740    /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
741    /// source order. Pairs whose LHS is a MySQL session/user
742    /// variable (`@VAR` / `@@VAR`) are recorded with the raw
743    /// name so the engine can ignore them; pairs whose LHS is
744    /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
745    /// go through the regular `set_session_param` path.
746    SetParameterList(Vec<(String, SetValue)>),
747    /// v7.39 (round 430) — MySQL's USER-defined variables:
748    /// `SET @x = 5, @s := CONCAT('a','b')`. Distinct from
749    /// [`Self::SetParameter`] (a `@@`-style engine/session setting) in
750    /// every way that matters: the value is an arbitrary EXPRESSION, the
751    /// name lives in its own per-session namespace, and reading an unset
752    /// one answers NULL rather than raising. `:=` and `=` are the same
753    /// assignment here.
754    ///
755    /// Before this the parser stripped every `@`, so `@x` and `@@x` were
756    /// the same node: `SET @x = 5` silently landed in the session-parameter
757    /// store where nothing could read it back, and `SELECT @x` failed with
758    /// "Unknown system variable".
759    /// v7.39 (round 554) — `SET @a = …, SETTING = …`.
760    ///
761    /// `settings` is the trailing half a mysqldump preamble writes:
762    /// `SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'`
763    /// saves a value and changes it in one statement. The parser used
764    /// to refuse the mixture outright, so no mysqldump could be
765    /// restored past its preamble.
766    SetUserVars(Vec<(String, Expr)>, Vec<(String, Expr)>),
767    /// v7.38 轴 4 — `SET [SESSION] TRANSACTION ISOLATION LEVEL …`
768    /// (plus optional READ ONLY / READ WRITE / DEFERRABLE clauses
769    /// silently accepted). PG-standard surface for picking an
770    /// isolation level. Engine tracks the value on
771    /// `Engine::current_isolation_level()`; actual MVCC / SSI
772    /// semantics implementation lands separately. PG itself maps
773    /// READ UNCOMMITTED to READ COMMITTED; SPG mirrors that —
774    /// effectively every level reads as READ COMMITTED in v7.37.8.
775    SetTransaction {
776        modes: TransactionModes,
777    },
778    /// v7.38 轴 4 — `SHOW <param>` returns a 1-column 1-row result
779    /// with the parameter's current value as TEXT. Today the only
780    /// recognised param is `transaction_isolation`; further
781    /// surfaces (`search_path`, `application_name`, …) land as the
782    /// session-parameter inventory grows.
783    ShowParameter(String),
784    /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
785    /// to its default. No-op for parameters SPG does not track.
786    ResetParameter(Option<String>),
787    /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
788    /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
789    /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
790    /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
791    /// languages parse but error at exec time with a clear
792    /// unsupported message.
793    CreateFunction(CreateFunctionStatement),
794    /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
795    /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
796    /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
797    /// triggers and column-list / WHEN clauses are out of scope
798    /// for v7.12.4.
799    CreateTrigger(CreateTriggerStatement),
800    /// v7.39 (round 139) — `CREATE RULE name AS ON event TO table [WHERE cond]
801    /// DO [ALSO|INSTEAD] { NOTHING | command }` query-rewrite rule.
802    CreateRule(CreateRuleStatement),
803    /// v7.39 (round 139) — `DROP RULE [IF EXISTS] name ON table`.
804    DropRule {
805        name: String,
806        table: String,
807        if_exists: bool,
808    },
809    /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
810    /// no-op when missing if `IF EXISTS` is set.
811    DropTrigger {
812        name: String,
813        table: String,
814        if_exists: bool,
815    },
816    /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
817    /// DROP TRIGGER but global (no table scope).
818    DropFunction {
819        name: String,
820        /// v7.39 (read01 round 62) — the argument TYPES, when the statement gave
821        /// them: `DROP FUNCTION f(int)` drops that overload only. `None` = no
822        /// argument list, which PG accepts only when the name is unambiguous.
823        args: Option<Vec<String>>,
824        if_exists: bool,
825    },
826    /// v7.17.0 — `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] name
827    /// [AS data_type]
828    /// [INCREMENT [BY] n]
829    /// [MINVALUE n | NO MINVALUE]
830    /// [MAXVALUE n | NO MAXVALUE]
831    /// [START [WITH] n]
832    /// [CACHE n]
833    /// [[NO] CYCLE]
834    /// [OWNED BY {table.col | NONE}]`.
835    /// Closes the round-7+ silent-no-op SEQUENCE story so pg_dump
836    /// emits + nextval/currval/setval downstream all work.
837    CreateSequence(CreateSequenceStatement),
838    /// v7.17.0 — `ALTER SEQUENCE [IF EXISTS] name <options>` with
839    /// the same option grammar as CREATE SEQUENCE, plus
840    /// `RESTART [WITH n]` and `OWNED BY ...` re-attach.
841    AlterSequence(AlterSequenceStatement),
842    /// v7.17.0 — `DROP SEQUENCE [IF EXISTS] name [, name…]
843    /// [CASCADE | RESTRICT]`. CASCADE / RESTRICT trailers parsed
844    /// silently (no FK on sequences).
845    DropSequence {
846        names: Vec<String>,
847        if_exists: bool,
848    },
849    /// v7.17.0 Phase 1.2 — `CREATE [OR REPLACE] [TEMPORARY] VIEW
850    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …>`. Closes the
851    /// silent-no-op VIEW story from the v7.17 customer-readiness
852    /// audit: pre-v7.17 SPG parsed CREATE VIEW as Statement::Empty
853    /// so any downstream `SELECT FROM v` errored with table-not-
854    /// found. The view body is stored verbatim; SELECT FROM <v>
855    /// rewrites at exec-time by prepending the view body as a
856    /// synthetic CTE.
857    CreateView(CreateViewStatement),
858    /// v7.17.0 Phase 1.2 — `DROP VIEW [IF EXISTS] name [, name…]
859    /// [CASCADE | RESTRICT]`. Removes the matching view from the
860    /// catalog; CASCADE/RESTRICT parsed silently.
861    DropView {
862        names: Vec<String>,
863        if_exists: bool,
864    },
865    /// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW [IF NOT
866    /// EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
867    /// Closes the silent-no-op MATERIALIZED VIEW story. Storage
868    /// model: the materialised result lives as a regular table
869    /// with the matching name + a parallel
870    /// `materialized_views` registry mapping name → body source
871    /// (used by REFRESH).
872    CreateMaterializedView(CreateMaterializedViewStatement),
873    /// v7.17.0 Phase 1.3 — `REFRESH MATERIALIZED VIEW name [WITH
874    /// [NO] DATA]`. Re-runs the stored body and replaces the
875    /// cached rows. `WITH NO DATA` truncates without re-running.
876    RefreshMaterializedView {
877        name: String,
878        with_data: bool,
879    },
880    /// v7.17.0 Phase 1.3 — `DROP MATERIALIZED VIEW [IF EXISTS]
881    /// name [, name…] [CASCADE | RESTRICT]`. Drops both the
882    /// backing table and the source registry entry.
883    DropMaterializedView {
884        names: Vec<String>,
885        if_exists: bool,
886    },
887    /// v7.17.0 Phase 1.4 — `CREATE TYPE name AS ENUM ('a', 'b',
888    /// …)`. Closes the silent-no-op CREATE TYPE story so PG
889    /// dumps that declare enum types load with real constraints
890    /// instead of becoming free-form TEXT. Future kinds
891    /// (composite / range / domain) extend the inner `kind`
892    /// enum.
893    CreateType(CreateTypeStatement),
894    /// v7.37 D.55 — `ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
895    /// [{BEFORE | AFTER} 'existing']`. Extends an enum's label list so
896    /// enum evolution stops being a silent no-op. `position` is
897    /// `Some((is_before, anchor))`.
898    /// v7.39 (read01 round 49) — `ALTER TYPE t RENAME VALUE 'old' TO 'new'`.
899    /// Used to be swallowed by the ALTER TYPE no-op tail, so the rename was
900    /// accepted and silently ignored.
901    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
902    /// Used to be swallowed as dump noise, so a comment was accepted and lost
903    /// (and obj_description / col_description always returned NULL).
904    /// `kind` is lowercase ("table" / "column" / "index" / …); for a column
905    /// `name` is the dotted `table.column`. `comment: None` = `IS NULL` = remove.
906    CommentOn {
907        kind: String,
908        name: String,
909        comment: Option<String>,
910    },
911    AlterTypeRenameValue {
912        type_name: String,
913        old: String,
914        new: String,
915    },
916    AlterTypeAddValue {
917        type_name: String,
918        label: String,
919        if_not_exists: bool,
920        position: Option<(bool, String)>,
921    },
922    /// v7.17.0 Phase 1.4 — `DROP TYPE [IF EXISTS] name [, name…]
923    /// [CASCADE | RESTRICT]`. Removes the matching enum/domain
924    /// from the catalog.
925    DropType {
926        names: Vec<String>,
927        if_exists: bool,
928    },
929    /// v7.17.0 Phase 1.5 — `CREATE DOMAIN name AS base_type
930    /// [DEFAULT expr] [NOT NULL | NULL] [CHECK (expr)]*`.
931    /// A DOMAIN is a named CHECK-constrained alias over a built-
932    /// in type. The CHECK + NOT NULL + DEFAULT clauses apply to
933    /// every column declared with the domain. Closes the
934    /// silent-no-op CREATE DOMAIN story so PG dumps that ship
935    /// validated identifier types (email, positive_int, …) keep
936    /// their guarantees.
937    CreateDomain(CreateDomainStatement),
938    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`. Every form was
939    /// previously swallowed by the catch-all DDL arm: the statement
940    /// reported success and did nothing, so a migration that dropped a
941    /// constraint kept rejecting the data it had just been told to
942    /// accept.
943    AlterDomain {
944        name: String,
945        action: AlterDomainAction,
946    },
947    /// v7.17.0 Phase 1.5 — `DROP DOMAIN [IF EXISTS] name
948    /// [, name…] [CASCADE | RESTRICT]`. Removes the matching
949    /// domain from the catalog.
950    DropDomain {
951        names: Vec<String>,
952        if_exists: bool,
953    },
954    /// v7.17.0 Phase 1.6 — `CREATE SCHEMA [IF NOT EXISTS]
955    /// name [AUTHORIZATION user]`. SPG is single-database;
956    /// schemas are tracked as a namespace registry so pg_dump
957    /// multi-schema declarations land cleanly and `SELECT *
958    /// FROM information_schema.schemata` returns real entries.
959    /// Schema-qualified `schema.table` references still strip
960    /// the prefix at lookup time per PG (schemas are not
961    /// isolation boundaries in v7.17 — see project-next-docket
962    /// for the v7.18+ isolation tracking).
963    CreateSchema {
964        name: String,
965        if_not_exists: bool,
966    },
967    /// v7.17.0 Phase 1.6 — `DROP SCHEMA [IF EXISTS] name
968    /// [, name…] [CASCADE | RESTRICT]`. Removes the schema
969    /// from the registry; built-in `public` / `pg_catalog` /
970    /// `information_schema` cannot be dropped.
971    DropSchema {
972        names: Vec<String>,
973        if_exists: bool,
974    },
975}
976
977/// v7.39 (round 260) — the `ALTER DOMAIN` actions SPG implements.
978#[derive(Debug, Clone, PartialEq)]
979pub enum AlterDomainAction {
980    AddConstraint { name: Option<String>, check: Expr },
981    DropConstraint { name: String, if_exists: bool },
982    SetDefault(Expr),
983    DropDefault,
984    SetNotNull,
985    DropNotNull,
986    RenameTo(String),
987}
988
989/// v7.17.0 Phase 1.5 — `CREATE DOMAIN` AST.
990#[derive(Debug, Clone, PartialEq)]
991pub struct CreateDomainStatement {
992    pub name: String,
993    /// Base type for the domain (one of the built-in
994    /// `ColumnTypeName` variants).
995    pub base_type: ColumnTypeName,
996    /// v7.39 (round 259) — `CREATE DOMAIN child AS parent …` where
997    /// `parent` is itself a DOMAIN. The parser already captured the
998    /// unknown type name; it just was not carried here, so the parent's
999    /// CHECK constraints were invisible and a value violating them was
1000    /// silently accepted. `base_type` still holds the ultimate scalar
1001    /// type, which is what the storage tier stores.
1002    pub base_domain: Option<String>,
1003    /// Optional `DEFAULT <expr>`. Resolved at engine-side
1004    /// CREATE TABLE time when a column is bound to this domain.
1005    pub default: Option<Expr>,
1006    /// `NOT NULL` from the domain definition. Engine ORs this
1007    /// with the column-level nullability so the strictest of the
1008    /// two wins (i.e. the column is non-nullable if either side
1009    /// says so).
1010    pub not_null: bool,
1011    /// Zero-or-more `CHECK (expr)` predicates. Each one is
1012    /// enforced as part of the column's CHECK list at INSERT /
1013    /// UPDATE time, with `VALUE` substituted for the column's
1014    /// current cell value.
1015    pub checks: Vec<Expr>,
1016}
1017
1018/// v7.17.0 Phase 1.4 — `CREATE TYPE` AST.
1019#[derive(Debug, Clone, PartialEq, Eq)]
1020pub struct CreateTypeStatement {
1021    pub name: String,
1022    pub kind: TypeKind,
1023}
1024
1025/// v7.17.0 Phase 1.4 — flavour of the new type. Only ENUM is
1026/// implemented; the variant set is open so Phase 1.5 (DOMAIN)
1027/// and later (COMPOSITE, RANGE) can land without an AST shape
1028/// migration.
1029///
1030/// v7.37.x (ζ-B Phase 1 composite accept) — added Composite for
1031/// `CREATE TYPE name AS (field_name field_type, …)`. Phase 1
1032/// stores the field list in the catalog so PG dumps that emit
1033/// `CREATE TYPE … AS (…)` don't error out; using a composite type
1034/// as a column type lands in Phase 2 (Value::Composite encoding +
1035/// ROW() literal + field-access syntax).
1036#[derive(Debug, Clone, PartialEq, Eq)]
1037pub enum TypeKind {
1038    /// `AS ENUM ('a', 'b', …)`. Order is preserved (PG enum
1039    /// labels are ordered).
1040    Enum { labels: Vec<String> },
1041    /// `AS (field_name field_type, …)`. Order matters; PG
1042    /// composite literals are positional.
1043    Composite {
1044        fields: Vec<(String, ColumnTypeName)>,
1045        /// v7.39 (round 264) — parallel to `fields`: the raw type NAME
1046        /// when a field's type is not a builtin (i.e. another composite).
1047        /// The parser already captures it; without carrying it here a
1048        /// nested composite field resolved to the Text placeholder and
1049        /// the inner record never became a record.
1050        field_user_types: Vec<Option<String>>,
1051    },
1052}
1053
1054/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
1055/// a string literal, an identifier (often a config name), an
1056/// integer/float, or the bare `DEFAULT` keyword.
1057#[derive(Debug, Clone, PartialEq)]
1058pub enum SetValue {
1059    String(String),
1060    Ident(String),
1061    Number(String),
1062    Default,
1063}
1064
1065/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1066/// at parse time and tracks the selected value on the engine. The
1067/// actual semantic differentiation (REPEATABLE READ snapshot,
1068/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1069/// today every level reads as effective READ COMMITTED (which is
1070/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1071/// READ COMMITTED). Default = `ReadCommitted`.
1072#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1073pub enum IsolationLevel {
1074    ReadUncommitted,
1075    #[default]
1076    ReadCommitted,
1077    RepeatableRead,
1078    Serializable,
1079}
1080
1081impl IsolationLevel {
1082    /// Canonical PG-style display name, as `SHOW transaction_isolation`
1083    /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1084    /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1085    /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1086    /// `read uncommitted`) and only BEHAVES as read committed; the old
1087    /// fold renamed the label too.
1088    /// v7.39 — the MySQL display name, which is NOT the PG one: MySQL
1089    /// hyphenates and upper-cases. Measured on MySQL 9.7.2 by setting
1090    /// each level and reading `@@transaction_isolation` back:
1091    /// `READ-UNCOMMITTED` / `READ-COMMITTED` / `REPEATABLE-READ` /
1092    /// `SERIALIZABLE` (the last has no hyphen because it is one word).
1093    ///
1094    /// This exists so the two MySQL surfaces cannot drift: both
1095    /// `SHOW VARIABLES` and `@@transaction_isolation` used to carry
1096    /// their own hard-coded literal, and the literals disagreed —
1097    /// one said `REPEATABLE-READ` while the engine ran read committed.
1098    /// v7.39 — parse what `default_transaction_isolation` holds. PG
1099    /// accepts the SQL spellings and stores them lower-cased with a
1100    /// space; anything else is not a level this understands and the
1101    /// caller keeps its own default rather than guessing.
1102    #[must_use]
1103    pub fn from_pg_name(name: &str) -> Option<Self> {
1104        match name.trim().to_ascii_lowercase().as_str() {
1105            "read uncommitted" => Some(Self::ReadUncommitted),
1106            "read committed" => Some(Self::ReadCommitted),
1107            "repeatable read" => Some(Self::RepeatableRead),
1108            "serializable" => Some(Self::Serializable),
1109            _ => None,
1110        }
1111    }
1112
1113    #[must_use]
1114    pub fn as_mysql_str(self) -> &'static str {
1115        match self {
1116            Self::ReadUncommitted => "READ-UNCOMMITTED",
1117            Self::ReadCommitted => "READ-COMMITTED",
1118            Self::RepeatableRead => "REPEATABLE-READ",
1119            Self::Serializable => "SERIALIZABLE",
1120        }
1121    }
1122
1123    pub fn as_pg_str(self) -> &'static str {
1124        match self {
1125            Self::ReadUncommitted => "read uncommitted",
1126            Self::ReadCommitted => "read committed",
1127            Self::RepeatableRead => "repeatable read",
1128            Self::Serializable => "serializable",
1129        }
1130    }
1131}
1132
1133impl core::fmt::Display for IsolationLevel {
1134    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1135        f.write_str(self.as_pg_str())
1136    }
1137}
1138
1139/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1140/// single fixed-shape DDL; the WITH-clause options PG supports
1141/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1142/// scope for v6.1.4 — `enabled` defaults to true and there are
1143/// no other knobs to set in v6.1.x.
1144#[derive(Debug, Clone, PartialEq, Eq)]
1145pub struct CreateSubscriptionStatement {
1146    pub name: String,
1147    /// Connection string in PG keyword=value form (e.g.
1148    /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1149    /// `host` and `port` fields; the rest is reserved for
1150    /// future v6.1.x options.
1151    pub conn_str: String,
1152    /// One or more publications on the remote side. Order is
1153    /// preserved verbatim from the DDL; the worker requests them
1154    /// in this order. v6.1.4 records the list; v6.1.5
1155    /// publisher-side filtering enforces it.
1156    pub publications: Vec<String>,
1157}
1158
1159/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1160#[derive(Debug, Clone, PartialEq, Eq)]
1161pub struct CreateSequenceStatement {
1162    pub name: String,
1163    pub if_not_exists: bool,
1164    pub temporary: bool,
1165    /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1166    pub data_type: Option<SequenceDataType>,
1167    pub options: SequenceOptions,
1168}
1169
1170/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1172pub enum SequenceDataType {
1173    SmallInt,
1174    Int,
1175    BigInt,
1176}
1177
1178/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1179/// All fields are optional. `min_value`/`max_value` carry
1180/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1181#[derive(Debug, Clone, Default, PartialEq, Eq)]
1182pub struct SequenceOptions {
1183    pub increment: Option<i64>,
1184    pub min_value: Option<SeqBound>,
1185    pub max_value: Option<SeqBound>,
1186    pub start: Option<i64>,
1187    /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1188    /// RESTART, `Some(Some(n))` = RESTART WITH n.
1189    pub restart: Option<Option<i64>>,
1190    pub cache: Option<i64>,
1191    pub cycle: Option<bool>,
1192    pub owned_by: Option<SequenceOwnedBy>,
1193}
1194
1195/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1197pub enum SeqBound {
1198    Value(i64),
1199    NoBound,
1200}
1201
1202/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1203#[derive(Debug, Clone, PartialEq, Eq)]
1204pub enum SequenceOwnedBy {
1205    None,
1206    Column { table: String, column: String },
1207}
1208
1209/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1210#[derive(Debug, Clone, PartialEq)]
1211pub struct CreateMaterializedViewStatement {
1212    pub name: String,
1213    pub if_not_exists: bool,
1214    /// Optional `(col, col, …)` rename list. Applies to the
1215    /// backing table at CREATE / REFRESH time.
1216    pub columns: Vec<String>,
1217    /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1218    /// the cached rows.
1219    pub body: SelectStatement,
1220    /// `WITH DATA` (default) = materialise the rows at CREATE
1221    /// time. `WITH NO DATA` = create an empty backing table;
1222    /// callers must REFRESH before SELECT returns rows.
1223    pub with_data: bool,
1224    /// v7.38 (read01 P6.49) — when true this node came from
1225    /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1226    /// executor creates a plain table and does NOT register it in the
1227    /// materialized-view registry (no REFRESH semantics).
1228    pub as_plain_table: bool,
1229    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1230    /// meaningful together with `as_plain_table`; the executor puts the
1231    /// resulting table in the creating session's namespace.
1232    pub temporary: bool,
1233}
1234
1235/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1236/// auto-updatable view. `Cascaded` is PG's default when the bare
1237/// `WITH CHECK OPTION` is written.
1238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1239pub enum ViewCheckOption {
1240    Local,
1241    Cascaded,
1242}
1243
1244/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1245#[derive(Debug, Clone, PartialEq)]
1246pub struct CreateViewStatement {
1247    pub name: String,
1248    pub or_replace: bool,
1249    pub if_not_exists: bool,
1250    pub temporary: bool,
1251    /// Optional `(col, col, …)` rename list. When non-empty,
1252    /// these override the body's projected column names per-
1253    /// position at SELECT-from-view time.
1254    pub columns: Vec<String>,
1255    /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1256    /// time to materialise the view as a synthetic CTE.
1257    pub body: SelectStatement,
1258    /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1259    /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1260    /// 44000). `None` = no check option.
1261    pub check_option: Option<ViewCheckOption>,
1262}
1263
1264/// v7.17.0 — `ALTER SEQUENCE` AST node.
1265#[derive(Debug, Clone, PartialEq, Eq)]
1266pub struct AlterSequenceStatement {
1267    pub name: String,
1268    pub if_exists: bool,
1269    pub options: SequenceOptions,
1270    /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1271    /// instead of `options`; the two forms are mutually exclusive in PG.
1272    pub rename_to: Option<String>,
1273}
1274
1275/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1276/// the [`PublicationScope`] shape. v6.1.2 only accepted
1277/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1278/// variants by flipping the parser gate (no AST migration).
1279#[derive(Debug, Clone, PartialEq, Eq)]
1280pub struct CreatePublicationStatement {
1281    pub name: String,
1282    pub scope: PublicationScope,
1283}
1284
1285/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1286/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1287/// variants — the on-disk shape, snapshot serialisation, and the
1288/// AST round-trip Display path were already in place in v6.1.2
1289/// so this is a parser-only widening.
1290#[derive(Debug, Clone, PartialEq, Eq)]
1291pub enum PublicationScope {
1292    AllTables,
1293    ForTables(Vec<String>),
1294    AllTablesExcept(Vec<String>),
1295    /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1296    /// (PG 15+). AST-only: the executor folds `public` to
1297    /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1298    /// and refuses any other schema with PG's sentence, so the
1299    /// catalog / serializer / replication filter never see it.
1300    TablesInSchema(String),
1301}
1302
1303#[derive(Debug, Clone, PartialEq, Eq)]
1304pub struct AlterIndexStatement {
1305    pub name: String,
1306    pub target: AlterIndexTarget,
1307}
1308
1309#[derive(Debug, Clone, PartialEq, Eq)]
1310pub enum AlterIndexTarget {
1311    /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1312    /// rebuilds the existing graph in place without touching the
1313    /// column encoding; `Some(enc)` re-encodes every cell first.
1314    Rebuild { encoding: Option<VecEncoding> },
1315    /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1316    /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1317    /// uses it to make the migration idempotent (re-running on a
1318    /// DB where the rename already happened is a no-op rather
1319    /// than an error).
1320    Rename { new: String, if_exists: bool },
1321    /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1322    /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1323    /// does not exist`), so the index is validated and the storage
1324    /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1325    /// SET/RESET arms already record).
1326    StorageParams,
1327}
1328
1329/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1330/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1331/// can add more SET subjects without changing the dispatch shape.
1332#[derive(Debug, Clone, PartialEq)]
1333pub struct AlterTableStatement {
1334    pub name: String,
1335    /// v7.13.2 — mailrs round-6 S1. One or more subactions
1336    /// separated by commas in the source SQL. PG-semantic apply
1337    /// is sequential; engine bails on first error (no
1338    /// transactional rollback of completed subactions in v7.13).
1339    /// Single-subaction shape stays a 1-element vec.
1340    pub targets: Vec<AlterTableTarget>,
1341}
1342
1343#[derive(Debug, Clone, PartialEq)]
1344#[allow(clippy::large_enum_variant)]
1345pub enum AlterTableTarget {
1346    /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1347    ///
1348    /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1349    /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1350    /// the reasoning went stale: `NO INHERIT` reported success while the
1351    /// child stayed attached, which is the worst kind of answer — the
1352    /// statement says it worked and the catalog disagrees.
1353    Inherit { parent: String, detach: bool },
1354    /// Per-table hot-tier byte budget override. The freezer
1355    /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1356    SetHotTierBytes(u64),
1357    /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1358    /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1359    /// Engine validates existing rows against the new constraint
1360    /// before installing it.
1361    AddForeignKey(ForeignKeyConstraint),
1362    /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1363    /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1364    /// no-op when no FK with that name exists; otherwise raises.
1365    DropForeignKey { name: String, if_exists: bool },
1366    /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1367    /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1368    /// as the standalone `DROP INDEX` statement.
1369    DropIndex { name: String, if_exists: bool },
1370    /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1371    /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1372    /// (20 migrate-*.sql hits). Engine appends the column to the
1373    /// schema and back-fills every existing row with the DEFAULT
1374    /// (or NULL when no DEFAULT and the column is nullable).
1375    AddColumn {
1376        column: ColumnDef,
1377        if_not_exists: bool,
1378    },
1379    /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1380    /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1381    /// existing row's column value by evaluating the optional
1382    /// USING expression (default `col::<ty>`) and re-coercing
1383    /// against the new column type.
1384    AlterColumnType {
1385        column: String,
1386        new_type: ColumnTypeName,
1387        using: Option<Expr>,
1388        /// v7.39 (round 713) — `COLLATE <name>` between the type and
1389        /// USING. PG re-collates the column, and an ABSENT clause RESETS
1390        /// the collation to the type default (measured round 713) — so
1391        /// `None` is not "leave it alone". The type parser consumed the
1392        /// clause all along and this surface dropped it on the floor:
1393        /// the statement succeeded and the ordering did not change, the
1394        /// silent-divergence shape. Folded variant + the name as written.
1395        collation: Option<(Collation, String)>,
1396    },
1397    /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1398    /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1399    /// every row's value at that position is removed; any index
1400    /// on the column is dropped. `if_exists` makes the drop a
1401    /// no-op when the column is missing. `cascade` removes
1402    /// dependents (FKs referencing the column, partial indexes
1403    /// whose predicate names the column); without it, the engine
1404    /// rejects when dependents exist.
1405    DropColumn {
1406        column: String,
1407        if_exists: bool,
1408        cascade: bool,
1409    },
1410    /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1411    /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1412    /// CONSTRAINT name CHECK (expr)` — table-level constraints
1413    /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1414    /// separate ALTER TABLE statement, so this surface lets the
1415    /// dump load straight through.
1416    AddTableConstraint(TableConstraint),
1417    /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1418    /// there is nothing to record; what PG does that SPG did not is
1419    /// REFUSE a role that does not exist. The name has to reach the
1420    /// engine for that, because only the engine knows the roles.
1421    OwnerTo { role: String },
1422    /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1423    /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1424    /// the hint is still a no-op; naming an index that does not exist is
1425    /// not.
1426    ClusterOn { index: Option<String> },
1427    /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1428    /// already in the table against a constraint added `NOT VALID` and,
1429    /// if they all pass, mark it validated. It used to be swallowed as a
1430    /// no-op on the theory that SPG validated at ADD time; SPG did not.
1431    ValidateConstraint { name: String },
1432    /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1433    /// Renames the column in the schema and propagates the rename
1434    /// to every stored source string that references it as a
1435    /// (potentially-qualified) column identifier: CHECK predicates,
1436    /// partial-index predicates, runtime DEFAULT expressions, and
1437    /// triggers' `UPDATE OF` column lists. Function bodies and
1438    /// trigger bodies are NOT auto-rewritten — they're loose
1439    /// source text and may contain references SPG can't statically
1440    /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1441    /// the column even if dependents exist; users renaming a
1442    /// column referenced by a function body update the function
1443    /// body separately.
1444    RenameColumn { old: String, new: String },
1445    /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1446    /// Reachable now that the schema stores user-supplied constraint names.
1447    RenameConstraint { old: String, new: String },
1448    /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1449    /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1450    /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1451    /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1452    /// (identity); both lower to this. SPG's auto-increment is
1453    /// max+1-scan based, so the dump's `setval(…)` calls stay
1454    /// no-ops without losing the sequence position.
1455    SetColumnAutoIncrement {
1456        column: String,
1457        /// The implicit sequence pg_dump names for an identity
1458        /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1459        /// nextval target for a serial default. The engine creates
1460        /// it if absent so the dump's later `setval(s, …)` lands.
1461        seq_name: Option<String>,
1462    },
1463    /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1464    /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1465    /// migrate-042 uses it). The engine moves the table entry
1466    /// in the catalog under the new name; child catalog state
1467    /// (FKs pointing at this table, triggers watching this
1468    /// table) tracks the rename through the storage layer.
1469    RenameTable { new: String },
1470    /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1471    /// { ALL | <name> }`. Toggles whether row-level triggers
1472    /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1473    /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1474    /// ENABLE epilogue around every table's data block so the
1475    /// rows already-computed in prod don't get re-rewritten
1476    /// (and so trigger-driven side effects like
1477    /// audit/queueing don't re-fire during a bulk reload).
1478    /// `which == TriggerSelector::All` toggles every trigger
1479    /// on the table; `Named(name)` toggles one trigger. The
1480    /// engine persists the disabled state on `TriggerDef.enabled`
1481    /// (catalog FILE_VERSION 25+) and the row-write paths skip
1482    /// the trigger when `!enabled`.
1483    SetTriggerEnabled {
1484        which: TriggerSelector,
1485        enabled: bool,
1486    },
1487    /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1488    /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1489    /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1490    /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1491    SetRowSecurity {
1492        enabled: Option<bool>,
1493        force: Option<bool>,
1494    },
1495    /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1496    /// <bounds>`. Promotes an existing table `child` to a partition
1497    /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1498    /// Engine validates that `child`'s columns are layout-compatible
1499    /// with `parent` and that every row in `child` satisfies the
1500    /// bound before installing the role.
1501    AttachPartition {
1502        child: String,
1503        bounds: PartitionOfBoundsAst,
1504    },
1505    /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1506    /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1507    /// to a standalone table (clears `partition_role`) and removes
1508    /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1509    /// is parser-accepted; engine performs the same atomic detach
1510    /// (single-engine, no replication lag — the PG semantics that
1511    /// require the two-phase split don't apply).
1512    DetachPartition {
1513        child: String,
1514        concurrently: bool,
1515        finalize: bool,
1516    },
1517    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1518    /// <expr>`. Engine re-parses + freezes the literal at this point,
1519    /// matching CREATE TABLE-side default semantics. Volatile shapes
1520    /// (`now()` / `nextval`) take the runtime-default path.
1521    AlterColumnSetDefault { column: String, default_expr: Expr },
1522    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1523    AlterColumnDropDefault { column: String },
1524    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1525    /// Engine validates that no existing row has NULL in that column
1526    /// before flipping the flag (PG semantics — partial NOT NULL
1527    /// would surface inconsistently).
1528    AlterColumnSetNotNull { column: String },
1529    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1530    AlterColumnDropNotNull { column: String },
1531    /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1532    /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1533    /// column's start value = 1). Engine records a next-value floor over
1534    /// SPG's max+1 identity allocation.
1535    AlterColumnRestart { column: String, with: Option<i64> },
1536    /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1537    /// EXPRESSION` turns a stored generated column into a plain column
1538    /// (its generation expression is removed; existing values are kept).
1539    AlterColumnDropExpression { column: String, if_exists: bool },
1540    /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1541    /// de-generate an identity column into a plain column.
1542    AlterColumnDropIdentity { column: String, if_exists: bool },
1543    /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1544    /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1545    /// expression and recomputes every existing row.
1546    AlterColumnSetExpression { column: String, expr: Expr },
1547    /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1548    /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1549    /// (PG: `type "x" does not exist`).
1550    OfType { type_name: String },
1551    /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1552    /// identity setting no-ops (SPG has no logical replication consumer);
1553    /// the INDEX must exist on this table (PG: `index "i" for table "t"
1554    /// does not exist`).
1555    ReplicaIdentityUsingIndex { index: String },
1556}
1557
1558/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1559/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1560/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1561/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1562/// shouldn't surface from a dump.
1563#[derive(Debug, Clone, PartialEq, Eq)]
1564pub enum TriggerSelector {
1565    /// Every trigger on the table.
1566    All,
1567    /// A specific trigger by name.
1568    Named(String),
1569}
1570
1571/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1572/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1573/// bitflags word or a nested options struct would only relocate the lint
1574/// while making the option each caller sets harder to read.
1575#[allow(clippy::struct_excessive_bools)]
1576#[derive(Debug, Clone, PartialEq)]
1577pub struct ExplainStatement {
1578    pub analyze: bool,
1579    /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1580    /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1581    /// `Insert on / Update on / Delete on` trees for them.
1582    pub inner: Box<Statement>,
1583    /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1584    /// advisor pass: after the regular plan tree, the engine
1585    /// emits one suggestion line per column referenced in the
1586    /// query's WHERE / JOIN that has no covering index on the
1587    /// owning table.
1588    pub suggest: bool,
1589    /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1590    /// `elapsed=…us` annotations from the Total line (and any
1591    /// future cost-bearing lines). PG-standard option used by
1592    /// regression suites and diff-friendly EXPLAIN output. When
1593    /// `true`, takes precedence over the per-session
1594    /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1595    pub costs_off: bool,
1596    /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1597    /// option that surfaces hot/cold/shared block counters. SPG's
1598    /// hot-tier scan path counts examined rows; the BUFFERS option
1599    /// makes that an explicit per-operator annotation.
1600    pub buffers: bool,
1601    /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1602    /// uses this to disable per-operator timing while still
1603    /// emitting actual-row counts (cheaper than ANALYZE). Default
1604    /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1605    /// timing portion of the Total line. Decoupled from `costs_off`:
1606    /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1607    /// measured wall-clock.
1608    pub timing_off: bool,
1609    /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1610    /// modified GUC values to the plan output. SPG emits the
1611    /// session params that diverge from default after the main
1612    /// plan body.
1613    pub settings: bool,
1614    /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1615    /// bytes / records / FPI emitted by the query. SPG's
1616    /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1617    /// ANALYZE) report against the engine WAL counter delta.
1618    pub wal: bool,
1619    /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1620    /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1621    /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1622    /// this is set.
1623    pub summary_off: bool,
1624    /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1625    /// PG's standard format selector. Default is text. JSON / XML
1626    /// / YAML emit a single-row TEXT result whose body wraps the
1627    /// existing line-per-operator text in the chosen container —
1628    /// PG-compatible just enough for dashboards that parse those
1629    /// container shapes (pgAdmin's JSON path picker, etc.).
1630    pub format: ExplainFormat,
1631}
1632
1633#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1634pub enum ExplainFormat {
1635    #[default]
1636    Text,
1637    Json,
1638    Xml,
1639    Yaml,
1640}
1641
1642/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1643#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1644pub enum PolicyCmd {
1645    All,
1646    Select,
1647    Insert,
1648    Update,
1649    Delete,
1650}
1651
1652/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1653/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1654#[derive(Debug, Clone, PartialEq)]
1655pub struct CreatePolicyStatement {
1656    pub name: String,
1657    pub table: String,
1658    /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1659    pub permissive: bool,
1660    pub cmd: PolicyCmd,
1661    /// Empty = PUBLIC.
1662    pub roles: Vec<String>,
1663    pub using: Option<Expr>,
1664    pub with_check: Option<Expr>,
1665}
1666
1667/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1668/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1669/// or the command (matches PG).
1670#[derive(Debug, Clone, PartialEq)]
1671pub struct AlterPolicyStatement {
1672    pub name: String,
1673    pub table: String,
1674    pub rename_to: Option<String>,
1675    pub roles: Option<Vec<String>>,
1676    pub using: Option<Expr>,
1677    pub with_check: Option<Expr>,
1678}
1679
1680/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1681#[derive(Debug, Clone, PartialEq, Eq)]
1682pub struct DropPolicyStatement {
1683    pub name: String,
1684    pub table: String,
1685    pub if_exists: bool,
1686}
1687
1688#[derive(Debug, Clone, PartialEq, Eq)]
1689pub struct CreateUserStatement {
1690    pub name: String,
1691    /// Empty when the statement carried no PASSWORD — legal for a bare
1692    /// `CREATE ROLE`, which cannot log in anyway.
1693    pub password: String,
1694    /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1695    /// the parser; the engine validates against `Role::parse` so a
1696    /// typo lands as a runtime error with a clear message rather than
1697    /// a parse failure.
1698    pub role: String,
1699    /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1700    /// statement did not say, so the default for its spelling applies:
1701    /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1702    /// both default to INHERIT and NOSUPERUSER.
1703    pub login: Option<bool>,
1704    pub inherit: Option<bool>,
1705    pub superuser: Option<bool>,
1706    /// `true` when spelled `CREATE USER` (LOGIN by default).
1707    pub is_user: bool,
1708}
1709
1710/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1711/// it tells the planner how far a call may be moved or folded. SPG records
1712/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1713/// yet exploit it for constant folding.
1714#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1715pub enum FunctionVolatility {
1716    Immutable,
1717    Stable,
1718    #[default]
1719    Volatile,
1720}
1721
1722impl FunctionVolatility {
1723    /// PG's one-character `pg_proc.provolatile` code.
1724    #[must_use]
1725    pub const fn as_pg_char(self) -> &'static str {
1726        match self {
1727            Self::Immutable => "i",
1728            Self::Stable => "s",
1729            Self::Volatile => "v",
1730        }
1731    }
1732
1733    #[must_use]
1734    pub const fn as_sql(self) -> &'static str {
1735        match self {
1736            Self::Immutable => "IMMUTABLE",
1737            Self::Stable => "STABLE",
1738            Self::Volatile => "VOLATILE",
1739        }
1740    }
1741}
1742
1743/// v7.39 (round 322, V46) — PG's parallel-safety class.
1744#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1745pub enum FunctionParallel {
1746    #[default]
1747    Unsafe,
1748    Restricted,
1749    Safe,
1750}
1751
1752impl FunctionParallel {
1753    /// PG's one-character `pg_proc.proparallel` code.
1754    #[must_use]
1755    pub const fn as_pg_char(self) -> &'static str {
1756        match self {
1757            Self::Unsafe => "u",
1758            Self::Restricted => "r",
1759            Self::Safe => "s",
1760        }
1761    }
1762
1763    #[must_use]
1764    pub const fn as_sql(self) -> &'static str {
1765        match self {
1766            Self::Unsafe => "PARALLEL UNSAFE",
1767            Self::Restricted => "PARALLEL RESTRICTED",
1768            Self::Safe => "PARALLEL SAFE",
1769        }
1770    }
1771}
1772
1773/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1774/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1775/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1776/// language's default cost / rows.
1777#[derive(Debug, Clone, Copy, PartialEq, Default)]
1778pub struct FunctionAttrs {
1779    pub volatility: FunctionVolatility,
1780    /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1781    /// argument returns NULL without running the body.
1782    pub strict: bool,
1783    pub security_definer: bool,
1784    pub leakproof: bool,
1785    pub parallel: FunctionParallel,
1786    /// `COST n` — `None` leaves PG's per-language default.
1787    pub cost: Option<f64>,
1788    /// `ROWS n` — set-returning functions only; `None` = default.
1789    pub rows: Option<f64>,
1790}
1791
1792impl FunctionAttrs {
1793    /// The attribute words `pg_get_functiondef` puts on their own line,
1794    /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1795    /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1796    /// at its default — PG then emits no such line at all.
1797    #[must_use]
1798    pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1799        let mut out = alloc::vec::Vec::new();
1800        if self.volatility != FunctionVolatility::Volatile {
1801            out.push(alloc::string::String::from(self.volatility.as_sql()));
1802        }
1803        if self.parallel != FunctionParallel::Unsafe {
1804            out.push(alloc::string::String::from(self.parallel.as_sql()));
1805        }
1806        if self.strict {
1807            out.push(alloc::string::String::from("STRICT"));
1808        }
1809        if self.security_definer {
1810            out.push(alloc::string::String::from("SECURITY DEFINER"));
1811        }
1812        if self.leakproof {
1813            out.push(alloc::string::String::from("LEAKPROOF"));
1814        }
1815        if let Some(c) = self.cost {
1816            out.push(alloc::format!("COST {}", render_attr_number(c)));
1817        }
1818        if let Some(r) = self.rows {
1819            out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1820        }
1821        out
1822    }
1823}
1824
1825/// PG prints a whole-numbered cost / rows without a decimal point.
1826fn render_attr_number(v: f64) -> alloc::string::String {
1827    // no_std: `f64::fract` lives in std, so compare against the truncation.
1828    let whole = v as i64;
1829    if v.abs() < 1e15 && (whole as f64) == v {
1830        alloc::format!("{whole}")
1831    } else {
1832        alloc::format!("{v}")
1833    }
1834}
1835
1836/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1837/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1838/// (the row-level trigger body the CREATE TRIGGER below references).
1839/// Non-trigger user-defined functions parse but error at execution
1840/// time with a clear unsupported message; that surface lands in
1841/// v7.12.5+.
1842#[derive(Debug, Clone, PartialEq)]
1843pub struct CreateFunctionStatement {
1844    pub name: String,
1845    /// `OR REPLACE` was present; an existing function with the
1846    /// same name is overwritten instead of erroring.
1847    pub or_replace: bool,
1848    /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1849    /// list `()` (sufficient for trigger functions). Other shapes
1850    /// parse and store the args but the executor refuses to call
1851    /// them.
1852    pub args: Vec<FunctionArg>,
1853    /// `RETURNS <type>` — `trigger` is the supported shape for
1854    /// v7.12.4; arbitrary return types parse to
1855    /// [`FunctionReturn::Other`].
1856    pub returns: FunctionReturn,
1857    /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1858    /// side of `AS $$...$$`; the parser canonicalises to one slot.
1859    /// `plpgsql` and `sql` are the two interesting values.
1860    pub language: String,
1861    /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1862    /// a structured AST; non-trigger / non-plpgsql bodies stay as
1863    /// the raw source text so the v7.12.5+ executor can pick them
1864    /// up without a parser rev.
1865    pub body: FunctionBody,
1866    /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1867    /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1868    /// on either side of the body; before this they were a parse error, so
1869    /// PG's own `pg_dump` output would not restore.
1870    pub attrs: FunctionAttrs,
1871}
1872
1873/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1874#[derive(Debug, Clone, PartialEq)]
1875pub struct FunctionArg {
1876    /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1877    /// (the default); `OUT` / `INOUT` parse but the executor
1878    /// refuses them.
1879    pub mode: FunctionArgMode,
1880    /// Optional arg name. Trigger functions traditionally don't
1881    /// name their args (they read NEW/OLD instead), so `None` is
1882    /// the common case.
1883    pub name: Option<String>,
1884    /// Declared type, normalised to the SPG `DataType` mapping
1885    /// where one exists. Unknown / extension types parse as a
1886    /// raw string under [`FunctionArgType::Raw`].
1887    pub ty: FunctionArgType,
1888}
1889
1890#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1891pub enum FunctionArgMode {
1892    In,
1893    Out,
1894    InOut,
1895}
1896
1897#[derive(Debug, Clone, PartialEq)]
1898pub enum FunctionArgType {
1899    Typed(ColumnTypeName),
1900    /// Unknown / extension types — kept as the parser-side raw
1901    /// identifier so error messages can name them precisely.
1902    Raw(String),
1903}
1904
1905#[derive(Debug, Clone, PartialEq)]
1906pub enum FunctionReturn {
1907    /// `RETURNS TRIGGER` — the row-level trigger function shape.
1908    /// v7.12.4 ships exactly this for execution.
1909    Trigger,
1910    /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
1911    /// the function is unused (since v7.12.4 doesn't ship scalar
1912    /// function invocation).
1913    Void,
1914    /// `RETURNS <type>` for any concrete data type. Reserved for
1915    /// v7.12.5+'s scalar UDF surface.
1916    Type(ColumnTypeName),
1917    /// `RETURNS <ident>` for types SPG doesn't know — extension
1918    /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
1919    Other(String),
1920}
1921
1922#[derive(Debug, Clone, PartialEq)]
1923pub enum FunctionBody {
1924    /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
1925    /// trigger-function executor walks this directly without
1926    /// re-parsing.
1927    PlPgSql(PlPgSqlBlock),
1928    /// Raw source text — parser couldn't (or didn't try to)
1929    /// structure-parse the body. Used for `LANGUAGE sql`
1930    /// functions and any PL/pgSQL body that contains v7.12.5+
1931    /// features the v7.12.4 parser doesn't yet recognise. The
1932    /// executor returns an unsupported error when invoked.
1933    Raw(String),
1934}
1935
1936/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
1937/// from assignment + return to a real-PL/pgSQL surface:
1938/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
1939/// control flow, `RAISE` diagnostics, and embedded SQL
1940/// statements that execute through the regular engine path.
1941/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
1942/// which mailrs's trigger doesn't need but other PG customers
1943/// may; deferred to a future minor release.
1944#[derive(Debug, Clone, PartialEq)]
1945pub struct PlPgSqlBlock {
1946    /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
1947    /// preceding `BEGIN`. Empty when the body opens directly with
1948    /// `BEGIN`. Declarations execute in order; each may reference
1949    /// earlier-declared locals in its init expression.
1950    pub declarations: Vec<PlPgSqlDeclare>,
1951    pub statements: Vec<PlPgSqlStmt>,
1952    /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
1953    /// <body>` handlers appended to the block. Empty when no
1954    /// EXCEPTION clause is present. When a body statement raises
1955    /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
1956    /// handlers are tried in order; the first matching condition
1957    /// runs its body and the block terminates cleanly. `OTHERS`
1958    /// matches any exception. Unhandled exceptions propagate.
1959    pub exception_handlers: Vec<ExceptionHandler>,
1960}
1961
1962/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
1963/// arm inside an EXCEPTION block.
1964#[derive(Debug, Clone, PartialEq)]
1965pub struct ExceptionHandler {
1966    /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
1967    /// conditions joined by `OR` share one handler body.
1968    pub conditions: Vec<String>,
1969    /// Statements to run when a matching exception is caught.
1970    pub body: Vec<PlPgSqlStmt>,
1971}
1972
1973/// v7.12.6 — single `DECLARE` entry: variable name + declared
1974/// type + optional initialiser. Variables default to SQL NULL
1975/// when no init is given (matches PG).
1976#[derive(Debug, Clone, PartialEq)]
1977pub struct PlPgSqlDeclare {
1978    pub name: String,
1979    /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
1980    /// knows it; raw text otherwise).
1981    pub ty: FunctionArgType,
1982    pub default: Option<Expr>,
1983}
1984
1985#[derive(Debug, Clone, PartialEq)]
1986pub enum PlPgSqlStmt {
1987    /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
1988    /// for clarity in error reporting (PG also forbids it) — the
1989    /// executor errors with a clear "OLD is read-only" message.
1990    Assign { target: AssignTarget, value: Expr },
1991    /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
1992    /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
1993    /// the SELECT statement with the INTO clause stripped; the
1994    /// engine runs it via `Engine::execute`, takes the first
1995    /// row's first column, and assigns to the local variable
1996    /// in the DECLARE scope. Single-column / single-row
1997    /// queries only at v7.16.2; multi-target (`INTO a, b`) is
1998    /// a v7.16.x follow-up.
1999    SelectInto {
2000        var: String,
2001        body: Box<SelectStatement>,
2002    },
2003    /// `RETURN <target>;` — trigger functions canonically return
2004    /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
2005    /// expression for forward compatibility with scalar UDFs.
2006    Return(ReturnTarget),
2007    /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
2008    /// set a SETOF function is building, and KEEP GOING. Not a return.
2009    ReturnNext(Expr),
2010    /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
2011    /// query yields, and keep going. It used to desugar to a side-effect
2012    /// statement whose result was DISCARDED — in a SETOF function that is the
2013    /// whole answer thrown away.
2014    ReturnQuery(Box<SelectStatement>),
2015    /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
2016    /// twin. Its rows go to the set too; it used to run and discard them.
2017    ReturnQueryExecute { sql: Expr },
2018    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
2019    /// [ELSE body] END IF;`. Branches are tried in order; first
2020    /// truthy condition wins; the optional ELSE runs when no
2021    /// condition matched.
2022    If {
2023        branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
2024        else_branch: Vec<PlPgSqlStmt>,
2025    },
2026    /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
2027    /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
2028    /// (logging — observable side effect only) or `EXCEPTION`
2029    /// (aborts the trigger and propagates as an error). v7.12.6
2030    /// supports the basic format-string substitution PG uses
2031    /// (`%` placeholders consumed positionally).
2032    Raise {
2033        level: RaiseLevel,
2034        message: String,
2035        args: Vec<Expr>,
2036    },
2037    /// v7.12.6 — embedded SQL statement inside the trigger body
2038    /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
2039    /// NEW.col / OLD.col references inside the embedded
2040    /// statement's expression tree are substituted with the
2041    /// current trigger context before the engine re-executes the
2042    /// statement. Recursion depth into nested triggers is
2043    /// bounded by the engine's existing trigger-fire guard.
2044    EmbeddedSql(Box<Statement>),
2045    /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
2046    /// the condition evaluates falsy the trigger / DO block aborts
2047    /// with the message (defaulting to a generic shape when none
2048    /// is provided). Same propagation shape as `RAISE EXCEPTION`
2049    /// — the error reaches the caller's query path. PG's behaviour
2050    /// is identical except for a `plpgsql.check_asserts` GUC that
2051    /// can disable the check globally; SPG always evaluates.
2052    Assert {
2053        condition: Expr,
2054        message: Option<Expr>,
2055    },
2056    /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2057    /// Iterate the body while condition evaluates truthy. Iteration
2058    /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2059    /// loops; the executor errors out when reached. EXIT / CONTINUE
2060    /// inside the body queue with 20.2.
2061    While {
2062        condition: Expr,
2063        body: Vec<PlPgSqlStmt>,
2064    },
2065    /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2066    /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2067    /// bounds inclusive on both sides. REVERSE walks backward.
2068    /// Iteration budget guards runaway.
2069    ForRange {
2070        var: String,
2071        start: Expr,
2072        end: Expr,
2073        reverse: bool,
2074        body: Vec<PlPgSqlStmt>,
2075    },
2076    /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2077    /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2078    /// budget guards runaway.
2079    Loop { body: Vec<PlPgSqlStmt> },
2080    /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2081    /// Unconditional (no WHEN) or conditional (only breaks when
2082    /// condition is truthy). Bubbles up as BodyOutcome::Break which
2083    /// the enclosing loop catches. Outside a loop it's a no-op.
2084    Exit { when: Option<Expr> },
2085    /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2086    /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2087    /// which the enclosing loop catches, skipping the remainder of
2088    /// the body and jumping to the next iteration.
2089    Continue { when: Option<Expr> },
2090    /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2091    /// computed SQL statement. The expression is evaluated to a
2092    /// text value, the resulting string is parsed and dispatched
2093    /// through the engine like an EmbeddedSql. USING <param_list>
2094    /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2095    ExecuteDynamic { sql: Expr },
2096    /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2097    /// END LOOP;`. Runs the SELECT once, iterates the resulting
2098    /// rows, binds the first column of each row to `var` as a
2099    /// scalar Value, then runs the body per iteration. EXIT /
2100    /// CONTINUE / ASSERT / RAISE etc. propagate through the
2101    /// enclosing loop's BodyOutcome discipline the same way
2102    /// FOR range and WHILE do. Full record-binding (var as
2103    /// composite carrying all columns) queues with v7.40 record
2104    /// type infrastructure.
2105    ForQuery {
2106        var: String,
2107        query: Box<SelectStatement>,
2108        body: Vec<PlPgSqlStmt>,
2109    },
2110    /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2111    /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2112    /// computed at runtime from a text expression, parsed on the
2113    /// fly, then iterated. Enables dynamic queries where the
2114    /// projection / FROM / WHERE clauses depend on runtime values.
2115    ForExecute {
2116        var: String,
2117        sql_expr: Expr,
2118        body: Vec<PlPgSqlStmt>,
2119    },
2120}
2121
2122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2123pub enum RaiseLevel {
2124    /// `RAISE NOTICE` — diagnostic message, observable in the
2125    /// server log. Does not affect the trigger's outcome.
2126    Notice,
2127    /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2128    Warning,
2129    /// `RAISE INFO` — like NOTICE, slightly quieter.
2130    Info,
2131    /// `RAISE LOG` — like NOTICE, lower priority.
2132    Log,
2133    /// `RAISE DEBUG` — like NOTICE, lowest priority.
2134    Debug,
2135    /// `RAISE EXCEPTION` — aborts the trigger function with the
2136    /// given message, propagating up to the caller as a query-
2137    /// level error.
2138    Exception,
2139}
2140
2141#[derive(Debug, Clone, PartialEq)]
2142pub enum AssignTarget {
2143    NewColumn(String),
2144    OldColumn(String),
2145    /// Reserved for v7.12.5 DECLARE'd local variables.
2146    Local(String),
2147}
2148
2149#[derive(Debug, Clone, PartialEq)]
2150pub enum ReturnTarget {
2151    /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2152    /// actually gets written (possibly with NEW.col mutations
2153    /// applied). For AFTER triggers, the return value is ignored.
2154    New,
2155    /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2156    /// the delete proceed; for BEFORE UPDATE / INSERT it's
2157    /// equivalent to dropping the write.
2158    Old,
2159    /// `RETURN NULL;` — for BEFORE triggers, skips the write
2160    /// entirely. For AFTER, the return value is ignored.
2161    Null,
2162    /// `RETURN <expr>;` — non-row return shape; reserved for the
2163    /// scalar UDF surface in v7.12.5+. Executor errors when used
2164    /// inside a trigger function.
2165    Expr(Expr),
2166}
2167
2168/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2169/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2170/// but the executor refuses them. `WHEN (cond)` clauses are out
2171/// of scope; the trigger function can short-circuit on a leading
2172/// IF inside its body once v7.12.5 lands IF.
2173#[derive(Debug, Clone, PartialEq)]
2174pub struct CreateTriggerStatement {
2175    pub name: String,
2176    pub or_replace: bool,
2177    pub timing: TriggerTiming,
2178    /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2179    /// three entries in order.
2180    pub events: Vec<TriggerEvent>,
2181    pub table: String,
2182    /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2183    /// only `Row`; `Statement` parses but the executor refuses.
2184    pub for_each: TriggerForEach,
2185    /// Name of the function to invoke. The function must exist at
2186    /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2187    /// forward reference (`function no_such_fn() does not exist`), so
2188    /// requiring it IS the PG behaviour (the old note claimed the
2189    /// opposite).
2190    pub function: String,
2191    /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2192    /// (mailrs round-5 G7). Non-empty only when the events list
2193    /// contains UPDATE and the user wrote the column-list filter.
2194    /// PG fires the trigger only when at least one of these
2195    /// columns appears in the SET clause; SPG conservatively
2196    /// fires on any UPDATE matching the listed columns or
2197    /// rewriting them at the row level. Empty vec = no filter
2198    /// (fire on every UPDATE).
2199    pub update_columns: Vec<String>,
2200    /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2201    /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2202    /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2203    pub when_condition: Option<Expr>,
2204}
2205
2206/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2207#[derive(Debug, Clone, PartialEq)]
2208pub struct CreateRuleStatement {
2209    pub name: String,
2210    pub or_replace: bool,
2211    /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2212    pub event: String,
2213    pub table: String,
2214    /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2215    /// (run alongside; PG's default when neither keyword is written).
2216    pub instead: bool,
2217    /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2218    pub when_condition: Option<Expr>,
2219    /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2220    pub commands: Vec<Statement>,
2221}
2222
2223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2224pub enum TriggerTiming {
2225    /// Fires before the row is written; the trigger function's
2226    /// return value (NEW or NULL) decides the row content and
2227    /// whether the write proceeds at all.
2228    Before,
2229    /// Fires after the row is written; the return value is
2230    /// ignored.
2231    After,
2232    /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2233    /// v7.12.4 (SPG has no updatable-view surface).
2234    InsteadOf,
2235}
2236
2237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2238pub enum TriggerEvent {
2239    Insert,
2240    Update,
2241    Delete,
2242    /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2243    /// so the trigger never fires.
2244    Truncate,
2245}
2246
2247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2248pub enum TriggerForEach {
2249    Row,
2250    Statement,
2251}
2252
2253/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2254///
2255/// SPG's index does not scan in a direction, but `indexdef` reproduces
2256/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2257/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2258/// which case PG's default applies — LAST for ascending, FIRST for
2259/// descending, and neither is rendered.
2260#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2261pub struct IndexColumnOrder {
2262    pub descending: bool,
2263    pub nulls_first: Option<bool>,
2264}
2265
2266#[derive(Debug, Clone, PartialEq)]
2267pub struct CreateIndexStatement {
2268    pub name: String,
2269    /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2270    /// either way, so this changes nothing about how the index is made
2271    /// — it is carried because PG refuses the CONCURRENTLY form inside
2272    /// a transaction block and accepts the plain one, and the engine
2273    /// cannot tell them apart without it.
2274    pub concurrently: bool,
2275    /// v7.39 (round 537) — the leading key column's ordering clause,
2276    /// which is the column SPG indexes.
2277    pub key_order: IndexColumnOrder,
2278    /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2279    /// written. SPG orders text by bytes, so honouring it changes
2280    /// nothing; PG prints it, because an explicitly named collation and
2281    /// the one a column inherits are different objects.
2282    pub key_collation: Option<String>,
2283    pub table: String,
2284    pub column: String,
2285    /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2286    /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2287    /// any NULL in the key exempts the row from the uniqueness check.
2288    pub nulls_not_distinct: bool,
2289    /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2290    /// graph for vector kNN); unspecified is the default B-tree index.
2291    pub method: IndexMethod,
2292    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2293    /// index name already exists, instead of raising `DuplicateIndex`.
2294    pub if_not_exists: bool,
2295    /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2296    /// non-key columns the planner should treat as "covered" by
2297    /// this index when checking whether a query can run as an
2298    /// index-only scan. Empty when no `INCLUDE` clause was given.
2299    pub included_columns: Vec<String>,
2300    /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2301    /// for which `<expr>` evaluates truthy enter the index;
2302    /// queries whose `WHERE` clause's canonical Display form
2303    /// matches this expression's Display form can be served by the
2304    /// partial index. Stored as a parsed `Expr` so the engine
2305    /// re-uses the existing evaluation path; storage persists the
2306    /// Display form on the catalog snapshot.
2307    pub partial_predicate: Option<Expr>,
2308    /// v6.8.2 — expression-based index. When `Some(expr)`, the
2309    /// index key is the result of `expr` evaluated on each row
2310    /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2311    /// field still names the *primary* column the expression
2312    /// touches so existing planner shortcuts that resolve a
2313    /// column position stay valid. `None` = plain
2314    /// column-reference index (the legacy shape).
2315    pub expression: Option<Expr>,
2316    /// v7.9.14 — extra column names after the leading column in a
2317    /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2318    /// planner today still only uses the leading column for index
2319    /// seeks; the extras are tracked verbatim so the same DDL
2320    /// round-trips through WAL replay + catalog snapshot, and so
2321    /// the engine can emit a clear warning at INDEX CREATE time
2322    /// that only the leading column is currently honoured.
2323    /// Composite BTree index keys land in v7.10.
2324    pub extra_columns: Vec<String>,
2325    /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2326    /// enforces uniqueness on the indexed key (combined with the
2327    /// `partial_predicate` filter — only rows where the predicate
2328    /// evaluates truthy enter the uniqueness check). Standard SQL
2329    /// and PG's canonical way to express conditional uniqueness.
2330    /// mailrs K1.
2331    pub is_unique: bool,
2332    /// v7.15.0 — operator class on the leading column, when the
2333    /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2334    /// Lower-cased. Most opclasses are still informational; the
2335    /// engine routes on `gin_trgm_ops` specifically to build a
2336    /// trigram-shingle GIN over a TEXT column, and otherwise
2337    /// keeps the current "accepted and discarded" behaviour for
2338    /// pg_dump compatibility.
2339    pub opclass: Option<String>,
2340    /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2341    /// there was no `USING` clause.
2342    ///
2343    /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2344    /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2345    /// implementation for still load. That degradation is deliberate, but
2346    /// it loses the name — and the operator-class check needs it, both to
2347    /// look the class up under the AM the user actually named and to say
2348    /// which AM it was missing from, the way PG's message does.
2349    pub method_name: Option<String>,
2350}
2351
2352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2353pub enum IndexMethod {
2354    /// Default — B-tree over `IndexKey`. Used for equality / range
2355    /// lookups on scalar columns.
2356    BTree,
2357    /// `USING hnsw` — NSW graph for kNN over a vector column.
2358    Hnsw,
2359    /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2360    /// metadata that records (min_key, max_key) for each page in a
2361    /// cold-tier segment, on the indexed column. The optimizer
2362    /// can use these summaries to skip pages whose range does NOT
2363    /// overlap a query's WHERE predicate. BRIN indexes carry no
2364    /// in-memory data — the summaries live in the segment v2
2365    /// envelope's sidecar. Created via the standard
2366    /// `CREATE INDEX … USING brin (col)` syntax.
2367    Brin,
2368    /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2369    /// column. Posting lists map `lexeme word` → row locators; the
2370    /// planner uses them to narrow `WHERE col @@ tsquery` to the
2371    /// candidate rows whose vectors contain a matching term, then
2372    /// re-evaluates the full `@@` semantics on each candidate.
2373    /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2374    /// silently degraded to a full scan at query time.
2375    Gin,
2376}
2377
2378/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2379/// inside a CREATE TABLE column list.
2380///
2381/// The source table's shape can only be read from the catalog, so the
2382/// parser records the clause and the engine expands it. `at` is how many
2383/// explicit columns preceded it: PG keeps the written order, so
2384/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2385#[derive(Debug, Clone, PartialEq)]
2386pub struct LikeSpec {
2387    pub source: String,
2388    pub at: usize,
2389    pub options: LikeOptions,
2390}
2391
2392/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2393/// types and NOT NULL and nothing else — measured on PG18, where a
2394/// copied generated column becomes a plain one and a copied identity
2395/// column loses its identity.
2396#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2397pub struct LikeOptions {
2398    pub defaults: bool,
2399    pub constraints: bool,
2400    pub identity: bool,
2401    pub generated: bool,
2402    pub indexes: bool,
2403    pub comments: bool,
2404}
2405
2406#[derive(Debug, Clone, PartialEq)]
2407pub struct CreateTableStatement {
2408    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2409    /// creating session's own namespace: it shadows a permanent table of the
2410    /// same name, other sessions never see it, and it is dropped when the
2411    /// session ends. A `bool` here lands in the struct's existing padding.
2412    pub temporary: bool,
2413    pub name: String,
2414    /// v7.39 — the `ENGINE=` a MySQL dump names. Consumed and discarded
2415    /// before, so `ENGINE=NONSUCH` built a table where MySQL 9.7.2
2416    /// answers `ERROR 1286`, and `sql_mode` claimed
2417    /// `NO_ENGINE_SUBSTITUTION` while doing it.
2418    pub engine: Option<String>,
2419    pub columns: Vec<ColumnDef>,
2420    /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2421    /// the order written. Empty for a table that has none.
2422    pub like_specs: Vec<LikeSpec>,
2423    /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2424    /// Empty for a table that inherits from nothing. Order matters:
2425    /// the child takes each parent's columns in this order before its
2426    /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2427    pub inherits: Vec<String>,
2428    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2429    /// table name already exists, instead of raising `DuplicateTable`.
2430    pub if_not_exists: bool,
2431    /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2432    /// constraints. Column-level `REFERENCES` (single-column inline
2433    /// form) is normalised into this vec at parse time so the engine
2434    /// sees one uniform list.
2435    pub foreign_keys: Vec<ForeignKeyConstraint>,
2436    /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2437    /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2438    /// Engine resolves each into a BTree index named after the
2439    /// constraint's leading column at CREATE TABLE time; INSERT
2440    /// path enforces composite uniqueness via row scan on the
2441    /// leading column index.
2442    pub table_constraints: Vec<TableConstraint>,
2443    /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2444    /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2445    /// the engine creates a parent table whose own rows stay
2446    /// empty and routes INSERT/SELECT through children. Mutually
2447    /// exclusive with `partition_of` (parser enforces).
2448    pub partition_by: Option<PartitionBySpec>,
2449    /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2450    /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2451    /// the table inherits its column list from `parent` (the
2452    /// parser rejects an explicit column list when this is set);
2453    /// engine routes child rows back to the parent at INSERT.
2454    pub partition_of: Option<PartitionOfSpec>,
2455}
2456
2457/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2458/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2459/// future LIST / HASH without breaking the public AST shape.
2460#[derive(Debug, Clone, PartialEq)]
2461pub struct PartitionBySpec {
2462    pub kind: PartitionKindAst,
2463    /// One or more ident references into the parent's column list.
2464    /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2465    /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2466    /// shape PG-compatible.
2467    pub key_columns: Vec<String>,
2468}
2469
2470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2471pub enum PartitionKindAst {
2472    Range,
2473    /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2474    /// `FOR VALUES IN (lit, lit, …)`.
2475    List,
2476    /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2477    /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2478    Hash,
2479}
2480
2481/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2482/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2483/// or the catch-all `DEFAULT` partition.
2484#[derive(Debug, Clone, PartialEq)]
2485pub struct PartitionOfSpec {
2486    pub parent_name: String,
2487    pub bounds: PartitionOfBoundsAst,
2488}
2489
2490#[derive(Debug, Clone, PartialEq)]
2491pub enum PartitionOfBoundsAst {
2492    /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2493    /// (lits include vector bodies), so we box both bounds to keep
2494    /// the variant size in line with `Default` for clippy and to
2495    /// minimise per-statement footprint when the partition shape
2496    /// isn't in use.
2497    Range {
2498        lower: Box<Expr>,
2499        upper: Box<Expr>,
2500    },
2501    /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2502    /// expr resolves to a typed literal at child-create time.
2503    List {
2504        values: Vec<Expr>,
2505    },
2506    /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2507    /// PG enforces `0 ≤ r < m`; m must be positive.
2508    Hash {
2509        modulus: u32,
2510        remainder: u32,
2511    },
2512    Default,
2513}
2514
2515/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2516/// column list. Either a composite PRIMARY KEY or a UNIQUE
2517/// (single- or multi-column).
2518#[derive(Debug, Clone, PartialEq)]
2519pub enum TableConstraint {
2520    /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2521    /// referenced column. Engine builds a BTree index named
2522    /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2523    PrimaryKey {
2524        name: Option<String>,
2525        columns: Vec<String>,
2526        /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2527        /// Round 621 consumed the clauses; these carry them.
2528        deferrable: bool,
2529        initially_deferred: bool,
2530    },
2531    /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2532    /// named `<table>_<leading_col>_key` (single-column) or
2533    /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2534    /// uniqueness on INSERT.
2535    Unique {
2536        name: Option<String>,
2537        columns: Vec<String>,
2538        /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2539        /// G10). PG 15+ flips the NULL handling so any number of
2540        /// NULL rows collide on the constraint. Default is
2541        /// `false` (NULLS DISTINCT, standard SQL behaviour).
2542        nulls_not_distinct: bool,
2543        /// v7.39 (round 711) — see PrimaryKey.
2544        deferrable: bool,
2545        initially_deferred: bool,
2546    },
2547    /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2548    /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2549    /// this same variant at parse time. Engine evaluates the
2550    /// predicate against each INSERT/UPDATE candidate row; a
2551    /// false / NULL result rejects the mutation.
2552    /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2553    /// PG adds such a constraint without scanning the existing rows: new
2554    /// rows are checked, the ones already there are grandfathered in, and
2555    /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2556    /// scans and flips it. pg_dump emits the suffix for exactly those, so
2557    /// validating them on restore would refuse a dump PG itself produced.
2558    Check {
2559        name: Option<String>,
2560        expr: Expr,
2561        not_valid: bool,
2562    },
2563    /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2564    /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2565    /// every element (the booking/scheduling non-overlap constraint,
2566    /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2567    /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2568    /// enforcement doesn't build the index yet). Each element pairs a
2569    /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2570    Exclude {
2571        name: Option<String>,
2572        method: Option<String>,
2573        elements: Vec<(String, String)>,
2574    },
2575    /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2576    /// non-unique secondary-index declaration inline in CREATE
2577    /// TABLE. Engine builds a BTree index on the leading column
2578    /// (composite columns parse but only the leading column is
2579    /// honoured at v7.15 — matches the existing
2580    /// `CreateIndexStatement::extra_columns` semantics). Useful
2581    /// for `mysql/blog`-style schemas that lean on routine
2582    /// secondary indexes for ORM lookups.
2583    Index {
2584        name: Option<String>,
2585        columns: Vec<String>,
2586    },
2587    /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2588    /// (cols)` inline declaration. Pre-v7.17 the parser
2589    /// silently dropped these so MyISAM-imported FULLTEXT
2590    /// indexes vanished; v7.17 routes them through the
2591    /// existing tsvector-GIN engine path so MATCH AGAINST
2592    /// queries get a real inverted index instead of falling
2593    /// back to a full scan. Multi-column FULLTEXT KEYs build
2594    /// one GIN per column at v7.17 (per-column posting lists);
2595    /// the leading column drives query planning.
2596    FulltextIndex {
2597        name: Option<String>,
2598        columns: Vec<String>,
2599    },
2600}
2601
2602#[derive(Debug, Clone, PartialEq)]
2603#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2604pub struct ColumnDef {
2605    pub name: String,
2606    pub ty: ColumnTypeName,
2607    pub nullable: bool,
2608    /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2609    /// evaluates this once (with an empty row) and caches the resulting
2610    /// `Value` on the column schema.
2611    pub default: Option<Expr>,
2612    /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2613    /// per such column and fills the slot when INSERT leaves it
2614    /// unbound (omitted from a column-list INSERT or explicitly NULL).
2615    pub auto_increment: bool,
2616    /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2617    /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2618    /// an implicit BTree index named `<table>_pkey` over this
2619    /// column at CREATE TABLE time, satisfying the parent-side
2620    /// index requirement for any FOREIGN KEY pointing at it.
2621    pub is_primary_key: bool,
2622    /// v7.13.0 — inline `UNIQUE` column constraint
2623    /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2624    /// into a single-column `TableConstraint::Unique` so the
2625    /// engine path stays uniform with table-level UNIQUE.
2626    pub is_unique: bool,
2627    /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2628    /// inline column constraint: treat NULL keys as equal so only one NULL
2629    /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2630    /// `TableConstraint::Unique { nulls_not_distinct }`.
2631    pub unique_nulls_not_distinct: bool,
2632    /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2633    /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2634    /// since this round so the fold into the table-level constraint keeps it.
2635    pub constraint_deferrable: bool,
2636    pub constraint_initially_deferred: bool,
2637    /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2638    /// (mailrs round-5 G3). Stored alongside the column so the
2639    /// CREATE TABLE handler can fold these into table-level
2640    /// CHECK constraints. Multiple inline CHECKs on the same
2641    /// column are concatenated with AND at the table level.
2642    pub check: Option<Expr>,
2643    /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2644    /// parser sees an unknown column-type ident (anything not in
2645    /// the built-in `parse_column_type_name` table), it sets
2646    /// `ty = ColumnTypeName::Text` and records the original name
2647    /// here. The engine resolves at CREATE TABLE time: if a
2648    /// catalog enum/domain with this name exists, the column is
2649    /// bound to it (label-checked on INSERT for enums; CHECK-
2650    /// constrained for domains); otherwise the CREATE TABLE
2651    /// errors with "unknown type".
2652    pub user_type_ref: Option<String>,
2653    /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2654    /// CURRENT_TIMESTAMP` column attribute. When set, an
2655    /// UPDATE that does NOT explicitly bind this column
2656    /// overrides the new value with `now()` (engine clock).
2657    /// Pre-v7.17 SPG silently accepted the syntax and never
2658    /// fired the override — `updated_at` columns from mysqldump
2659    /// stayed pinned at their initial DEFAULT forever, an
2660    /// audit Tier-S silent-failure. Generalised as a stored
2661    /// expression source so future shapes (`ON UPDATE
2662    /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2663    /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2664    pub on_update_runtime: Option<Expr>,
2665    /// v7.17.0 Phase 2.5 — text collation derived from the
2666    /// post-fix `COLLATE <name>` clause (and / or the table-level
2667    /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2668    /// per column). Pre-2.5 SPG accepted the clause and
2669    /// discarded the name, leaving every column byte-compared
2670    /// — a Tier-S silent failure when the customer expected
2671    /// `_ci` / `case_insensitive` semantics. Parser normalises
2672    /// the raw collation name into the variants in `Collation`.
2673    /// Default `Binary` preserves the legacy compare path.
2674    pub collation: Collation,
2675    /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2676    /// explicit `COLLATE <name>` clause rather than the default. Under the
2677    /// MySQL dialect a text column with NO explicit clause takes the
2678    /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2679    /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2680    /// flag is the only thing that tells them apart.
2681    pub collation_explicit: bool,
2682    /// v7.39 (round 676) — the collation name AS WRITTEN, because
2683    /// `collation` above cannot carry it: `Collation` is a two-variant
2684    /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2685    /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2686    /// tell them apart.
2687    pub collation_name: Option<String>,
2688    /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2689    /// 4.4 SPG accepted and discarded the keyword, leaving
2690    /// negative values silently accepted on a column the
2691    /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2692    /// rejects negative INSERT / UPDATE values on UNSIGNED int
2693    /// columns. SPG widening to `u64`-shaped storage is out of
2694    /// v7.17 scope; the upper bound remains the signed-type max
2695    /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2696    /// exceeds what every mailrs / Rails app actually uses.
2697    pub is_unsigned: bool,
2698    /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2699    /// value list captured at parse time. When `Some`, the parser
2700    /// recognised `ENUM(...)` in the type slot; the engine
2701    /// validates INSERT cells against this list at
2702    /// column_def_to_schema time and persists the variants on
2703    /// `ColumnSchema.inline_enum_variants`. None for all
2704    /// non-ENUM columns.
2705    pub inline_enum_variants: Option<Vec<String>>,
2706    /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2707    /// value list. Distinct from ENUM (subset semantics rather
2708    /// than pick-one). None for all non-SET columns.
2709    pub inline_set_variants: Option<Vec<String>>,
2710    /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2711    /// STORED` computed-column source. When `Some`, the engine
2712    /// stores the Display-form of the parsed expression on
2713    /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2714    /// and re-evaluates the expression against every INSERT /
2715    /// UPDATE candidate row, overwriting whatever the caller
2716    /// supplied for this column. Boxed to keep `ColumnDef` from
2717    /// blowing past the `large_enum_variant` clippy ceiling
2718    /// (`Expr` widens with vector literals).
2719    pub generated_stored_expr: Option<Box<Expr>>,
2720    /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2721    /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2722    /// `auto_increment`; this additionally marks the ALWAYS one, whose
2723    /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2724    /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2725    /// VALUE`. Only meaningful when the column is also an identity column.
2726    pub identity_always: bool,
2727    /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2728    /// integer width (TINYINT / MEDIUMINT), captured before the type
2729    /// collapses to SmallInt / Int. The engine copies it to
2730    /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2731    /// path can enforce the real range. None for every other column and
2732    /// under the PG dialect.
2733    pub mysql_int_width: Option<MysqlIntWidth>,
2734    /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2735    /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2736    /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2737    /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2738    /// CREATE TABLE time so the write path can truncate and the render path
2739    /// can pad. None under the PG dialect, where temporal columns keep full
2740    /// microseconds.
2741    pub mysql_fsp: Option<u8>,
2742    /// v7.39.2 — the column was written `TIMESTAMP` rather than
2743    /// `DATETIME` in a MySQL session. The engine copies it to
2744    /// `ColumnSchema.mysql_declared_timestamp` at CREATE TABLE.
2745    pub mysql_declared_timestamp: bool,
2746    /// v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair, copied to
2747    /// `ColumnSchema.mysql_float_md` at CREATE TABLE.
2748    pub mysql_float_md: Option<(u8, u8)>,
2749}
2750
2751/// v7.17.0 Phase 2.5 — text collation classification surfaced
2752/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2753/// engine bridges between the two at CREATE TABLE time.
2754///
2755/// Recognised collation-name patterns (case-insensitive):
2756///   * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase`         → CaseInsensitive
2757///   * Everything else (`C`, `POSIX`, `default`,
2758///     `pg_catalog.default`, `*_cs`, `*_bin`, unknown names)   → Binary
2759#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2760pub enum Collation {
2761    Binary,
2762    CaseInsensitive,
2763}
2764
2765/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2766/// integer width for a column whose `ColumnTypeName` is too wide to carry
2767/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2768/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2769/// TABLE time. Only recorded under the MySQL dialect.
2770#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2771pub enum MysqlIntWidth {
2772    Tiny,
2773    Small,
2774    Medium,
2775    Int,
2776    /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2777    Big,
2778}
2779
2780#[allow(clippy::derivable_impls)]
2781impl Default for Collation {
2782    fn default() -> Self {
2783        Self::Binary
2784    }
2785}
2786
2787impl Collation {
2788    /// Classify a `COLLATE <name>` ident into one of the supported
2789    /// variants. Empty / unknown names fall back to `Binary` —
2790    /// matches the pre-2.5 silent-accept behaviour for snapshots
2791    /// that load through but don't actually depend on the
2792    /// collation semantics.
2793    #[must_use]
2794    pub fn from_collation_name(name: &str) -> Self {
2795        let lc = name.trim().to_ascii_lowercase();
2796        // Strip any quotes / schema-qualifier the parser left on
2797        // (e.g. `pg_catalog.default`).
2798        let bare = lc
2799            .trim_matches(|c: char| c == '"' || c == '\'')
2800            .rsplit('.')
2801            .next()
2802            .unwrap_or("");
2803        if bare.is_empty() {
2804            return Self::Binary;
2805        }
2806        if bare == "case_insensitive" || bare == "nocase" {
2807            return Self::CaseInsensitive;
2808        }
2809        // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2810        // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2811        if bare.ends_with("_ci") {
2812            return Self::CaseInsensitive;
2813        }
2814        Self::Binary
2815    }
2816}
2817
2818/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2819/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2820/// parse into this shape — the column-level form has a single-entry
2821/// `columns` / `parent_columns`.
2822#[derive(Debug, Clone, PartialEq)]
2823pub struct ForeignKeyConstraint {
2824    /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2825    /// today but parses + stores it so a future ALTER TABLE DROP
2826    /// CONSTRAINT can target by name (v7.6.8).
2827    pub name: Option<String>,
2828    /// Local columns participating in the FK (≥ 1).
2829    pub columns: Vec<String>,
2830    /// Referenced parent table.
2831    pub parent_table: String,
2832    /// Referenced parent columns. Must have the same arity as
2833    /// `columns`; engine validates parent has a PK / UNIQUE index
2834    /// on exactly this column set (v7.6.1).
2835    pub parent_columns: Vec<String>,
2836    /// `ON DELETE` action. Defaults to `Restrict` if absent.
2837    pub on_delete: FkAction,
2838    /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2839    pub on_update: FkAction,
2840    /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2841    pub match_type: MatchType,
2842    /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2843    /// dropped on the floor, so a constraint declared DEFERRABLE was
2844    /// enforced immediately and a circular-FK migration could not load.
2845    pub deferrable: bool,
2846    /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2847    /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2848    pub initially_deferred: bool,
2849}
2850
2851/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2852/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2853/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2854#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2855pub enum MatchType {
2856    #[default]
2857    Simple,
2858    Full,
2859}
2860
2861/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2862#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2863pub enum FkAction {
2864    /// Reject the parent mutation if any child row references it.
2865    /// SQL spec default; SPG default when no clause is given.
2866    Restrict,
2867    /// Recursively propagate the parent's delete / update to the
2868    /// child rows. Same TX.
2869    Cascade,
2870    /// Set the child FK column(s) to NULL. Requires the FK columns
2871    /// to be NULL-able.
2872    SetNull,
2873    /// Set the child FK column(s) to their declared DEFAULT.
2874    /// Requires the child column(s) to have DEFAULT.
2875    SetDefault,
2876    /// SQL spec `NO ACTION` (deferred check). SPG treats this as
2877    /// `Restrict` because the single-writer model has no deferred
2878    /// constraint window; the keyword is accepted for compatibility.
2879    NoAction,
2880}
2881
2882/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
2883/// optional `USING <encoding>` clause; omitting it keeps the
2884/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
2885/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
2886/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
2887/// binary16 (2× compression, ~3 decimal digits of precision).
2888#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2889pub enum VecEncoding {
2890    /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
2891    /// uncompressed `vector` type wire / storage layout.
2892    #[default]
2893    F32,
2894    /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
2895    /// `spg_storage::quantize::Sq8Vector` for the math + recall
2896    /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
2897    /// dim ≥ 32).
2898    Sq8,
2899    /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
2900    /// per-element. DDL keyword `HALF` (pgvector convention).
2901    /// Bit-exact dequantise to f32 at the storage layer; no
2902    /// rerank pass needed for kNN search.
2903    F16,
2904}
2905
2906impl fmt::Display for VecEncoding {
2907    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2908        match self {
2909            Self::F32 => f.write_str("F32"),
2910            Self::Sq8 => f.write_str("SQ8"),
2911            // pgvector convention: DDL keyword is `HALF`, not `F16`.
2912            Self::F16 => f.write_str("HALF"),
2913        }
2914    }
2915}
2916
2917/// SQL-level type names. The mapping to the storage runtime's `DataType`
2918/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
2919#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2920pub enum ColumnTypeName {
2921    /// v7.39 (round 291) — PG's `name`, the identifier type its
2922    /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
2923    /// answered `type "name" does not exist` to.
2924    Name,
2925    /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
2926    /// 32-bit wrapping counter the row header carries; `xid8` is the
2927    /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
2928    /// SPG answered `type "xid" does not exist` to.
2929    Xid,
2930    Xid8,
2931    /// v7.39 (round 667) — `OID`. `XID` was already a column type here
2932    /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
2933    /// `type "oid" does not exist` while `t(x XID)` built fine.
2934    Oid,
2935    SmallInt,
2936    Int,
2937    BigInt,
2938    Float,
2939    /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
2940    /// IEEE. It used to map to [`Self::Float`] on the theory that a
2941    /// wider float is harmless, but the width is observable: a `real`
2942    /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
2943    /// answered false where PG answers true.
2944    Real,
2945    Text,
2946    /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
2947    Varchar(u32),
2948    /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
2949    Char(u32),
2950    Bool,
2951    /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
2952    /// `USING <encoding>` clause; omitting it surfaces as
2953    /// `encoding = VecEncoding::F32` (the pre-v6 default).
2954    Vector {
2955        dim: u32,
2956        encoding: VecEncoding,
2957    },
2958    /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
2959    /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
2960    /// v7.39 (round 271) — scale widened to u16 alongside the value's.
2961    /// v7.39 (round 272) — precision too: PG's runs to 1000.
2962    /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
2963    /// a negative one rounds to tens / hundreds. A VALUE's display scale
2964    /// stays unsigned.
2965    Numeric(u16, i16),
2966    /// `DATE` — calendar day, no time-of-day component.
2967    Date,
2968    /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
2969    /// precision.
2970    Timestamp,
2971    /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
2972    /// stores all timestamps as UTC microseconds-since-epoch and
2973    /// does not carry per-row offset (PG's internal representation
2974    /// is the same — TZ is a display convention). The distinction
2975    /// from `TIMESTAMP` exists for the PG-wire layer to advertise
2976    /// OID 1184 so sqlx-style clients decode into
2977    /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
2978    Timestamptz,
2979    /// v4.9 `JSON` — text-backed JSON document. No parse-time
2980    /// validation; the engine round-trips the literal verbatim.
2981    /// PG OID 114 on the wire.
2982    Json,
2983    /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
2984    /// PG OID 3802 on the wire so sqlx-style binary-typed clients
2985    /// decode without a custom type registration.
2986    Jsonb,
2987    /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
2988    /// Literal forms (decoded by the engine at coercion time):
2989    ///   - PG hex form: `'\xDEADBEEF'`
2990    ///   - Escape form: `'foo\\000bar'` (backslash octal triples)
2991    Bytes,
2992    /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
2993    /// OID 1009. Literal forms accepted by the parser:
2994    ///   - `ARRAY['a', 'b', NULL]`
2995    ///   - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
2996    ///     form at coerce time)
2997    TextArray,
2998    /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
2999    /// 1007. Same literal forms as TEXT[] (substituting integer
3000    /// elements).
3001    IntArray,
3002    /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
3003    /// OID 1016.
3004    BigIntArray,
3005    /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
3006    /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
3007    /// external form). G-CRIT-3.
3008    TsVector,
3009    /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
3010    /// wire OID 3615.
3011    TsQuery,
3012    /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
3013    /// Literal input accepts canonical hyphenated, unhyphenated,
3014    /// uppercase, and `{...}`-braced forms; display normalises to
3015    /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
3016    /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
3017    /// gen_random_uuid()`.
3018    Uuid,
3019    /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
3020    /// microseconds since 00:00:00. PG wire OID 1083. Literal
3021    /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
3022    /// (6-digit microsecond precision). Display normalises to
3023    /// the canonical `HH:MM:SS[.ffffff]`.
3024    Time,
3025    /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
3026    /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
3027    /// PG OID; advertised as INT4 on the wire. Display always
3028    /// 4 digits zero-padded.
3029    Year,
3030    /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
3031    /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
3032    /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
3033    /// Offset range: ±14 hours.
3034    TimeTz,
3035    /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
3036    /// (locale-independent storage). Wire OID 790. Literal input
3037    /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
3038    /// major units), optional leading `-`. Display: en_US locale.
3039    Money,
3040    /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
3041    /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
3042    /// — the engine bridges to `DataType::Range(RangeKind)`.
3043    Range(RangeKindAst),
3044    /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
3045    /// `text => text` map with NULL value support.
3046    Hstore,
3047    /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
3048    IntArray2D,
3049    BigIntArray2D,
3050    TextArray2D,
3051    /// v7.39 (read01 round 75) — `bool[][]`.
3052    BoolArray2D,
3053    /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
3054    /// three-field {months, days, micros} struct (PG-byte-equal),
3055    /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
3056    /// β-P2 `INTERVAL` was runtime-only — literal in expression
3057    /// position but rejected at CREATE TABLE.
3058    Interval,
3059    /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
3060    /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
3061    /// PG external form quotes each non-NULL element because
3062    /// interval text contains spaces / colons
3063    /// (`{"1 day","24:00:00",NULL}`).
3064    IntervalArray,
3065    /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3066    /// mirrors a scalar `ColumnTypeName` that already existed.
3067    BoolArray,
3068    SmallIntArray,
3069    FloatArray,
3070    NumericArray,
3071    DateArray,
3072    TimestampArray,
3073    TimestamptzArray,
3074    UuidArray,
3075    JsonArray,
3076    JsonbArray,
3077    BytesArray,
3078    VarcharArray,
3079    CharArray,
3080    /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3081    /// as `Range(RangeKindAst)` — one column type variant covers
3082    /// all six builtin multiranges, kind pins the element type.
3083    /// Wire OIDs in pgwire.
3084    Multirange(RangeKindAst),
3085    /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3086    /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3087    /// Wire OIDs in pgwire.
3088    Point,
3089    Lseg,
3090    Path,
3091    PgBox,
3092    Polygon,
3093    Line,
3094    Circle,
3095    /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3096    Inet,
3097    Cidr,
3098    Macaddr,
3099    Macaddr8,
3100    /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3101    Bit(u32),
3102    /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3103    BitVarying(u32),
3104    Xml,
3105    Char1,
3106    MoneyArray,
3107}
3108
3109/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3110/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3111/// crate doesn't depend on storage. Bridged at engine boundary.
3112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3113pub enum RangeKindAst {
3114    Int4,
3115    Int8,
3116    Num,
3117    Ts,
3118    TsTz,
3119    Date,
3120}
3121
3122impl fmt::Display for ColumnTypeName {
3123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3124        match self {
3125            Self::SmallInt => f.write_str("SMALLINT"),
3126            Self::Int => f.write_str("INT"),
3127            Self::BigInt => f.write_str("BIGINT"),
3128            Self::Float => f.write_str("FLOAT"),
3129            Self::Real => f.write_str("REAL"),
3130            Self::Text => f.write_str("TEXT"),
3131            Self::Name => f.write_str("name"),
3132            Self::Xid => f.write_str("xid"),
3133            Self::Xid8 => f.write_str("xid8"),
3134            Self::Oid => f.write_str("oid"),
3135            Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3136            Self::Char(n) => write!(f, "CHAR({n})"),
3137            Self::Bool => f.write_str("BOOL"),
3138            Self::Vector { dim, encoding } => match encoding {
3139                VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3140                VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3141                VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3142            },
3143            Self::Json => f.write_str("JSON"),
3144            Self::Jsonb => f.write_str("JSONB"),
3145            Self::Bytes => f.write_str("BYTEA"),
3146            Self::TextArray => f.write_str("TEXT[]"),
3147            Self::IntArray => f.write_str("INT[]"),
3148            Self::BigIntArray => f.write_str("BIGINT[]"),
3149            Self::TsVector => f.write_str("TSVECTOR"),
3150            Self::TsQuery => f.write_str("TSQUERY"),
3151            Self::Uuid => f.write_str("UUID"),
3152            Self::Numeric(p, s) => {
3153                if *s == 0 {
3154                    write!(f, "NUMERIC({p})")
3155                } else {
3156                    write!(f, "NUMERIC({p}, {s})")
3157                }
3158            }
3159            Self::Date => f.write_str("DATE"),
3160            Self::Timestamp => f.write_str("TIMESTAMP"),
3161            Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3162            Self::Time => f.write_str("TIME"),
3163            Self::Year => f.write_str("YEAR"),
3164            Self::TimeTz => f.write_str("TIMETZ"),
3165            Self::Money => f.write_str("MONEY"),
3166            Self::Range(k) => f.write_str(match k {
3167                RangeKindAst::Int4 => "INT4RANGE",
3168                RangeKindAst::Int8 => "INT8RANGE",
3169                RangeKindAst::Num => "NUMRANGE",
3170                RangeKindAst::Ts => "TSRANGE",
3171                RangeKindAst::TsTz => "TSTZRANGE",
3172                RangeKindAst::Date => "DATERANGE",
3173            }),
3174            Self::Hstore => f.write_str("HSTORE"),
3175            Self::Interval => f.write_str("INTERVAL"),
3176            Self::IntervalArray => f.write_str("INTERVAL[]"),
3177            Self::BoolArray => f.write_str("BOOL[]"),
3178            Self::SmallIntArray => f.write_str("SMALLINT[]"),
3179            Self::FloatArray => f.write_str("FLOAT[]"),
3180            Self::NumericArray => f.write_str("NUMERIC[]"),
3181            Self::DateArray => f.write_str("DATE[]"),
3182            Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3183            Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3184            Self::UuidArray => f.write_str("UUID[]"),
3185            Self::JsonArray => f.write_str("JSON[]"),
3186            Self::JsonbArray => f.write_str("JSONB[]"),
3187            Self::BytesArray => f.write_str("BYTEA[]"),
3188            Self::VarcharArray => f.write_str("VARCHAR[]"),
3189            Self::CharArray => f.write_str("CHAR[]"),
3190            Self::Multirange(k) => f.write_str(match k {
3191                RangeKindAst::Int4 => "INT4MULTIRANGE",
3192                RangeKindAst::Int8 => "INT8MULTIRANGE",
3193                RangeKindAst::Num => "NUMMULTIRANGE",
3194                RangeKindAst::Ts => "TSMULTIRANGE",
3195                RangeKindAst::TsTz => "TSTZMULTIRANGE",
3196                RangeKindAst::Date => "DATEMULTIRANGE",
3197            }),
3198            Self::Point => f.write_str("POINT"),
3199            Self::Lseg => f.write_str("LSEG"),
3200            Self::Path => f.write_str("PATH"),
3201            Self::PgBox => f.write_str("BOX"),
3202            Self::Polygon => f.write_str("POLYGON"),
3203            Self::Line => f.write_str("LINE"),
3204            Self::Circle => f.write_str("CIRCLE"),
3205            Self::Inet => f.write_str("INET"),
3206            Self::Cidr => f.write_str("CIDR"),
3207            Self::Macaddr => f.write_str("MACADDR"),
3208            Self::Macaddr8 => f.write_str("MACADDR8"),
3209            Self::Bit(0) => f.write_str("BIT"),
3210            Self::Bit(n) => write!(f, "BIT({n})"),
3211            Self::BitVarying(0) => f.write_str("VARBIT"),
3212            Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3213            Self::Xml => f.write_str("XML"),
3214            Self::Char1 => f.write_str("\"char\""),
3215            Self::MoneyArray => f.write_str("MONEY[]"),
3216            Self::IntArray2D => f.write_str("INT[][]"),
3217            Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3218            Self::TextArray2D => f.write_str("TEXT[][]"),
3219            Self::BoolArray2D => f.write_str("BOOL[][]"),
3220        }
3221    }
3222}
3223
3224/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3225/// engine evaluates `expr` per matched row in the table's row order
3226/// and rewrites cells in place. Indexed columns are dropped + re-
3227/// inserted into the affected B-tree on each row change.
3228/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3229/// tail on a DML statement. Boxed off the statement struct so the PG-only
3230/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3231/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3232/// the identical meaning, so both share this one payload rather than each
3233/// growing its own.
3234#[derive(Debug, Clone, PartialEq)]
3235pub struct DmlOrderLimit {
3236    pub order_by: Vec<OrderBy>,
3237    pub limit: Option<u32>,
3238}
3239
3240/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3241/// FROM, kept so the engine can finish the job.
3242///
3243/// The parser rewrites the statement onto correlated subqueries, and it
3244/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3245/// name belongs to the target or to a source needs their column lists,
3246/// which parse time does not have. Carrying the clause lets the engine
3247/// — which has the catalog — resolve the rest.
3248#[derive(Debug, Clone, PartialEq)]
3249pub struct UpdateFromSources {
3250    pub from: FromClause,
3251    pub sub_where: Option<Expr>,
3252}
3253
3254#[derive(Debug, Clone, PartialEq)]
3255pub struct UpdateStatement {
3256    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3257    /// level UPDATE. Empty for a plain UPDATE.
3258    pub ctes: Vec<Cte>,
3259    pub table: String,
3260    /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3261    /// to `t`'s own rows and not to anything that descends from it.
3262    ///
3263    /// Round 644 taught the FROM clause the keyword and left DML behind
3264    /// because it needed a field here, and this struct carries a warning
3265    /// that round 413 measured widening it in place overflowing the
3266    /// parser's nesting stack. That warning was about `from_sources`, a
3267    /// struct wide enough to need boxing; a `bool` lands in the padding
3268    /// already present — same as `CreateTableStatement::temporary`.
3269    ///
3270    /// It also earns its keep beyond the spelling: the inheritance
3271    /// fan-out needs a way to say "the parent's own rows" as a
3272    /// statement, or running one on the parent recurses forever.
3273    pub only: bool,
3274    /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3275    /// statement's expressions refer to the target row by. PG allows the
3276    /// bare spelling here (unlike INSERT, which requires AS).
3277    pub alias: Option<String>,
3278    pub assignments: Vec<(String, Expr)>,
3279    /// v7.39 (round 533) — boxed: round 413 measured that widening this
3280    /// struct in place overflows the parser's nesting stack.
3281    pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3282    pub where_: Option<Expr>,
3283    /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3284    /// mutate the first `limit` rows in the given order. PG has no such
3285    /// clause; the parser accepts it only under the MySQL dialect. Boxed
3286    /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3287    /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3288    /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3289    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3290    /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3291    /// clause (legacy CommandComplete path). Some = engine
3292    /// evaluates the projection over each mutated row and
3293    /// streams the result as a Rows QueryResult.
3294    pub returning: Option<Vec<SelectItem>>,
3295}
3296
3297/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3298/// from the active catalog and prunes them from every index.
3299#[derive(Debug, Clone, PartialEq)]
3300pub struct DeleteStatement {
3301    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3302    /// level DELETE. Empty for a plain DELETE.
3303    pub ctes: Vec<Cte>,
3304    pub table: String,
3305    /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3306    /// to `t`'s own rows and not to anything that descends from it.
3307    ///
3308    /// Round 644 taught the FROM clause the keyword and left DML behind
3309    /// because it needed a field here, and this struct carries a warning
3310    /// that round 413 measured widening it in place overflowing the
3311    /// parser's nesting stack. That warning was about `from_sources`, a
3312    /// struct wide enough to need boxing; a `bool` lands in the padding
3313    /// already present — same as `CreateTableStatement::temporary`.
3314    ///
3315    /// It also earns its keep beyond the spelling: the inheritance
3316    /// fan-out needs a way to say "the parent's own rows" as a
3317    /// statement, or running one on the parent recurses forever.
3318    pub only: bool,
3319    /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3320    /// the WHERE / RETURNING expressions refer to the target row by.
3321    pub alias: Option<String>,
3322    pub where_: Option<Expr>,
3323    /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3324    /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3325    /// form (round 413), so it shares that payload — and it is boxed for
3326    /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3327    /// statement tipped the parser's 512 KiB nesting stack.
3328    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3329    /// v7.9.4 — `RETURNING <projection>`.
3330    pub returning: Option<Vec<SelectItem>>,
3331}
3332
3333/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3334/// One WHEN clause fires per source row depending on whether the
3335/// `on` condition matched any target row(s); the executor walks
3336/// `clauses` in declaration order and fires the first whose
3337/// `matched` kind and optional `condition` are both satisfied.
3338#[derive(Debug, Clone, PartialEq)]
3339pub struct MergeStatement {
3340    /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3341    /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3342    /// in PG). Each CTE materialises before the merge runs and its alias
3343    /// resolves as a source relation.
3344    pub ctes: Vec<Cte>,
3345    pub target: String,
3346    pub target_alias: Option<String>,
3347    pub source: String,
3348    pub source_alias: Option<String>,
3349    /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3350    /// the engine materialises this SELECT for the source rows and `source`
3351    /// is empty; the alias (required by PG for a subquery source) is in
3352    /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3353    pub source_select: Option<Box<SelectStatement>>,
3354    /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3355    /// positional column-alias list after the source alias. Empty when
3356    /// the statement carries none; the engine renames the materialised
3357    /// source columns positionally (PG's rule).
3358    pub source_column_aliases: Vec<String>,
3359    pub on: Expr,
3360    pub clauses: Vec<MergeWhenClause>,
3361    /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3362    /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3363    /// target/source aliases. `None` = no RETURNING (the common form).
3364    pub returning: Option<Vec<SelectItem>>,
3365}
3366
3367#[derive(Debug, Clone, PartialEq)]
3368pub struct MergeWhenClause {
3369    pub matched: MergeMatched,
3370    /// Optional `AND <expr>` filter — when present, the clause
3371    /// only fires for the source rows whose match-pair satisfies
3372    /// the predicate.
3373    pub condition: Option<Expr>,
3374    pub action: MergeAction,
3375}
3376
3377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3378pub enum MergeMatched {
3379    Matched,
3380    /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3381    /// target row (the classic insert branch).
3382    NotMatched,
3383    /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3384    /// row no source row matches. Actions are UPDATE / DELETE / DO
3385    /// NOTHING only (INSERT is a syntax error, as in PG).
3386    NotMatchedBySource,
3387}
3388
3389#[derive(Debug, Clone, PartialEq)]
3390pub enum MergeAction {
3391    /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3392    /// explicit column list (the bare `INSERT VALUES (vals)`
3393    /// shape lands later).
3394    Insert {
3395        columns: Vec<String>,
3396        values: Vec<Expr>,
3397    },
3398    /// `UPDATE SET col = expr [, …]` — applied to every matched
3399    /// target row for the firing source row.
3400    Update { assignments: Vec<(String, Expr)> },
3401    /// `DELETE` — drop every matched target row.
3402    Delete,
3403    /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3404    /// the clause and SPG mirrors so a customer-side MERGE that
3405    /// uses it for branch-control doesn't error).
3406    DoNothing,
3407}
3408
3409#[derive(Debug, Clone, PartialEq)]
3410pub struct InsertStatement {
3411    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3412    /// level INSERT (writable CTE outer body). Empty for a plain
3413    /// INSERT. PG semantics: each CTE materialises before the
3414    /// outer INSERT runs, sharing the same transaction.
3415    pub ctes: Vec<Cte>,
3416    pub table: String,
3417    /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3418    /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3419    /// row by. PG requires the AS keyword in this position.
3420    pub alias: Option<String>,
3421    /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3422    /// `None`, every tuple is positional and must match the table arity.
3423    /// When `Some`, the engine maps each tuple slot to the named column and
3424    /// fills the rest with NULL (must be nullable).
3425    pub columns: Option<Vec<String>>,
3426    /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3427    /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3428    /// `select_source` is `Some` (the engine builds rows from the
3429    /// inner SELECT result set instead).
3430    pub rows: Vec<Vec<Expr>>,
3431    /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3432    /// round-5 G4). When present, `rows` is empty and the engine
3433    /// materialises the SELECT result, coerces each output tuple to
3434    /// the target column types, and inserts as a single batch.
3435    pub select_source: Option<Box<SelectStatement>>,
3436    /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3437    /// upsert clause. None = legacy INSERT (conflict raises a
3438    /// DuplicateKey error). mailrs migration blocker #2.
3439    pub on_conflict: Option<OnConflictClause>,
3440    /// v7.9.4 — `RETURNING <projection>`.
3441    pub returning: Option<Vec<SelectItem>>,
3442    /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3443    /// between the column list and VALUES. Governs how explicitly-supplied
3444    /// values interact with `GENERATED … AS IDENTITY` columns:
3445    ///   * `None` — default. A `GENERATED ALWAYS` identity column rejects
3446    ///     an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3447    ///   * `System` — override the ALWAYS restriction: the explicit value
3448    ///     is used verbatim, as for a `BY DEFAULT` column.
3449    ///   * `User` — ignore any explicit value on a `BY DEFAULT` identity
3450    ///     column and generate from the sequence instead (no effect on
3451    ///     non-identity columns).
3452    pub overriding: Overriding,
3453    /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3454    /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3455    /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3456    /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3457    /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3458    /// into a NOT NULL column becomes the type's default), and the engine
3459    /// cannot recover that intent from the conflict clause alone. A plain
3460    /// `bool` lands in this struct's existing padding, so the AST does not
3461    /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3462    pub mysql_ignore: bool,
3463}
3464
3465/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3466#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3467pub enum Overriding {
3468    /// No `OVERRIDING` clause.
3469    #[default]
3470    None,
3471    /// `OVERRIDING SYSTEM VALUE`.
3472    System,
3473    /// `OVERRIDING USER VALUE`.
3474    User,
3475}
3476
3477/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3478#[derive(Debug, Clone, PartialEq)]
3479pub struct OnConflictClause {
3480    /// Local columns that identify the conflict (must match a
3481    /// UNIQUE / PRIMARY KEY index on the target table). Empty
3482    /// list means the user wrote `ON CONFLICT DO …` without a
3483    /// target — the engine arbitrates on every unique constraint
3484    /// (round 240).
3485    pub target_columns: Vec<String>,
3486    /// v7.39 (round 240) — the index predicate after the target list
3487    /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3488    /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3489    /// which satisfy any predicate, so it is parsed and carried but not
3490    /// consulted (recorded residual: partial-unique-index arbiters).
3491    pub index_where: Option<Expr>,
3492    /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3493    /// <name>`: the pg_dump conflict-target form. The engine
3494    /// resolves the name to the constraint's columns.
3495    pub constraint_name: Option<String>,
3496    /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3497    /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3498    /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3499    /// `ON CONFLICT DO UPDATE` is refused (42601).
3500    pub mysql_lowered: bool,
3501    /// The action on conflict.
3502    pub action: OnConflictAction,
3503}
3504
3505/// v7.9.7 — action on conflict.
3506#[derive(Debug, Clone, PartialEq)]
3507pub enum OnConflictAction {
3508    /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3509    /// silently skips conflicting ones.
3510    Nothing,
3511    /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3512    /// may reference `EXCLUDED.col` to read the incoming row's
3513    /// value (engine wires `EXCLUDED` as a virtual table).
3514    Update {
3515        assignments: Vec<(String, Expr)>,
3516        where_: Option<Expr>,
3517    },
3518}
3519
3520/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3521///
3522/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3523/// policies are spelled again here and mapped at the engine boundary.
3524/// v7.39 — the modes `BEGIN` / `START TRANSACTION` / `SET TRANSACTION`
3525/// / `SET SESSION CHARACTERISTICS` accept. `read_only` used to be parsed
3526/// and dropped on the floor, so `BEGIN READ ONLY` opened an ordinary
3527/// read-write transaction and every write in it was accepted.
3528///
3529/// `None` on either field means the statement did not name that mode, so
3530/// the session default applies — which is not the same as naming the
3531/// default explicitly.
3532#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3533pub struct TransactionModes {
3534    pub isolation: Option<IsolationLevel>,
3535    /// `Some(true)` = READ ONLY, `Some(false)` = READ WRITE.
3536    pub read_only: Option<bool>,
3537}
3538
3539/// v7.39 — what a read-only transaction refuses, and what PG calls it.
3540///
3541/// SPG did not enforce read-only transactions at all: `BEGIN READ ONLY;
3542/// INSERT …` answered `INSERT 0 1` and committed, and
3543/// `default_transaction_read_only = on` changed nothing. Both GUCs were
3544/// in the inventory, so a session could set one, read it back, and be
3545/// told it held a guarantee nothing was enforcing. Applications open
3546/// read-only transactions as a SAFETY measure — a reporting connection,
3547/// a read-only leg in a pool, a "this path must not write" discipline —
3548/// so accepting the writes is the worst possible answer.
3549///
3550/// `Some(tag)` means refuse with PG's message, `cannot execute {tag} in
3551/// a read-only transaction` (SQLSTATE 25006). Every tag below was read
3552/// back from PostgreSQL 18.6 by running the statement inside
3553/// `BEGIN READ ONLY`, one statement per transaction so no error could be
3554/// attributed to the wrong line.
3555///
3556/// Several answers were not what one would guess, which is why they were
3557/// measured rather than reasoned:
3558///
3559///   * `CREATE TEMP TABLE` is REFUSED, tagged `CREATE TABLE`.
3560///   * `NOTIFY`, `LISTEN` and `REINDEX` are ALLOWED.
3561///   * `PREPARE` of an INSERT is ALLOWED — only the EXECUTE writes.
3562///   * `UPDATE … WHERE false`, which changes nothing, is still REFUSED:
3563///     the verb decides, not the row count.
3564///   * `GRANT`, `COMMENT ON` and `SELECT … FOR SHARE` are all REFUSED.
3565///
3566/// The match is exhaustive on purpose. A new statement cannot be added
3567/// without deciding here whether it writes, which is the failure this
3568/// repository keeps meeting: one member of a family gets handled and its
3569/// siblings quietly do not.
3570impl Statement {
3571    #[must_use]
3572    pub fn read_only_violation_tag(&self) -> Option<&'static str> {
3573        match self {
3574            // ---- writes rows -------------------------------------------
3575            Self::Insert { .. } => Some("INSERT"),
3576            Self::Update { .. } => Some("UPDATE"),
3577            Self::Delete { .. } => Some("DELETE"),
3578            Self::Merge { .. } => Some("MERGE"),
3579            Self::Truncate { .. } => Some("TRUNCATE TABLE"),
3580            Self::CopyFromFile { .. } => Some("COPY FROM"),
3581
3582            // A SELECT that takes row locks writes lock state, and PG
3583            // names the strength it was asked for.
3584            Self::Select(sel) => sel.locking.as_ref().map(|l| match l.strength {
3585                LockStrength::Update => "SELECT FOR UPDATE",
3586                LockStrength::NoKeyUpdate => "SELECT FOR NO KEY UPDATE",
3587                LockStrength::Share => "SELECT FOR SHARE",
3588                LockStrength::KeyShare => "SELECT FOR KEY SHARE",
3589            }),
3590
3591            // ---- changes the catalog -----------------------------------
3592            Self::CreateTable { .. } => Some("CREATE TABLE"),
3593            Self::DropTable { .. } => Some("DROP TABLE"),
3594            Self::AlterTable { .. } => Some("ALTER TABLE"),
3595            Self::CreateIndex { .. } => Some("CREATE INDEX"),
3596            Self::DropIndex { .. } => Some("DROP INDEX"),
3597            Self::AlterIndex { .. } => Some("ALTER INDEX"),
3598            Self::CreateView { .. } => Some("CREATE VIEW"),
3599            Self::DropView { .. } => Some("DROP VIEW"),
3600            Self::CreateMaterializedView { .. } => Some("CREATE MATERIALIZED VIEW"),
3601            Self::RefreshMaterializedView { .. } => Some("REFRESH MATERIALIZED VIEW"),
3602            Self::DropMaterializedView { .. } => Some("DROP MATERIALIZED VIEW"),
3603            Self::CreateSequence { .. } => Some("CREATE SEQUENCE"),
3604            Self::AlterSequence { .. } => Some("ALTER SEQUENCE"),
3605            Self::DropSequence { .. } => Some("DROP SEQUENCE"),
3606            Self::CreateType { .. } => Some("CREATE TYPE"),
3607            Self::DropType { .. } => Some("DROP TYPE"),
3608            Self::AlterTypeAddValue { .. } | Self::AlterTypeRenameValue { .. } => {
3609                Some("ALTER TYPE")
3610            }
3611            Self::CreateDomain { .. } => Some("CREATE DOMAIN"),
3612            Self::AlterDomain { .. } => Some("ALTER DOMAIN"),
3613            Self::DropDomain { .. } => Some("DROP DOMAIN"),
3614            Self::CreateSchema { .. } => Some("CREATE SCHEMA"),
3615            Self::DropSchema { .. } => Some("DROP SCHEMA"),
3616            Self::CreateFunction { .. } => Some("CREATE FUNCTION"),
3617            Self::DropFunction { .. } => Some("DROP FUNCTION"),
3618            Self::CreateTrigger { .. } => Some("CREATE TRIGGER"),
3619            Self::DropTrigger { .. } => Some("DROP TRIGGER"),
3620            Self::CreateRule { .. } => Some("CREATE RULE"),
3621            Self::DropRule { .. } => Some("DROP RULE"),
3622            Self::CreateExtension { .. } => Some("CREATE EXTENSION"),
3623            Self::CreateStatistics { .. } => Some("CREATE STATISTICS"),
3624            Self::DropStatistics { .. } => Some("DROP STATISTICS"),
3625            Self::DropAggregate { .. } => Some("DROP AGGREGATE"),
3626            Self::CommentOn { .. } => Some("COMMENT"),
3627            Self::DropDatabase { .. } => Some("DROP DATABASE"),
3628            Self::CreatePublication { .. } => Some("CREATE PUBLICATION"),
3629            Self::DropPublication { .. } => Some("DROP PUBLICATION"),
3630            Self::CreateSubscription { .. } => Some("CREATE SUBSCRIPTION"),
3631            Self::DropSubscription { .. } => Some("DROP SUBSCRIPTION"),
3632
3633            // ---- changes roles / permissions ---------------------------
3634            Self::CreateUser { .. } => Some("CREATE ROLE"),
3635            Self::DropUser { .. } => Some("DROP ROLE"),
3636            Self::AlterRolePassword { .. } | Self::SetDbRoleSetting { .. } => Some("ALTER ROLE"),
3637            Self::Grant { .. } => Some("GRANT"),
3638            Self::Revoke { .. } => Some("REVOKE"),
3639            Self::CreatePolicy { .. } => Some("CREATE POLICY"),
3640            Self::AlterPolicy { .. } => Some("ALTER POLICY"),
3641            Self::DropPolicy { .. } => Some("DROP POLICY"),
3642            Self::AlterSystem { .. } => Some("ALTER SYSTEM"),
3643
3644            // ---- SPG's own writers -------------------------------------
3645            // Rewrites cold-tier segments on disk. PG has no equivalent to
3646            // ask, so the test is what it does, not what it is called.
3647            Self::CompactColdSegments => Some("COMPACT COLD SEGMENTS"),
3648
3649            // ---- allowed -----------------------------------------------
3650            // Reads, transaction control, session state, cursors, and the
3651            // maintenance statements PG itself permits. `REINDEX` really is
3652            // allowed in a read-only transaction (measured), which is why
3653            // `Maintain` is here.
3654            //
3655            // `Prepare`, `Execute`, `Call` and `DoBlock` are allowed at
3656            // this level for the reason PG allows them: the write inside
3657            // is refused when it runs, by this same check. Measured:
3658            // `PREPARE p AS INSERT …` succeeds; `DO $$ … INSERT … $$`
3659            // fails with `cannot execute INSERT`.
3660            Self::Explain { .. }
3661            | Self::CopyTo { .. }
3662            | Self::CopyToFile { .. }
3663            | Self::Analyze { .. }
3664            | Self::Maintain { .. }
3665            | Self::Vacuum { .. }
3666            | Self::Begin { .. }
3667            | Self::Commit
3668            | Self::Rollback
3669            | Self::Savepoint { .. }
3670            | Self::RollbackToSavepoint { .. }
3671            | Self::ReleaseSavepoint { .. }
3672            | Self::PrepareTransaction { .. }
3673            | Self::SetTransaction { .. }
3674            | Self::SetConstraints { .. }
3675            | Self::SetParameter { .. }
3676            | Self::SetParameterList { .. }
3677            | Self::SetUserVars { .. }
3678            | Self::SetRole { .. }
3679            | Self::ResetParameter { .. }
3680            | Self::ShowParameter { .. }
3681            | Self::Discard { .. }
3682            | Self::Prepare { .. }
3683            | Self::Execute { .. }
3684            | Self::Deallocate { .. }
3685            | Self::Call { .. }
3686            | Self::DoBlock { .. }
3687            | Self::DeclareCursor { .. }
3688            | Self::FetchCursor { .. }
3689            | Self::MoveCursor { .. }
3690            | Self::CloseCursor { .. }
3691            | Self::Listen { .. }
3692            | Self::Notify { .. }
3693            | Self::Unlisten { .. }
3694            | Self::Kill { .. }
3695            | Self::WaitForWalPosition { .. }
3696            | Self::ValidateOnly { .. }
3697            | Self::NoOpPreventedInTransaction { .. }
3698            | Self::Empty
3699            | Self::ShowTables
3700            | Self::ShowDatabases
3701            | Self::UseDatabase(_)
3702            | Self::ShowCreateTable { .. }
3703            | Self::ShowIndexes { .. }
3704            | Self::ShowStatus
3705            | Self::ShowVariables
3706            | Self::ShowVariablesLike { .. }
3707            | Self::ShowProcesslist
3708            | Self::ShowColumns { .. }
3709            | Self::ShowUsers
3710            | Self::ShowPublications
3711            | Self::ShowSubscriptions => None,
3712        }
3713    }
3714}
3715
3716#[derive(Debug, Clone, PartialEq, Eq)]
3717pub struct LockingClause {
3718    pub strength: LockStrength,
3719    /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3720    pub of_tables: Vec<String>,
3721    pub policy: LockWait,
3722}
3723
3724/// PG's four tuple-lock strengths, weakest first.
3725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3726pub enum LockStrength {
3727    KeyShare,
3728    Share,
3729    NoKeyUpdate,
3730    Update,
3731}
3732
3733/// What to do when the row is already locked.
3734#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3735pub enum LockWait {
3736    /// Block until it is free — PG's default.
3737    #[default]
3738    Wait,
3739    /// `NOWAIT` — fail the statement with 55P03.
3740    NoWait,
3741    /// `SKIP LOCKED` — leave the row out of the result.
3742    SkipLocked,
3743}
3744
3745#[derive(Debug, Clone, PartialEq, Default)]
3746pub struct SelectStatement {
3747    /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3748    /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3749    /// whole syntax and locked nothing: two workers running the classic
3750    /// `SKIP LOCKED` queue take both took the same row.
3751    /// v7.39 (round 305) — boxed. A locking clause appears on a
3752    /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3753    /// cost every `SelectStatement` 32 bytes, and this struct sits in
3754    /// recursive evaluation frames where the engine already runs close to
3755    /// its stack budget (a 512 KB depth guard is the canary).
3756    pub locking: Option<alloc::boxed::Box<LockingClause>>,
3757    /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3758    /// expressions, materialised once at query start before the
3759    /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3760    /// only — no `WITH RECURSIVE` for v4.x.
3761    pub ctes: Vec<Cte>,
3762    pub distinct: bool,
3763    /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3764    /// keep the first row (per ORDER BY) of each group the
3765    /// expressions define. Empty = no DISTINCT ON.
3766    pub distinct_on: Vec<Expr>,
3767    pub items: Vec<SelectItem>,
3768    pub from: Option<FromClause>,
3769    pub where_: Option<Expr>,
3770    pub group_by: Option<Vec<Expr>>,
3771    /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3772    /// expands `group_by` to every non-aggregate SELECT-list item
3773    /// before the executor runs. Mutually exclusive with an
3774    /// explicit `group_by` list (the parser sets exactly one).
3775    pub group_by_all: bool,
3776    /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3777    /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3778    /// aggregate executor resolves them through the same synthetic
3779    /// schema used for the SELECT items.
3780    pub having: Option<Expr>,
3781    /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3782    /// itself a `SelectStatement` with `order_by = None` and `limit =
3783    /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3784    /// top of the chain).
3785    pub unions: Vec<(UnionKind, SelectStatement)>,
3786    /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3787    /// Keys are matched left-to-right: first key decides, ties break
3788    /// to the second, etc.
3789    pub order_by: Vec<OrderBy>,
3790    /// `LIMIT <n>` — bound on row output. `n` is an integer
3791    /// literal **or** (v7.9.24) a placeholder `$N` resolved
3792    /// against the prepared-statement Bind values. mailrs
3793    /// migration follow-up H2.
3794    pub limit: Option<LimitExpr>,
3795    /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3796    /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3797    pub offset: Option<LimitExpr>,
3798    /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3799    /// (SQL:2008). When true and an ORDER BY is present, the
3800    /// executor extends past the LIMIT-truncated tail to include
3801    /// every row whose ORDER BY key equals the last-kept row's
3802    /// key. Requires an ORDER BY; the executor errors otherwise
3803    /// (matching PG's `WITH TIES` rule). The parser was already
3804    /// accepting `WITH TIES` since Phase 5.1; this field captures
3805    /// the choice so the executor can act on it.
3806    pub limit_with_ties: bool,
3807    /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3808    /// that NOTHING referenced. PG analyses every definition whether
3809    /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3810    /// and silently succeeded here — the referenced ones get their columns
3811    /// resolved through the WindowFunction nodes they were inlined into,
3812    /// and the unreferenced ones used to be dropped at parse, unexamined.
3813    /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3814    ///
3815    /// Not part of `Display`: an unreferenced definition has no effect on
3816    /// the result, so a deparsed body (a stored view) omits it.
3817    pub window_check_exprs: Vec<Expr>,
3818}
3819
3820impl Expr {
3821    /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3822    /// directly inside this expression to `f`. `f` receives each nested
3823    /// statement once; descending further (into that statement's own
3824    /// clauses) is the caller's job, which keeps this walk finite and
3825    /// lets the caller order the recursion.
3826    ///
3827    /// The match is deliberately **wildcard-free**: a new `Expr` variant
3828    /// does not compile until it says whether it can carry a subquery.
3829    /// The row-count resolution pass is built on this, and a shape it
3830    /// silently failed to visit would leave a `LimitExpr::Expr` behind —
3831    /// which every row-count reader would take as "no limit", i.e. the
3832    /// whole table. Compile-time exhaustiveness is what rules that out.
3833    /// Iterative on purpose. Expression trees here get deep (long
3834    /// boolean chains, big IN lists), and this walk is on the path of
3835    /// every statement; recursing would add a frame per node to a stack
3836    /// budget the engine already runs close to — a depth guard that runs
3837    /// on a deliberately small stack caught exactly that. Depth costs
3838    /// heap here instead.
3839    pub fn for_each_subquery_mut<E>(
3840        &mut self,
3841        f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
3842    ) -> Result<(), E> {
3843        let mut stack: Vec<&mut Self> = alloc::vec![self];
3844        while let Some(e) = stack.pop() {
3845            match e {
3846                Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
3847                Self::NamedArg { expr, .. }
3848                | Self::Collate { expr, .. }
3849                | Self::Variadic(expr)
3850                | Self::Unary { expr, .. }
3851                | Self::Cast { expr, .. }
3852                | Self::FieldAccess { base: expr, .. }
3853                | Self::IsNull { expr, .. }
3854                | Self::BoolTest { expr, .. }
3855                | Self::Extract { source: expr, .. } => stack.push(expr),
3856                Self::Binary { lhs, rhs, .. } => {
3857                    stack.push(lhs);
3858                    stack.push(rhs);
3859                }
3860                Self::Like { expr, pattern, .. } => {
3861                    stack.push(expr);
3862                    stack.push(pattern);
3863                }
3864                Self::ArraySubscript { target, index } => {
3865                    stack.push(target);
3866                    stack.push(index);
3867                }
3868                Self::ArraySlice { target, lo, hi } => {
3869                    stack.push(target);
3870                    stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
3871                }
3872                Self::AnyAll { expr, array, .. } => {
3873                    stack.push(expr);
3874                    stack.push(array);
3875                }
3876                Self::FunctionCall { args, .. } | Self::Array(args) => {
3877                    stack.extend(args.iter_mut());
3878                }
3879                Self::AggregateOrdered {
3880                    call,
3881                    order_by,
3882                    filter,
3883                    ..
3884                } => {
3885                    stack.push(call);
3886                    stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
3887                    stack.extend(filter.iter_mut().map(|b| &mut **b));
3888                }
3889                Self::WindowFunction {
3890                    args,
3891                    partition_by,
3892                    order_by,
3893                    filter,
3894                    ..
3895                } => {
3896                    // `frame` bounds hold folded numbers / interval
3897                    // parts, never expressions — nothing to visit there.
3898                    stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
3899                    stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
3900                    stack.extend(filter.iter_mut().map(|b| &mut **b));
3901                }
3902                Self::InList { expr, list, .. } => {
3903                    stack.push(expr);
3904                    stack.extend(list.iter_mut());
3905                }
3906                Self::Case {
3907                    operand,
3908                    branches,
3909                    else_branch,
3910                } => {
3911                    stack.extend(
3912                        operand
3913                            .iter_mut()
3914                            .chain(else_branch.iter_mut())
3915                            .map(|b| &mut **b),
3916                    );
3917                    for (when, then) in branches.iter_mut() {
3918                        stack.push(when);
3919                        stack.push(then);
3920                    }
3921                }
3922                Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
3923                Self::InSubquery { expr, subquery, .. } => {
3924                    stack.push(expr);
3925                    f(subquery)?;
3926                }
3927                Self::RowInSubquery { row, subquery, .. }
3928                | Self::RowCmpSubquery { row, subquery, .. } => {
3929                    stack.extend(row.iter_mut());
3930                    f(subquery)?;
3931                }
3932            }
3933        }
3934        Ok(())
3935    }
3936}
3937
3938/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
3939/// time or a placeholder `$N` resolved during extended-query
3940/// Bind. mailrs migration follow-up H2.
3941///
3942/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
3943/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
3944/// made the compiler point at every site that used to duplicate a
3945/// row-count out of the AST, which is exactly the set that must not
3946/// bypass the resolution pre-pass.
3947#[derive(Debug, Clone, PartialEq)]
3948pub enum LimitExpr {
3949    /// `LIMIT 10` — value known at parse time.
3950    Literal(u32),
3951    /// `LIMIT $N` — the 1-based parameter index, resolved against
3952    /// the bind values when the prepared statement executes.
3953    Placeholder(u16),
3954    /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
3955    /// greatest(2,3)`: a row-count expression that isn't constant, so
3956    /// it can't be folded at parse time. Evaluated once, before
3957    /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
3958    /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
3959    /// "no limit"). **No execution path may see this variant** —
3960    /// `as_literal` would report `None`, which every row-count reader
3961    /// takes to mean "unlimited", i.e. the whole table.
3962    Expr(alloc::boxed::Box<Expr>),
3963}
3964
3965impl fmt::Display for LimitExpr {
3966    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3967        match self {
3968            Self::Literal(n) => write!(f, "{n}"),
3969            Self::Placeholder(n) => write!(f, "${n}"),
3970            // Parenthesised so the round-trip text re-parses as one
3971            // row-count expression (`LIMIT (SELECT 4)`), which is also
3972            // the only spelling `FETCH FIRST` accepts.
3973            Self::Expr(e) => write!(f, "({e})"),
3974        }
3975    }
3976}
3977
3978impl LimitExpr {
3979    /// Convenience for the simple-query path where no placeholders
3980    /// can possibly exist. Returns the literal value or `None` if
3981    /// this is a placeholder (caller must surface as Unsupported).
3982    ///
3983    /// v7.39 (round 305) — `None` is read by every row-count consumer as
3984    /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
3985    /// therefore silently return the whole table, so the engine's
3986    /// `resolve_limit_exprs` pre-pass rewrites the variant away before
3987    /// dispatch. The assertion makes a missed nesting site fail loudly
3988    /// in every test build rather than quietly widening a result set.
3989    #[must_use]
3990    pub fn as_literal(&self) -> Option<u32> {
3991        match self {
3992            Self::Literal(n) => Some(*n),
3993            Self::Placeholder(_) => None,
3994            Self::Expr(_) => {
3995                debug_assert!(
3996                    false,
3997                    "LimitExpr::Expr reached execution — resolve_limit_exprs \
3998                     missed a nesting site; treating it as `no limit` would \
3999                     return every row"
4000                );
4001                None
4002            }
4003        }
4004    }
4005}
4006
4007/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
4008/// the engine's `substitute_placeholders` pass these are
4009/// always Literal; in the simple-query path a Placeholder
4010/// shape returns None (executor surfaces as
4011/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
4012impl SelectStatement {
4013    #[must_use]
4014    pub fn limit_literal(&self) -> Option<u32> {
4015        self.limit.as_ref().and_then(LimitExpr::as_literal)
4016    }
4017    #[must_use]
4018    pub fn offset_literal(&self) -> Option<u32> {
4019        self.offset.as_ref().and_then(LimitExpr::as_literal)
4020    }
4021}
4022
4023#[derive(Debug, Clone, PartialEq)]
4024pub struct Cte {
4025    pub name: String,
4026    /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
4027    /// classical case) or a data-modifying statement
4028    /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
4029    /// CTE semantics. The modifying body's RETURNING projection
4030    /// becomes the materialised CTE table the outer query can
4031    /// reference; the modifying statement runs once before the
4032    /// outer query, within the same transaction.
4033    pub body: CteBody,
4034    /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
4035    /// RECURSIVE keyword. Applies to every CTE in the clause per
4036    /// PG semantics. A non-recursive body in a RECURSIVE WITH is
4037    /// allowed; the engine just runs it once.
4038    pub recursive: bool,
4039    /// v4.22: optional `WITH name(a, b, c)` column-name list. When
4040    /// non-empty, these override the body's output column names
4041    /// position-by-position; the engine errors out if the count
4042    /// doesn't match the body's projection width.
4043    pub column_overrides: Vec<String>,
4044    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
4045    /// SET seqcol` on a recursive CTE. Desugared at parse time into an
4046    /// extra ordering column on the body (see `rewrite_search_and_cycle`).
4047    pub search: Option<SearchClause>,
4048    /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
4049    /// USING pathcol` cycle detection, desugared at parse time.
4050    pub cycle: Option<CycleClause>,
4051}
4052
4053/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
4054#[derive(Debug, Clone, PartialEq)]
4055pub struct SearchClause {
4056    /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
4057    pub depth_first: bool,
4058    /// The CTE output columns the search orders by.
4059    pub by_columns: Vec<String>,
4060    /// The new column holding the ordering key (a row-array for depth,
4061    /// a `(depth, keys…)` row for breadth).
4062    pub set_column: String,
4063}
4064
4065/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
4066#[derive(Debug, Clone, PartialEq)]
4067pub struct CycleClause {
4068    /// Columns whose repetition along a path marks a cycle.
4069    pub columns: Vec<String>,
4070    /// The new boolean-ish column set to `mark_value` on a cycle.
4071    pub mark_column: String,
4072    /// Value written to `mark_column` when a cycle is detected (default
4073    /// `true`); `default_value` otherwise. PG allows any type; SPG carries
4074    /// them as literals.
4075    pub mark_value: Option<Literal>,
4076    pub default_value: Option<Literal>,
4077    /// The new column accumulating the visited-row path array.
4078    pub path_column: String,
4079}
4080
4081/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
4082/// (Insert / Update / Delete with optional RETURNING). The
4083/// data-modifying variants must carry a RETURNING projection for the
4084/// outer query to reference the CTE alias by; an empty RETURNING is
4085/// only valid if no outer reference materialises (rare — typically
4086/// caught at planning).
4087#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
4088#[derive(Debug, Clone, PartialEq)]
4089pub enum CteBody {
4090    Select(SelectStatement),
4091    Insert(Box<InsertStatement>),
4092    Update(Box<UpdateStatement>),
4093    Delete(Box<DeleteStatement>),
4094    /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
4095    /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
4096    Merge(Box<MergeStatement>),
4097}
4098
4099impl CteBody {
4100    /// Convenience accessor used by classical (read-only) CTE
4101    /// callsites that still expect a SELECT body. Returns None for
4102    /// data-modifying CTEs; callers must explicitly route those
4103    /// through `exec_with_ctes`'s modifying branch.
4104    #[must_use]
4105    pub fn as_select(&self) -> Option<&SelectStatement> {
4106        match self {
4107            Self::Select(s) => Some(s),
4108            _ => None,
4109        }
4110    }
4111
4112    #[must_use]
4113    pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
4114        match self {
4115            Self::Select(s) => Some(s),
4116            _ => None,
4117        }
4118    }
4119
4120    #[must_use]
4121    pub fn is_modifying(&self) -> bool {
4122        !matches!(self, Self::Select(_))
4123    }
4124}
4125
4126#[derive(Debug, Clone, PartialEq)]
4127pub struct OrderBy {
4128    pub expr: Expr,
4129    /// `false` = ASC (default), `true` = DESC.
4130    pub desc: bool,
4131    /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
4132    /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
4133    /// NULLS FIRST for DESC); the engine resolves the effective
4134    /// value via `nulls_first.unwrap_or(desc)`.
4135    pub nulls_first: Option<bool>,
4136    /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
4137    /// It lives here rather than in the expression for the same reason
4138    /// `desc` does: at an ORDER BY key a collation is ordering
4139    /// information, and nothing downstream of the sort needs it. A new
4140    /// `Expr` variant would instead put a new arm on `eval_expr`, which
4141    /// this repo has measured to overflow the debug stack.
4142    ///
4143    /// `None` means none was written, and the key falls back to whatever
4144    /// its COLUMN declares — which is every key that existed before this.
4145    pub collation: Option<String>,
4146}
4147
4148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4149pub enum UnionKind {
4150    /// `UNION` — dedupes the combined set.
4151    Distinct,
4152    /// `UNION ALL` — concatenates without dedup.
4153    All,
4154    /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
4155    /// present on both sides.
4156    Intersect,
4157    /// `INTERSECT ALL` — multiset intersection (min per-row count).
4158    IntersectAll,
4159    /// `EXCEPT` — distinct left rows absent from the right.
4160    Except,
4161    /// `EXCEPT ALL` — multiset subtraction.
4162    ExceptAll,
4163}
4164
4165#[derive(Debug, Clone, PartialEq)]
4166pub enum SelectItem {
4167    Wildcard,
4168    /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
4169    /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
4170    /// `NEW` pseudo-relation).
4171    QualifiedWildcard(String),
4172    Expr {
4173        expr: Expr,
4174        alias: Option<String>,
4175    },
4176}
4177
4178#[derive(Debug, Clone, PartialEq)]
4179pub struct TableRef {
4180    pub name: String,
4181    pub alias: Option<String>,
4182    /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
4183    /// children.
4184    ///
4185    /// The keyword used to be absorbed at parse time, on the reasoning
4186    /// that SPG's inheritance children are separate relations a plain
4187    /// scan does not descend into — so ONLY already described what the
4188    /// scan did. That stopped being true when a partition parent
4189    /// started unioning its children: measured, `SELECT count(*) FROM
4190    /// ONLY <partitioned parent>` answered 2 where PG answers 0.
4191    pub only: bool,
4192    /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
4193    /// When `Some(id)`, the scan restricts to rows that live in
4194    /// segment `<id>` only — useful for forensic inspection of a
4195    /// specific freezer-emitted segment without exposing the hot
4196    /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
4197    /// is STABILITY carve-out for v6.10 — needs the freezer to
4198    /// stamp each segment with a wall-clock at creation time.
4199    pub as_of_segment: Option<u32>,
4200    /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
4201    /// source. When `Some`, `name` is the alias (defaulting to
4202    /// `"unnest"` when no `AS` is given) and the engine builds a
4203    /// synthetic single-column table by evaluating the expression
4204    /// once at SELECT entry. Each TEXT[] element becomes one row;
4205    /// NULL elements become NULL cells. v7.11 supported
4206    /// uncorrelated UNNEST only as the FROM primary; v7.13.2
4207    /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
4208    /// position (cross-join with regular tables).
4209    pub unnest_expr: Option<Box<Expr>>,
4210    /// v7.13.2 — mailrs round-6 S5. PG-standard
4211    /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
4212    /// when non-empty, the first entry overrides the projected
4213    /// column name for the unnested column. Empty = fall back to
4214    /// the table alias (pre-v7.13.2 behaviour).
4215    pub unnest_column_aliases: Vec<String>,
4216    /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
4217    /// row-stream gains a trailing BIGINT column counting rows
4218    /// from 1 in element order. PG names it `ordinality`; a second
4219    /// entry in the column-alias list renames it.
4220    pub with_ordinality: bool,
4221    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4222    /// [, step])` set-returning source. When `Some`, the engine
4223    /// materialises a single-column virtual table by stepping
4224    /// `start` to `stop` inclusive. Args are the literal arg list
4225    /// (2 for default-step, 3 for explicit-step). Supports:
4226    ///   * SmallInt / Int / BigInt with integer step (default = 1)
4227    ///   * Timestamp with INTERVAL step (PG date-range pattern)
4228    /// Mutually exclusive with `unnest_expr` — both populate the
4229    /// same downstream dispatch slot. `name` defaults to
4230    /// `"generate_series"` when no alias is provided.
4231    pub generate_series_args: Option<Vec<Expr>>,
4232    /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
4233    /// table. When `Some`, the TableRef is a parenthesised SELECT
4234    /// that may reference columns from the preceding FROM items
4235    /// (correlated derived table). The executor materialises the
4236    /// subquery per left-row, substituting outer-column references
4237    /// against the current join row's values before running the
4238    /// inner SELECT, then cross-joins the result back.
4239    /// Mutually exclusive with `name` / `unnest_expr` /
4240    /// `generate_series_args`.
4241    pub lateral_subquery: Option<Box<SelectStatement>>,
4242    /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
4243    /// function as a FROM item. PG semantics: for each key/value
4244    /// pair in the JSONB object argument, emit one (key TEXT,
4245    /// value TEXT) row. When prefixed by `LATERAL` and joined via
4246    /// `CROSS JOIN LATERAL`, the argument may reference columns
4247    /// from a preceding FROM item, in which case the executor
4248    /// evaluates `<expr>` per outer row.
4249    /// Mutually exclusive with `unnest_expr` / `generate_series_args`
4250    /// / `lateral_subquery`. The optional `LATERAL` keyword does not
4251    /// require a separate flag — the executor evaluates per-row
4252    /// whenever the join sits in a JoinKind context.
4253    ///
4254    /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4255    /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4256    /// `json_each` / `json_each_text`) so the executor picks the
4257    /// value-column rendering (JSON text vs unwrapped text).
4258    pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4259    /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4260    /// function channel: `(lowercase fn name, args)`. Carries
4261    /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4262    /// dispatches by name.
4263    pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4264    /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4265    /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4266    /// reference to it yields the value, not a one-field composite
4267    /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4268    /// desugared shape is indistinguishable from a hand-written
4269    /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4270    /// only the parser knows which one it built, so it says so here.
4271    pub scalar_fn_item: bool,
4272    /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4273    /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4274    /// target-list SRFs follow — see round 67). The array-returning family keeps
4275    /// its own lowering; this channel carries the ones that have no array form
4276    /// (`generate_series`, a user `RETURNS SETOF` function).
4277    pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4278    /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4279    /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4280    /// tables (implicit LATERAL, like every SRF channel). Executed by
4281    /// walking the row path over the parsed doc, then each column's
4282    /// path per row-item; NESTED expands as a per-parent outer join.
4283    pub json_table: Option<Box<JsonTable>>,
4284}
4285
4286/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4287#[derive(Debug, Clone, PartialEq)]
4288pub struct JsonTable {
4289    /// The document expression (jsonb/json/text). May reference outer
4290    /// columns → implicit LATERAL.
4291    pub doc: Box<Expr>,
4292    /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4293    /// match is one row's context item.
4294    pub row_path: String,
4295    /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4296    pub columns: Vec<JsonTableColumn>,
4297    /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4298    pub passing: Vec<(String, Expr)>,
4299}
4300
4301/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4302#[derive(Debug, Clone, PartialEq)]
4303pub enum JsonTableColumn {
4304    /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4305    Ordinality { name: String },
4306    /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4307    /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4308    /// `<name> <type> EXISTS [PATH '<p>']`.
4309    Regular {
4310        name: String,
4311        ty: ColumnTypeName,
4312        /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4313        path: String,
4314        /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4315        exists: bool,
4316        /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4317        format_json: bool,
4318        /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4319        wrapper: bool,
4320        /// Behaviour when the path matches nothing (default NULL).
4321        on_empty: JsonTableOnBehavior,
4322        /// Behaviour when coercion fails (default NULL).
4323        on_error: JsonTableOnBehavior,
4324    },
4325    /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4326    /// row like a LEFT JOIN (a parent with no nested match still emits one
4327    /// row, nested cols NULL).
4328    Nested {
4329        path: String,
4330        columns: Vec<JsonTableColumn>,
4331    },
4332}
4333
4334/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4335#[derive(Debug, Clone, PartialEq)]
4336pub enum JsonTableOnBehavior {
4337    /// Default: the column value is NULL.
4338    Null,
4339    /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4340    Error,
4341    /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4342    Default(Box<Expr>),
4343}
4344
4345/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4346/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4347/// joins evaluate left-associatively in nested-loop order.
4348#[derive(Debug, Clone, PartialEq)]
4349pub struct FromClause {
4350    pub primary: TableRef,
4351    pub joins: Vec<FromJoin>,
4352}
4353
4354#[derive(Debug, Clone, PartialEq)]
4355pub struct FromJoin {
4356    pub kind: JoinKind,
4357    pub table: TableRef,
4358    /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4359    pub on: Option<Expr>,
4360    /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4361    /// USING column list so the executor can perform PG's column-merge
4362    /// (the join columns collapse to a single unqualified output column,
4363    /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4364    /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4365    /// USING into an equivalent `on` predicate so the join filter/count
4366    /// path works unchanged; `using_cols` drives only the output-shape
4367    /// rewrite. Empty/`None` for `ON` and CROSS joins.
4368    pub using_cols: Option<Vec<String>>,
4369    /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4370    /// column names are not known until the table schemas are available
4371    /// (parse time is schema-less), so the parser only sets this flag and
4372    /// leaves `on`/`using_cols` empty; the engine resolves the common
4373    /// columns at execution time, synthesises the `on` predicate + the
4374    /// USING column-merge, and clears the flag. If there are no common
4375    /// columns PG treats it as a CROSS join.
4376    pub natural: bool,
4377}
4378
4379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4380pub enum JoinKind {
4381    Inner,
4382    Left,
4383    Cross,
4384    /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4385    /// NULL-filling the left (drive) columns on unmatched right rows.
4386    /// The executor runs the LEFT algorithm's mirror: it tracks which
4387    /// peer rows matched and emits the unmatched ones with a NULL-left
4388    /// tuple after the probe loop. Output column order is unchanged
4389    /// (left-table cols then right-table cols).
4390    Right,
4391    /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4392    /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4393    FullOuter,
4394    /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4395    /// once, paired with the first peer row that satisfies the ON. Not
4396    /// reachable from SQL — the EXISTS pull-up emits it, which is what
4397    /// frees positive EXISTS from the round-721 uniqueness gate (an
4398    /// INNER join would multiply the outer rows; a semi join cannot).
4399    Semi,
4400}
4401
4402#[derive(Debug, Clone, PartialEq)]
4403pub enum Expr {
4404    Literal(Literal),
4405    /// v7.39.2 — `<expr> COLLATE <name>`: the collation this expression
4406    /// compares under, whatever the column or the database says.
4407    ///
4408    /// The parser used to refuse the locale names in this position and
4409    /// SILENTLY ABSORB the byte-order ones, so `'a' COLLATE "C" < 'B'`
4410    /// answered `t` where PostgreSQL 18.6 answers `f` — the one family
4411    /// it let through is the one where dropping it changes the answer.
4412    ///
4413    /// Whether dropping is safe depends on the DATABASE's own collation,
4414    /// which the parser cannot see: under `SPG_LC_COLLATE=C` absorbing
4415    /// `COLLATE "C"` is exactly right. So the name rides along and the
4416    /// engine, which knows, decides.
4417    Collate {
4418        expr: Box<Expr>,
4419        collation: String,
4420    },
4421    Column(ColumnName),
4422    /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4423    /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4424    /// callee's declared parameter names, and a user function's live in the
4425    /// catalog — which the parser cannot see. So the name rides along in the
4426    /// tree and the evaluator, which has the catalog, does the reordering.
4427    /// Appears only inside a `FunctionCall`'s argument list.
4428    NamedArg {
4429        name: String,
4430        expr: Box<Expr>,
4431    },
4432    /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
4433    /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
4434    /// expression evaluates to an array whose elements the evaluator splices
4435    /// into the call as individual trailing arguments. Appears only inside a
4436    /// `FunctionCall`'s argument list.
4437    Variadic(Box<Expr>),
4438    /// v6.1.1 — `$N` parameter placeholder for the extended query
4439    /// protocol. The number is 1-based per PostgreSQL convention.
4440    /// Evaluation looks up `params[N-1]` from the prepared-statement
4441    /// bind buffer; out-of-range indices raise a runtime error
4442    /// (same shape as a column-not-found miss).
4443    Placeholder(u16),
4444    Binary {
4445        lhs: Box<Expr>,
4446        op: BinOp,
4447        rhs: Box<Expr>,
4448    },
4449    Unary {
4450        op: UnOp,
4451        expr: Box<Expr>,
4452    },
4453    /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
4454    /// TEXT, BOOL targets; engine coerces at evaluation time.
4455    Cast {
4456        expr: Box<Expr>,
4457        target: CastTarget,
4458    },
4459    /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
4460    /// evaluates to a composite/record value (an explicit `ROW(...)`, a
4461    /// whole-row reference, or a composite-returning function); `field` names
4462    /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
4463    /// column names for a whole-row). Only the parenthesised form reaches
4464    /// here — a bare `a.b` is parsed as a qualified column reference.
4465    FieldAccess {
4466        base: Box<Expr>,
4467        field: String,
4468    },
4469    /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
4470    IsNull {
4471        expr: Box<Expr>,
4472        negated: bool,
4473    },
4474    /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
4475    /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
4476    /// `Some(false)` for FALSE and `None` for UNKNOWN.
4477    ///
4478    /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
4479    /// The semantics were right, but the AST then had no way to say what
4480    /// the user wrote, so every renderer printed the lowering:
4481    /// `CHECK ((a > 1) IS TRUE)` came back as
4482    /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
4483    /// dumped view lost the form too.
4484    BoolTest {
4485        expr: Box<Expr>,
4486        value: Option<bool>,
4487        negated: bool,
4488    },
4489    /// Function call `name(args...)`. v1.4 supports a small built-in set
4490    /// (length, upper, lower, abs, coalesce); unknown names error at eval
4491    /// time so the parser stays open for v1.5 aggregates.
4492    FunctionCall {
4493        name: String,
4494        args: Vec<Expr>,
4495    },
4496    /// v7.24 (mailrs round-16 A) — an aggregate call with an
4497    /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
4498    /// Wraps the plain [`Expr::FunctionCall`] so every existing
4499    /// FunctionCall consumer stays untouched; only the aggregate
4500    /// executor (and the expression walkers) know the wrapper.
4501    /// Non-aggregate evaluation contexts reject it at eval time.
4502    AggregateOrdered {
4503        call: Box<Expr>,
4504        order_by: Vec<OrderBy>,
4505        /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
4506        /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
4507        /// aggregate modifier so plain FunctionCall stays untouched.
4508        distinct: bool,
4509        /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
4510        /// Only the rows where `cond` is true contribute to this
4511        /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
4512        /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
4513        /// END)`, which is faithful for NULL-ignoring aggregates but
4514        /// WRONG for `array_agg` (it would collect a NULL per excluded
4515        /// row). The executor instead skips excluded rows before
4516        /// accumulation, which is correct for every aggregate.
4517        filter: Option<Box<Expr>>,
4518    },
4519    /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
4520    /// wildcards are `%` (any run) and `_` (one char), backslash escapes
4521    /// the next char (so `\%` matches a literal `%`).
4522    Like {
4523        expr: Box<Expr>,
4524        pattern: Box<Expr>,
4525        negated: bool,
4526        /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
4527        /// match. PG folds both operands.
4528        case_insensitive: bool,
4529    },
4530    /// v4.12 window function call: `name(args) OVER (PARTITION BY
4531    /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
4532    /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
4533    /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
4534    /// unordered windows and "from start of partition through
4535    /// current row" for ordered windows — no explicit ROWS /
4536    /// RANGE clause in v4.12 MVP.
4537    WindowFunction {
4538        name: String,
4539        args: Vec<Expr>,
4540        partition_by: Vec<Expr>,
4541        /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
4542        /// (None = PG default, same contract as [`OrderBy`]).
4543        order_by: Vec<(
4544            Expr,
4545            bool,         /* desc */
4546            Option<bool>, /* nulls_first */
4547        )>,
4548        /// v4.20 explicit frame. `None` means "use the default":
4549        /// whole-partition when unordered, running aggregate from
4550        /// partition start through current row when ordered.
4551        frame: Option<WindowFrame>,
4552        /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
4553        /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
4554        /// `Respect` (PG / ANSI default — NULLs participate). Other
4555        /// window functions ignore this flag.
4556        null_treatment: NullTreatment,
4557        /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
4558        /// = no FILTER. Only aggregate window functions honor it; the
4559        /// predicate restricts which peer rows contribute within the frame.
4560        filter: Option<Box<Expr>>,
4561    },
4562    /// v4.10 scalar subquery — `(SELECT ...)` used in expression
4563    /// position. Must return exactly one row × one column at eval
4564    /// time; the engine errors out otherwise. Uncorrelated only —
4565    /// the inner SELECT cannot reference outer columns.
4566    ScalarSubquery(Box<SelectStatement>),
4567    /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
4568    /// projection is ignored; only row-count matters.
4569    Exists {
4570        subquery: Box<SelectStatement>,
4571        negated: bool,
4572    },
4573    /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
4574    /// project exactly one column; membership is tested by Eq
4575    /// against each row's value (NULL handling follows ANSI:
4576    /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
4577    InSubquery {
4578        expr: Box<Expr>,
4579        subquery: Box<SelectStatement>,
4580        negated: bool,
4581    },
4582    /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
4583    /// against a multi-column subquery. Row comparisons against a *list*
4584    /// decompose to OR-of-AND at parse time, but the subquery form can't
4585    /// (its rows are only known at runtime), so this survives as its own
4586    /// node evaluated with PG's row-comparison three-valued logic.
4587    RowInSubquery {
4588        row: Vec<Expr>,
4589        subquery: Box<SelectStatement>,
4590        negated: bool,
4591    },
4592    /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
4593    /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
4594    /// RowInSubquery, the literal-RHS form decomposes at parse time but the
4595    /// subquery form can't, so it survives as its own node. The subquery
4596    /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
4597    RowCmpSubquery {
4598        row: Vec<Expr>,
4599        op: BinOp,
4600        subquery: Box<SelectStatement>,
4601    },
4602    /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
4603    /// list. Both the parser's literal-list path and the engine's
4604    /// IN-subquery materialisation used to desugar into a left-deep
4605    /// OR-Eq chain, so expression depth scaled with the element count
4606    /// — a 24k-row subquery result overflowed the 2 MiB worker stack
4607    /// (recursive eval AND recursive Box drop) and aborted embedding
4608    /// host processes. The flat node keeps depth constant: eval is an
4609    /// iterative scan with PG three-valued logic, drop is a Vec drop.
4610    InList {
4611        expr: Box<Expr>,
4612        list: Vec<Expr>,
4613        negated: bool,
4614    },
4615    /// `EXTRACT(<field> FROM <source>)` — pull an integer component
4616    /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
4617    /// because the `FROM` keyword is what separates the two halves,
4618    /// not a comma.
4619    Extract {
4620        field: ExtractField,
4621        source: Box<Expr>,
4622    },
4623    /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
4624    /// element is evaluated independently; NULLs are allowed.
4625    /// v7.10 supports only single-dimension TEXT[] semantically;
4626    /// non-text elements coerce at engine evaluation time when
4627    /// the surrounding context (column type / cast) makes the
4628    /// target clear.
4629    Array(Vec<Expr>),
4630    /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
4631    /// engine returns NULL for out-of-range indices.
4632    ArraySubscript {
4633        target: Box<Expr>,
4634        index: Box<Expr>,
4635    },
4636    /// Array slice `arr[lo:hi]` — PG 1-based, both ends
4637    /// inclusive; a missing bound extends to that end of the
4638    /// array and out-of-range bounds clamp. Returns an array of
4639    /// the same element type.
4640    ArraySlice {
4641        target: Box<Expr>,
4642        lo: Option<Box<Expr>>,
4643        hi: Option<Box<Expr>>,
4644    },
4645    /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
4646    /// operator is the comparison binary op (Eq / Ne / Lt / …);
4647    /// the engine desugars: `ANY` returns true if any element
4648    /// satisfies; `ALL` returns true only if every element does.
4649    /// NULL handling follows PG's three-valued logic.
4650    AnyAll {
4651        expr: Box<Expr>,
4652        op: BinOp,
4653        array: Box<Expr>,
4654        /// `true` = ANY, `false` = ALL.
4655        is_any: bool,
4656    },
4657    /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
4658    /// (searched form, `operand` is None) and
4659    /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
4660    /// `operand` is the lead expression compared against each
4661    /// branch's match). Each `(when_expr, then_expr)` branch
4662    /// stays as written; engine short-circuits on the first match.
4663    /// `else_branch` is `None` when no ELSE; evaluates to NULL.
4664    /// mailrs round-5 G9.
4665    Case {
4666        operand: Option<Box<Expr>>,
4667        branches: Vec<(Expr, Expr)>,
4668        else_branch: Option<Box<Expr>>,
4669    },
4670}
4671
4672/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
4673/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
4674/// in the offset walk. `Ignore` causes the function to skip NULL
4675/// values in the argument expression, returning the next non-NULL.
4676#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4677pub enum NullTreatment {
4678    #[default]
4679    Respect,
4680    Ignore,
4681}
4682
4683/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
4684/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
4685/// where end implicitly = CURRENT ROW.
4686#[derive(Debug, Clone, PartialEq, Eq)]
4687pub struct WindowFrame {
4688    pub kind: FrameKind,
4689    pub start: FrameBound,
4690    pub end: Option<FrameBound>,
4691    /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
4692    /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
4693    /// no-op; CURRENT ROW drops the current row from the frame.
4694    pub exclude: FrameExclusion,
4695}
4696
4697#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4698pub enum FrameExclusion {
4699    /// Default — exclude nothing.
4700    #[default]
4701    NoOthers,
4702    /// Drop the current row from the frame.
4703    CurrentRow,
4704    /// Drop the current row's whole peer group.
4705    Group,
4706    /// Drop the current row's peers but keep the current row.
4707    Ties,
4708}
4709
4710#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4711pub enum FrameKind {
4712    Rows,
4713    Range,
4714    /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
4715    /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
4716    /// bounds (no explicit integer offsets) GROUPS behaves identically
4717    /// to RANGE — both consult the peer-group of the current row.
4718    /// Integer offsets are not yet supported; the executor rejects
4719    /// them at run time.
4720    Groups,
4721}
4722
4723#[derive(Debug, Clone, PartialEq, Eq)]
4724pub enum FrameBound {
4725    UnboundedPreceding,
4726    OffsetPreceding(u64),
4727    CurrentRow,
4728    OffsetFollowing(u64),
4729    UnboundedFollowing,
4730    /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
4731    /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
4732    /// interval is folded to its (months, days, micros) components at
4733    /// parse time.
4734    IntervalPreceding {
4735        months: i32,
4736        days: i32,
4737        micros: i64,
4738    },
4739    IntervalFollowing {
4740        months: i32,
4741        days: i32,
4742        micros: i64,
4743    },
4744}
4745
4746impl fmt::Display for FrameBound {
4747    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4748        match self {
4749            Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
4750            Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
4751            Self::CurrentRow => f.write_str("CURRENT ROW"),
4752            Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
4753            Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
4754            Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
4755            Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
4756        }
4757    }
4758}
4759
4760#[derive(Debug, Clone, PartialEq, Eq)]
4761pub enum ExtractField {
4762    Year,
4763    Month,
4764    Day,
4765    Hour,
4766    Minute,
4767    Second,
4768    Microsecond,
4769    /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
4770    /// SPG keeps the integer convention — truncated seconds).
4771    Epoch,
4772    /// Day of week, 0 = Sunday … 6 = Saturday.
4773    Dow,
4774    /// ISO day of week, 1 = Monday … 7 = Sunday.
4775    Isodow,
4776    /// Day of year, 1-366.
4777    Doy,
4778    /// ISO 8601 week number, 1-53.
4779    Week,
4780    /// ISO 8601 week-numbering year (pairs with `Week`).
4781    Isoyear,
4782    /// Quarter, 1-4.
4783    Quarter,
4784    /// Year divided by 10 (floor).
4785    Decade,
4786    /// Century — 2001-2100 is century 21.
4787    Century,
4788    /// Millennium — 2001-3000 is millennium 3.
4789    Millennium,
4790    /// Julian day number (truncated for timestamps).
4791    Julian,
4792    /// Seconds and fraction in milliseconds (ss·1000 + frac).
4793    Millisecond,
4794    /// UTC offset in seconds — SPG sessions run UTC, so 0.
4795    Timezone,
4796    /// Hour component of the UTC offset — 0.
4797    TimezoneHour,
4798    /// Minute component of the UTC offset — 0.
4799    TimezoneMinute,
4800    /// v7.39 (round 253) — a field name the parser does not know. PG
4801    /// resolves EXTRACT fields at RUNTIME and reports them with the
4802    /// source type (`unit "nosuch" not recognized for type timestamp
4803    /// without time zone`, 22023), so the parser carries the raw name
4804    /// instead of rejecting.
4805    Other(String),
4806}
4807
4808impl fmt::Display for ExtractField {
4809    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4810        f.write_str(match self {
4811            Self::Year => "YEAR",
4812            Self::Month => "MONTH",
4813            Self::Day => "DAY",
4814            Self::Hour => "HOUR",
4815            Self::Minute => "MINUTE",
4816            Self::Second => "SECOND",
4817            Self::Microsecond => "MICROSECOND",
4818            Self::Epoch => "EPOCH",
4819            Self::Dow => "DOW",
4820            Self::Isodow => "ISODOW",
4821            Self::Doy => "DOY",
4822            Self::Week => "WEEK",
4823            Self::Isoyear => "ISOYEAR",
4824            Self::Quarter => "QUARTER",
4825            Self::Decade => "DECADE",
4826            Self::Century => "CENTURY",
4827            Self::Millennium => "MILLENNIUM",
4828            Self::Julian => "JULIAN",
4829            Self::Millisecond => "MILLISECOND",
4830            Self::Timezone => "TIMEZONE",
4831            Self::TimezoneHour => "TIMEZONE_HOUR",
4832            Self::TimezoneMinute => "TIMEZONE_MINUTE",
4833            Self::Other(name) => return f.write_str(name),
4834        })
4835    }
4836}
4837
4838#[derive(Debug, Clone, PartialEq, Eq)]
4839pub enum CastTarget {
4840    Int,
4841    BigInt,
4842    Float,
4843    Text,
4844    Bool,
4845    Vector,
4846    Date,
4847    Timestamp,
4848    /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
4849    /// H3a. Engine reuses the existing runtime-interval / timestamp
4850    /// paths (parse the text input, return the matching Value).
4851    Interval,
4852    Timestamptz,
4853    /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
4854    /// types (v7.9.0); the cast just routes Text→Json with the
4855    /// requested OID for the wire layer.
4856    Json,
4857    Jsonb,
4858    /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
4859    /// compatibility; engine surfaces as Unsupported with a
4860    /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
4861    RegType,
4862    RegClass,
4863    /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
4864    /// the PG external array form `{a,b,NULL}`.
4865    TextArray,
4866    /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
4867    /// `{1,2,3}` or widens a `TextArray` whose elements are
4868    /// integer-shaped.
4869    IntArray,
4870    BigIntArray,
4871    /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
4872    /// external form text representation. Used by pg_dump output
4873    /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
4874    TsVector,
4875    TsQuery,
4876    /// v7.17.0 — `::uuid`. Decodes the LHS Text via
4877    /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
4878    /// unhyphenated, uppercase, and brace-wrapped forms); malformed
4879    /// input is a SQL error.
4880    Uuid,
4881    /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
4882    /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
4883    /// inputs pass through unchanged. Closes the mailrs D-pre #3
4884    /// reverse-acceptance gap — anywhere a PG schema writes
4885    /// `expr::bytea`, SPG now matches.
4886    Bytea,
4887    /// v7.37.5 ship triage — generic cast target for the long tail
4888    /// of PG type names the parser meets in `expr::TYPE` shapes that
4889    /// don't deserve their own enum variant. The engine routes these
4890    /// through `column_type_to_data_type` + the existing typed
4891    /// `coerce_value` dispatch, so adding a new PG type to SPG
4892    /// implicitly adds its cast-target form too — no parser change
4893    /// per type. The string carries the lowercase PG type ident
4894    /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
4895    /// a clear message when the type isn't known.
4896    Named(String),
4897}
4898
4899impl fmt::Display for CastTarget {
4900    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4901        f.write_str(match self {
4902            Self::Int => "int",
4903            Self::BigInt => "bigint",
4904            Self::Float => "float",
4905            Self::Text => "text",
4906            Self::Bool => "bool",
4907            Self::Vector => "vector",
4908            Self::Interval => "interval",
4909            Self::Timestamptz => "timestamptz",
4910            Self::Json => "json",
4911            Self::Jsonb => "jsonb",
4912            Self::RegType => "regtype",
4913            Self::RegClass => "regclass",
4914            Self::Date => "date",
4915            Self::Timestamp => "timestamp",
4916            Self::TextArray => "TEXT[]",
4917            Self::IntArray => "INT[]",
4918            Self::BigIntArray => "BIGINT[]",
4919            Self::TsVector => "tsvector",
4920            Self::TsQuery => "tsquery",
4921            Self::Uuid => "uuid",
4922            Self::Bytea => "bytea",
4923            // v7.37.5 — `Self::Named` carries its own canonical name.
4924            Self::Named(name) => return f.write_str(name),
4925        })
4926    }
4927}
4928
4929#[derive(Debug, Clone, PartialEq)]
4930pub enum Literal {
4931    Integer(i64),
4932    Float(f64),
4933    /// Exact decimal literal — a bare `12.34`-style token, kept as
4934    /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
4935    /// before it becomes a `Value::Numeric`. PG parses such literals as
4936    /// `numeric`, not `double precision`. (Scientific/huge literals stay
4937    /// `Float`.)
4938    Numeric {
4939        unscaled: i128,
4940        /// v7.39 (round 271) — widened to u16. At u8 a literal with more
4941        /// than 255 decimal places could not be represented, and the
4942        /// conversion's `.expect("lexer-validated decimal")` aborted the
4943        /// query with an internal error on SQL PG accepts.
4944        scale: u16,
4945    },
4946    /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
4947    /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
4948    /// `Value::NumericBig` at eval; previously such literals fell back to double.
4949    NumericBig(String),
4950    String(String),
4951    /// v7.38.8 — a temporal constant that has already been decoded.
4952    ///
4953    /// Without these the only way to carry one through the AST was as
4954    /// text, and a predicate comparing a `timestamp` column against a
4955    /// literal then coerced that text back into a timestamp ONCE PER
4956    /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
4957    /// profile. `constfold` produced text for the same reason: its exit
4958    /// had nothing else to hand back.
4959    ///
4960    /// `text` keeps the spelling so `Display` round-trips byte for byte,
4961    /// the way `Interval` already does and for the same reason: this
4962    /// node is printed in EXPLAIN, in dumps and in error messages, and
4963    /// none of those should change because the value stopped being
4964    /// carried as a string. The enum already holds a `String` and an
4965    /// `i128`, so neither variant widens it.
4966    Timestamp {
4967        micros: i64,
4968        text: String,
4969    },
4970    /// Days since the epoch `Value::Date` counts from. See
4971    /// [`Literal::Timestamp`].
4972    Date {
4973        days: i32,
4974        text: String,
4975    },
4976    Bool(bool),
4977    Null,
4978    /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
4979    Vector(Vec<f32>),
4980    /// TEXT[] value carried through the prepared-bind path
4981    /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
4982    /// text form, so the array rides the AST natively).
4983    TextArray(Vec<Option<String>>),
4984    /// INT[] value carried through the prepared-bind path.
4985    IntArray(Vec<Option<i32>>),
4986    /// BIGINT[] value carried through the prepared-bind path.
4987    BigIntArray(Vec<Option<i64>>),
4988    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
4989    /// Three independent dimensions: `months` (variable-length;
4990    /// year/month), `days` (fixed 86400 seconds at non-DST, but
4991    /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
4992    /// stays distinguishable), and `micros` (sub-day; can carry).
4993    /// `text` keeps the original spelling so Display round-trips
4994    /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
4995    Interval {
4996        months: i32,
4997        days: i32,
4998        micros: i64,
4999        text: String,
5000    },
5001}
5002
5003#[derive(Debug, Clone, PartialEq, Eq)]
5004pub struct ColumnName {
5005    pub qualifier: Option<String>,
5006    pub name: String,
5007}
5008
5009#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5010pub enum BinOp {
5011    Or,
5012    And,
5013    Eq,
5014    NotEq,
5015    /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
5016    /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
5017    /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
5018    /// non-NULL behaviour matches `<>` / `=` exactly. Common in
5019    /// PG-style JOIN ON predicates and pg_dump output.
5020    IsDistinctFrom,
5021    IsNotDistinctFrom,
5022    /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
5023    /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
5024    /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
5025    /// is a real division (round 351).
5026    IntDiv,
5027    Lt,
5028    LtEq,
5029    Gt,
5030    GtEq,
5031    Add,
5032    Sub,
5033    Mul,
5034    Div,
5035    /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
5036    /// precedence as Mul/Div; result type follows left operand.
5037    Mod,
5038    /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
5039    /// operands of equal dimension; engine returns `Value::Float(d)`.
5040    L2Distance,
5041    /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
5042    GeomParallel,
5043    /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
5044    OverLeft,
5045    OverRight,
5046    /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
5047    GeomPerp,
5048    /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
5049    GeomSameAs,
5050    /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
5051    /// object to the left-hand one.
5052    ClosestPoint,
5053    /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
5054    GeomHoriz,
5055    /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
5056    /// more similar" remains true (matches pgvector's published convention).
5057    InnerProduct,
5058    /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
5059    CosineDistance,
5060    /// SQL string concatenation `||`. NULL propagates.
5061    Concat,
5062    /// Bitwise OR `|` on integers.
5063    BitOr,
5064    /// Bitwise AND `&` on integers.
5065    BitAnd,
5066    /// Bitwise XOR `#` on integers and equal-length bit strings.
5067    BitXor,
5068    /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
5069    /// sides as truth values and returns their exclusive-or (`1 XOR 0`
5070    /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
5071    /// MySQL dialect produces it; PG has no logical XOR. Its precedence
5072    /// sits between OR (loosest) and AND.
5073    LogicalXor,
5074    /// v4.14 `json -> key` — element access by string key (object)
5075    /// or integer index (array). Returns a JSON value.
5076    JsonGet,
5077    /// v4.14 `json ->> key` — same access, returns the result as
5078    /// TEXT (unwraps a top-level JSON string; renders other scalars
5079    /// as their canonical text).
5080    JsonGetText,
5081    /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
5082    /// text array literal like `'{a,0,b}'`. Returns JSON.
5083    JsonGetPath,
5084    /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
5085    JsonGetPathText,
5086    /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
5087    /// when every key/value in `sub_json` is structurally present in
5088    /// the left side. Matches PG semantics (top-level + recursive).
5089    JsonContains,
5090    /// `@?` — jsonb path existence (jsonb_path_exists).
5091    JsonPathExists,
5092    /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
5093    /// `a <@ b` is defined as `b @> a` (same semantics, swapped
5094    /// sides). Eval dispatch reuses `JsonContains` with swapped args.
5095    JsonContainedBy,
5096    /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
5097    /// returns BOOL. For an object, true if `key` is an existing
5098    /// member name; for an array, true if any element is the string
5099    /// `key` (PG semantics).
5100    JsonKeyExists,
5101    /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
5102    /// returns BOOL.
5103    JsonKeysAny,
5104    /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
5105    /// returns BOOL.
5106    JsonKeysAll,
5107    /// `jsonb #- path_text[]` — delete the value at a nested path.
5108    /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
5109    JsonDeletePath,
5110    /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
5111    /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
5112    /// tsvector` and engine eval normalises either ordering.
5113    TsMatch,
5114    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
5115    /// `<<`. LHS network is strictly inside RHS network (no equality).
5116    InetContainedBy,
5117    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
5118    /// `<<=`. LHS network ⊆ RHS network.
5119    InetContainedByEq,
5120    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
5121    /// LHS network strictly contains RHS network.
5122    InetContains,
5123    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
5124    /// LHS network ⊇ RHS network.
5125    InetContainsEq,
5126    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
5127    /// True iff either network contains any address of the other.
5128    InetOverlap,
5129    /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
5130    /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
5131    Intersects,
5132    /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
5133    /// (point, box).
5134    IsBelow,
5135    IsAbove,
5136    /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
5137    /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
5138    /// where `'A' < 'a'` is false under a non-C collation, which is the
5139    /// whole reason the operator family exists — it is what makes a LIKE
5140    /// prefix index-usable. pg_dump writes these into index definitions.
5141    PatternLt,
5142    PatternLtEq,
5143    PatternGt,
5144    PatternGtEq,
5145}
5146
5147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5148pub enum UnOp {
5149    Not,
5150    Neg,
5151    /// Bitwise NOT `~` on integers.
5152    BitNot,
5153    /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
5154    /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
5155    /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
5156    /// while PG18 and MariaDB accept every one of them.
5157    ///
5158    /// It is not a no-op to drop at parse time — PG refuses it on
5159    /// non-numeric operands ("operator does not exist: + boolean"), so the
5160    /// operand's type has to be seen at eval.
5161    Plus,
5162}
5163
5164// --- Display impls (round-trip-safe) --------------------------------------
5165
5166impl Statement {
5167    /// v7.18 — classify whether the statement is read-only at
5168    /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
5169    /// route SELECT-shaped traffic through the fan-out
5170    /// `AsyncReadHandle` (no writer-lock contention) while
5171    /// keeping DML / DDL / TX-control on the single-writer path.
5172    ///
5173    /// The classification matches what
5174    /// `Engine::execute_readonly_with_cancel` accepts: anything
5175    /// that does NOT mutate catalog, statistics, session state,
5176    /// or transaction state. WaitForWalPosition is included
5177    /// (engine returns `Unsupported`, but the classification is
5178    /// semantically read-only — no mutation). Empty is excluded
5179    /// out of an abundance of caution — the no-op routes
5180    /// through the writer so any future side effect lands
5181    /// uniformly.
5182    ///
5183    /// **Not connection-state aware**. `SET LOCAL` / `RESET`
5184    /// affect session parameters and must run on the writer
5185    /// engine that owns the session state; they classify as
5186    /// writer-path here. Same for `BEGIN` / `COMMIT` /
5187    /// `ROLLBACK` / `SAVEPOINT` — transaction control is
5188    /// always writer-path.
5189    /// v7.39 (round 435) — does this statement implicitly COMMIT an open
5190    /// transaction under MySQL?
5191    ///
5192    /// PG runs DDL inside the transaction; MySQL commits before (and after)
5193    /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
5194    /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
5195    /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
5196    /// nested START TRANSACTION; and measured NOT to fire for `CREATE
5197    /// TEMPORARY TABLE`, `SET`, or a SELECT.
5198    ///
5199    /// A positive list, not "everything that is not DML": a statement
5200    /// wrongly listed here commits a client's data early, which is as bad as
5201    /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
5202    /// COMPACT) are left out — a MySQL session never sends them.
5203    #[must_use]
5204    pub fn mysql_implicit_commit(&self) -> bool {
5205        match self {
5206            // MySQL's documented exception, measured on MariaDB 11: a
5207            // TEMPORARY table is not DDL for this purpose and does not
5208            // commit. (Round 435 got this for free because the parser then
5209            // lowered that spelling to `Statement::Empty`; round 436 made it
5210            // a real CREATE TABLE, and the round-435 pin caught it.)
5211            Self::CreateTable(c) => !c.temporary,
5212            // MySQL commits the open transaction and opens a fresh one.
5213            Self::Begin { .. }
5214            | Self::DropTable { .. }
5215            | Self::DropIndex { .. }
5216            | Self::CreateIndex(_)
5217            | Self::AlterIndex { .. }
5218            | Self::AlterTable(_)
5219            | Self::Truncate { .. }
5220            | Self::Analyze { .. }
5221            | Self::CreateStatistics { .. }
5222            | Self::DropStatistics { .. }
5223            | Self::CreateView { .. }
5224            | Self::DropView { .. }
5225            | Self::CreateMaterializedView { .. }
5226            | Self::RefreshMaterializedView { .. }
5227            | Self::DropMaterializedView { .. }
5228            | Self::CreateSequence(_)
5229            | Self::AlterSequence { .. }
5230            | Self::DropSequence { .. }
5231            | Self::CreateFunction(_)
5232            | Self::DropFunction { .. }
5233            | Self::CreateTrigger(_)
5234            | Self::DropTrigger { .. }
5235            | Self::CreateRule(_)
5236            | Self::DropRule { .. }
5237            | Self::CreateType(_)
5238            | Self::DropType { .. }
5239            | Self::AlterTypeAddValue { .. }
5240            | Self::AlterTypeRenameValue { .. }
5241            | Self::CreateDomain(_)
5242            | Self::AlterDomain { .. }
5243            | Self::DropDomain { .. }
5244            | Self::CreateSchema { .. }
5245            | Self::DropSchema { .. }
5246            | Self::CreateUser { .. }
5247            | Self::DropUser { .. }
5248            | Self::Grant { .. }
5249            | Self::Revoke { .. }
5250            | Self::CreatePolicy(_)
5251            | Self::AlterPolicy(_)
5252            | Self::DropPolicy { .. }
5253            | Self::CommentOn { .. }
5254            | Self::CreateExtension { .. } => true,
5255            _ => false,
5256        }
5257    }
5258
5259    #[must_use]
5260    pub fn is_readonly(&self) -> bool {
5261        match self {
5262            // v7.39 (round 288) — SET CONSTRAINTS changes transaction
5263            // state, and IMMEDIATE can run the deferred checks there and
5264            // then; writer-path.
5265            Statement::SetConstraints { .. } => false,
5266            // v7.39 (round 695) — it writes nothing (SPG has no
5267            // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
5268            // writer and a read-only session refuses it there too.
5269            Statement::AlterSystem { .. } => false,
5270            // Same shape: a no-op here, a writer to PG, so a read-only
5271            // session refuses it as PG's would.
5272            Statement::NoOpPreventedInTransaction { .. } => false,
5273            Statement::DropDatabase { .. } => false,
5274            // v7.39 (round 696) — they perform nothing, so nothing is
5275            // written; PG classes LOCK and the OWNED BY pair as writers and
5276            // a read-only session refuses them there.
5277            Statement::ValidateOnly { .. } => false,
5278            // v7.39 (round 750) — a credential rotation persists.
5279            Statement::AlterRolePassword { .. } => true,
5280            Statement::DropAggregate { .. } => false,
5281            // v7.39 (round 547) — records a GUC default in the catalog.
5282            Statement::SetDbRoleSetting(_) => false,
5283            // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5284            // but they name a relation and PG refuses one that is not
5285            // there, so they are not read-only in the sense this asks.
5286            Statement::Maintain { .. } => false,
5287            // v7.39 (round 277) — the prepared-statement surface is
5288            // session state, like SET; writer-path so it lands on the
5289            // engine that owns the session. EXECUTE may also run a
5290            // write, and its body is only known at execution time.
5291            Statement::Prepare { .. }
5292            | Statement::Execute { .. }
5293            | Statement::Deallocate(_)
5294            | Statement::Call(_)
5295            | Statement::PrepareTransaction(_)
5296            | Statement::CreateStatistics { .. }
5297            | Statement::DropStatistics { .. }
5298            // v7.39 (round 318, V51) — KILL signals another connection;
5299            // it must run on the writer path that owns the registry hook.
5300            | Statement::Kill { .. }
5301            // v7.39 (round 320, V53) — DISCARD throws session state away;
5302            // writer path, like SET / RESET.
5303            | Statement::Discard(_)
5304            // v7.39.2 — `USE <db>` writes session state, the same way
5305            // SET does, and takes the same path.
5306            | Statement::UseDatabase(_) => false,
5307            // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5308            // locks MUTATES the lock table, so it is not a read. Left as
5309            // a read it went to the read-only executor and the locking
5310            // pre-pass never ran at all — the clause was honoured only
5311            // inside an explicit transaction, and silently ignored in
5312            // autocommit, which is where a queue worker runs it.
5313            Statement::Select(s) if s.locking.is_some() => false,
5314            Statement::Select(_)
5315            | Statement::CopyTo { .. }
5316            | Statement::CopyToFile { .. }
5317            | Statement::Explain(_)
5318            | Statement::ShowTables
5319            | Statement::ShowDatabases
5320            | Statement::ShowCreateTable(_)
5321            | Statement::ShowIndexes(_)
5322            | Statement::ShowStatus
5323            | Statement::ShowVariables
5324            | Statement::ShowVariablesLike(_)
5325            | Statement::ShowProcesslist
5326            | Statement::ShowColumns(_)
5327            | Statement::ShowUsers
5328            | Statement::ShowPublications
5329            | Statement::ShowSubscriptions
5330            | Statement::WaitForWalPosition { .. } => true,
5331            // Everything else mutates catalog, statistics,
5332            // session state, or transaction state — writer path.
5333            // Listed explicitly so a new Statement variant fails
5334            // the match exhaustiveness check and forces a
5335            // classification decision at add-site.
5336            Statement::Empty
5337            // v7.39 (round 169) — VACUUM mutates storage (reclaims
5338            // tombstoned versions): writer path.
5339            | Statement::Vacuum { .. }
5340            | Statement::DropTable { .. }
5341            | Statement::DropIndex { .. }
5342            | Statement::CreateTable(_)
5343            | Statement::CreateExtension(_)
5344            | Statement::DoBlock(_)
5345            | Statement::CreateIndex(_)
5346            | Statement::Insert(_)
5347            | Statement::Update(_)
5348            | Statement::Delete(_)
5349            | Statement::Merge(_)
5350            | Statement::Begin(_)
5351            | Statement::Commit
5352            | Statement::Rollback
5353            | Statement::Savepoint(_)
5354            | Statement::RollbackToSavepoint(_)
5355            | Statement::ReleaseSavepoint(_)
5356            | Statement::CreateUser(_)
5357            | Statement::DropUser { .. }
5358            | Statement::SetRole(_)
5359            | Statement::Grant(_)
5360            | Statement::Revoke(_)
5361            | Statement::CreatePolicy(_)
5362            | Statement::AlterPolicy(_)
5363            | Statement::DropPolicy(_)
5364            | Statement::AlterIndex(_)
5365            | Statement::AlterTable(_)
5366            | Statement::CreatePublication(_)
5367            | Statement::DropPublication { .. }
5368            | Statement::CreateSubscription(_)
5369            | Statement::DropSubscription { .. }
5370            | Statement::Analyze(_)
5371            | Statement::Truncate { .. }
5372            | Statement::CompactColdSegments
5373            | Statement::SetParameter { .. }
5374            | Statement::SetParameterList(_)
5375            | Statement::SetUserVars(..)
5376            | Statement::SetTransaction { .. }
5377            | Statement::ShowParameter(_)
5378            | Statement::ResetParameter(_)
5379            | Statement::CreateFunction(_)
5380            | Statement::CreateTrigger(_)
5381            | Statement::DropTrigger { .. }
5382            | Statement::CreateRule(_)
5383            | Statement::DropRule { .. }
5384            | Statement::DropFunction { .. }
5385            | Statement::CreateSequence(_)
5386            | Statement::AlterSequence(_)
5387            | Statement::DropSequence { .. }
5388            | Statement::CreateView(_)
5389            | Statement::DropView { .. }
5390            | Statement::CreateMaterializedView(_)
5391            | Statement::RefreshMaterializedView { .. }
5392            | Statement::DropMaterializedView { .. }
5393            | Statement::CreateType(_)
5394            | Statement::AlterTypeAddValue { .. }
5395            | Statement::AlterTypeRenameValue { .. }
5396            | Statement::CommentOn { .. }
5397            | Statement::DropType { .. }
5398            | Statement::CreateDomain(_)
5399            | Statement::DropDomain { .. }
5400            | Statement::CreateSchema { .. }
5401            | Statement::DropSchema { .. }
5402            // v7.39 (round 218) — cursors mutate per-session cursor state
5403            // (open/position/close) on the writer engine: writer path.
5404            | Statement::DeclareCursor { .. }
5405            | Statement::FetchCursor { .. }
5406            | Statement::MoveCursor { .. }
5407            | Statement::CloseCursor { .. }
5408            // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5409            // state / the notification queue: writer path.
5410            | Statement::Listen(_)
5411            | Statement::Notify { .. }
5412            | Statement::Unlisten(_)
5413            | Statement::CopyFromFile { .. }
5414            | Statement::AlterDomain { .. } => false,
5415        }
5416    }
5417}
5418
5419/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5420/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5421#[derive(Debug, Clone, PartialEq, Eq)]
5422pub struct GrantStatement {
5423    /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5424    /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5425    /// is why they keep the case the user typed.
5426    pub privileges: Vec<GrantPriv>,
5427    /// What the privileges are on.
5428    pub object: GrantObject,
5429    /// The roles granted to / revoked from. An empty string entry = PUBLIC.
5430    pub grantees: Vec<String>,
5431    /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
5432    /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
5433    /// privilege itself).
5434    pub grant_option: bool,
5435}
5436
5437/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
5438/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
5439/// An empty column list means the privilege is table-wide.
5440#[derive(Debug, Clone, PartialEq, Eq)]
5441pub struct GrantPriv {
5442    pub word: String,
5443    pub columns: Vec<String>,
5444}
5445
5446/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
5447/// privileges; every other object class parses and is accepted as a no-op, so
5448/// a pg_dump that grants on schemas / sequences / functions still restores.
5449#[derive(Debug, Clone, PartialEq, Eq)]
5450pub enum GrantObject {
5451    /// `ON [TABLE] a, b` — the enforced case.
5452    Tables(Vec<String>),
5453    /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
5454    /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
5455    /// granted roles; the grantees are the members.
5456    Roles(Vec<String>),
5457    /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
5458    /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
5459    Sequences(Vec<String>),
5460    /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
5461    Schemas(Vec<String>),
5462    /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
5463    Databases(Vec<String>),
5464    /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
5465    /// (SPG keys functions by name); the argument list parses and is dropped.
5466    Functions(Vec<(String, Option<Vec<String>>)>),
5467    /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
5468    /// every table at GRANT time, exactly like PG.
5469    AllTablesInSchema,
5470    /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
5471    /// message.
5472    Other(String),
5473}
5474
5475impl GrantStatement {
5476    /// Round-trip text. `grant = false` renders the REVOKE form.
5477    fn render(&self, grant: bool) -> alloc::string::String {
5478        use core::fmt::Write as _;
5479        let mut s = alloc::string::String::new();
5480        let privs = if self.privileges.is_empty() {
5481            alloc::string::String::from("ALL")
5482        } else {
5483            let parts: Vec<_> = self
5484                .privileges
5485                .iter()
5486                .map(|p| {
5487                    if p.columns.is_empty() {
5488                        p.word.clone()
5489                    } else {
5490                        let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
5491                        alloc::format!("{} ({})", p.word, cols.join(", "))
5492                    }
5493                })
5494                .collect();
5495            parts.join(", ")
5496        };
5497        let obj = match &self.object {
5498            GrantObject::Tables(t) => {
5499                let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
5500                alloc::format!("TABLE {}", names.join(", "))
5501            }
5502            GrantObject::Roles(r) => {
5503                let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
5504                names.join(", ")
5505            }
5506            GrantObject::Sequences(n) => {
5507                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5508                alloc::format!("SEQUENCE {}", names.join(", "))
5509            }
5510            GrantObject::Schemas(n) => {
5511                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5512                alloc::format!("SCHEMA {}", names.join(", "))
5513            }
5514            GrantObject::Databases(n) => {
5515                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5516                alloc::format!("DATABASE {}", names.join(", "))
5517            }
5518            GrantObject::Functions(n) => {
5519                let names: Vec<_> = n
5520                    .iter()
5521                    .map(|(name, args)| match args {
5522                        Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
5523                        None => quote_ident(name),
5524                    })
5525                    .collect();
5526                alloc::format!("FUNCTION {}", names.join(", "))
5527            }
5528            GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
5529            GrantObject::Other(k) => k.clone(),
5530        };
5531        let who: Vec<_> = self
5532            .grantees
5533            .iter()
5534            .map(|g| {
5535                if g.is_empty() {
5536                    "PUBLIC".into()
5537                } else {
5538                    quote_ident(g)
5539                }
5540            })
5541            .collect();
5542        if let GrantObject::Roles(_) = &self.object {
5543            let _ = if grant {
5544                write!(s, "GRANT {obj} TO {}", who.join(", "))
5545            } else {
5546                write!(s, "REVOKE {obj} FROM {}", who.join(", "))
5547            };
5548            return s;
5549        }
5550        if grant {
5551            let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
5552            if self.grant_option {
5553                s.push_str(" WITH GRANT OPTION");
5554            }
5555        } else {
5556            s.push_str("REVOKE ");
5557            if self.grant_option {
5558                s.push_str("GRANT OPTION FOR ");
5559            }
5560            let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
5561        }
5562        s
5563    }
5564}
5565
5566impl fmt::Display for Statement {
5567    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5568        match self {
5569            Self::Empty => Ok(()),
5570            // v7.39 (round 695) — deparsed the way PG writes it.
5571            // v7.39 (round 696) — never deparsed into a dump (nothing is
5572            // stored), so the shortest faithful spelling of what it was.
5573            Self::DropAggregate { if_exists, items } => {
5574                f.write_str("DROP AGGREGATE ")?;
5575                if *if_exists {
5576                    f.write_str("IF EXISTS ")?;
5577                }
5578                for (i, (name, args)) in items.iter().enumerate() {
5579                    if i > 0 {
5580                        f.write_str(", ")?;
5581                    }
5582                    match args {
5583                        Some(a) => write!(f, "{name}({})", a.join(", "))?,
5584                        None => write!(f, "{name}(*)")?,
5585                    }
5586                }
5587                Ok(())
5588            }
5589            Self::AlterRolePassword { name, password } => {
5590                write!(f, "ALTER ROLE {}", quote_ident(name))?;
5591                match password {
5592                    Some(_) => f.write_str(" PASSWORD '<redacted>'"),
5593                    None => f.write_str(" PASSWORD NULL"),
5594                }
5595            }
5596            Self::ValidateOnly { kind, names } => match kind {
5597                ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
5598                ValidateOnlyKind::RoleName => {
5599                    write!(f, "DROP OWNED BY {}", names.join(", "))
5600                }
5601                ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
5602                ValidateOnlyKind::ExtensionAvailable => {
5603                    write!(f, "CREATE EXTENSION {}", names.join(", "))
5604                }
5605                ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
5606                ValidateOnlyKind::CollationName => {
5607                    write!(f, "DROP COLLATION {}", names.join(", "))
5608                }
5609                ValidateOnlyKind::TsConfigName => {
5610                    write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
5611                }
5612                ValidateOnlyKind::EventTriggerName => {
5613                    write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
5614                }
5615                ValidateOnlyKind::TablespaceName => {
5616                    write!(f, "DROP TABLESPACE {}", names.join(", "))
5617                }
5618                ValidateOnlyKind::LargeObjectOid => {
5619                    write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
5620                }
5621                ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
5622                ValidateOnlyKind::AggregateName => {
5623                    write!(f, "ALTER AGGREGATE {}", names.join(", "))
5624                }
5625                ValidateOnlyKind::ConversionName => {
5626                    write!(f, "DROP CONVERSION {}", names.join(", "))
5627                }
5628                ValidateOnlyKind::LanguageName => {
5629                    write!(f, "DROP LANGUAGE {}", names.join(", "))
5630                }
5631                ValidateOnlyKind::ExtensionInstalled => {
5632                    write!(f, "DROP EXTENSION {}", names.join(", "))
5633                }
5634            },
5635            Self::AlterSystem { parameter } => match parameter {
5636                Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
5637                None => f.write_str("ALTER SYSTEM RESET ALL"),
5638            },
5639            // v7.39 (round 547) — round-trips as PG writes it.
5640            Self::SetDbRoleSetting(st) => {
5641                match (&st.database, &st.role) {
5642                    (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
5643                    (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
5644                    (None, None) => f.write_str("ALTER ROLE ALL")?,
5645                }
5646                if let (Some(d), Some(_)) = (&st.database, &st.role) {
5647                    write!(f, " IN DATABASE {d}")?;
5648                }
5649                match (&st.param, &st.value) {
5650                    (None, _) => f.write_str(" RESET ALL"),
5651                    (Some(p), None) => write!(f, " RESET {p}"),
5652                    (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
5653                }
5654            }
5655            Self::Maintain {
5656                kind,
5657                concurrently,
5658                target,
5659            } => {
5660                f.write_str(match kind {
5661                    crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
5662                    _ => "REINDEX ",
5663                })?;
5664                if *concurrently {
5665                    f.write_str("CONCURRENTLY ")?;
5666                }
5667                if let Some(t) = target {
5668                    f.write_str(t)?;
5669                }
5670                Ok(())
5671            }
5672            Self::DropDatabase { name, if_exists } => {
5673                f.write_str("DROP DATABASE ")?;
5674                if *if_exists {
5675                    f.write_str("IF EXISTS ")?;
5676                }
5677                f.write_str(name)
5678            }
5679            Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
5680            Self::SetConstraints { names, deferred } => {
5681                f.write_str("SET CONSTRAINTS ")?;
5682                if names.is_empty() {
5683                    f.write_str("ALL")?;
5684                } else {
5685                    for (i, n) in names.iter().enumerate() {
5686                        if i > 0 {
5687                            f.write_str(", ")?;
5688                        }
5689                        f.write_str(n)?;
5690                    }
5691                }
5692                f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
5693            }
5694            // v7.39 (round 277) — the source text is kept verbatim so
5695            // `pg_prepared_statements.statement` can report it the way
5696            // PG does (the whole PREPARE statement, not just the body).
5697            Self::Prepare { source, .. } => f.write_str(source),
5698            Self::Execute { name, args } => {
5699                write!(f, "EXECUTE {}", quote_ident(name))?;
5700                if !args.is_empty() {
5701                    f.write_str("(")?;
5702                    for (i, a) in args.iter().enumerate() {
5703                        if i > 0 {
5704                            f.write_str(", ")?;
5705                        }
5706                        write!(f, "{a}")?;
5707                    }
5708                    f.write_str(")")?;
5709                }
5710                Ok(())
5711            }
5712            Self::CreateStatistics {
5713                name,
5714                if_not_exists,
5715                kinds,
5716                columns,
5717                table,
5718            } => {
5719                f.write_str("CREATE STATISTICS ")?;
5720                if *if_not_exists {
5721                    f.write_str("IF NOT EXISTS ")?;
5722                }
5723                write!(f, "{}", quote_ident(name))?;
5724                if !kinds.is_empty() {
5725                    write!(f, " ({})", kinds.join(", "))?;
5726                }
5727                write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
5728            }
5729            Self::DropStatistics { name, if_exists } => {
5730                f.write_str("DROP STATISTICS ")?;
5731                if *if_exists {
5732                    f.write_str("IF EXISTS ")?;
5733                }
5734                write!(f, "{}", quote_ident(name))
5735            }
5736            Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
5737            Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
5738            Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
5739            Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
5740            Self::DeclareCursor {
5741                name,
5742                scroll,
5743                hold,
5744                query,
5745            } => {
5746                write!(f, "DECLARE {} ", quote_ident(name))?;
5747                match scroll {
5748                    Some(true) => f.write_str("SCROLL ")?,
5749                    Some(false) => f.write_str("NO SCROLL ")?,
5750                    None => {}
5751                }
5752                f.write_str("CURSOR ")?;
5753                if *hold {
5754                    f.write_str("WITH HOLD ")?;
5755                }
5756                write!(f, "FOR {query}")
5757            }
5758            Self::FetchCursor { name, direction } => {
5759                write!(f, "FETCH {direction} FROM {}", quote_ident(name))
5760            }
5761            Self::MoveCursor { name, direction } => {
5762                write!(f, "MOVE {direction} FROM {}", quote_ident(name))
5763            }
5764            Self::CloseCursor { name } => match name {
5765                Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
5766                None => f.write_str("CLOSE ALL"),
5767            },
5768            Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
5769            Self::Notify { channel, payload } => {
5770                write!(f, "NOTIFY {}", quote_ident(channel))?;
5771                if let Some(p) = payload {
5772                    write!(f, ", '{}'", p.replace('\'', "''"))?;
5773                }
5774                Ok(())
5775            }
5776            Self::Unlisten(ch) => match ch {
5777                Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
5778                None => f.write_str("UNLISTEN *"),
5779            },
5780            Self::CopyTo {
5781                table,
5782                columns,
5783                query,
5784                options,
5785            } => {
5786                if let Some(q) = query {
5787                    write!(f, "COPY ({q})")?;
5788                } else {
5789                    write!(f, "COPY {table}")?;
5790                    if let Some(cols) = columns {
5791                        write!(f, " ({})", cols.join(", "))?;
5792                    }
5793                }
5794                write!(f, " TO STDOUT")?;
5795                let mut parts: Vec<String> = Vec::new();
5796                if options.format == CopyFormat::Csv {
5797                    parts.push("FORMAT csv".to_string());
5798                }
5799                if options.header {
5800                    parts.push("HEADER true".to_string());
5801                }
5802                if let Some(d) = options.delimiter {
5803                    parts.push(alloc::format!("DELIMITER '{d}'"));
5804                }
5805                if let Some(n) = &options.null_str {
5806                    parts.push(alloc::format!("NULL '{n}'"));
5807                }
5808                if let Some(q) = options.quote {
5809                    parts.push(alloc::format!("QUOTE '{q}'"));
5810                }
5811                if !parts.is_empty() {
5812                    write!(f, " WITH ({})", parts.join(", "))?;
5813                }
5814                Ok(())
5815            }
5816            Self::CopyFromFile {
5817                table,
5818                columns,
5819                path,
5820                options,
5821            } => {
5822                write!(f, "COPY {table}")?;
5823                if let Some(cols) = columns {
5824                    write!(f, " ({})", cols.join(", "))?;
5825                }
5826                write!(f, " FROM '{path}'")?;
5827                let mut parts: Vec<String> = Vec::new();
5828                if options.format == CopyFormat::Csv {
5829                    parts.push("FORMAT csv".to_string());
5830                }
5831                if options.header {
5832                    parts.push("HEADER true".to_string());
5833                }
5834                if let Some(d) = options.delimiter {
5835                    parts.push(alloc::format!("DELIMITER '{d}'"));
5836                }
5837                if let Some(n) = &options.null_str {
5838                    parts.push(alloc::format!("NULL '{n}'"));
5839                }
5840                if let Some(q) = options.quote {
5841                    parts.push(alloc::format!("QUOTE '{q}'"));
5842                }
5843                if !parts.is_empty() {
5844                    write!(f, " WITH ({})", parts.join(", "))?;
5845                }
5846                Ok(())
5847            }
5848            Self::CopyToFile {
5849                table,
5850                columns,
5851                query,
5852                path,
5853                options,
5854            } => {
5855                if let Some(q) = query {
5856                    write!(f, "COPY ({q})")?;
5857                } else {
5858                    write!(f, "COPY {table}")?;
5859                    if let Some(cols) = columns {
5860                        write!(f, " ({})", cols.join(", "))?;
5861                    }
5862                }
5863                write!(f, " TO '{path}'")?;
5864                let mut parts: Vec<String> = Vec::new();
5865                if options.format == CopyFormat::Csv {
5866                    parts.push("FORMAT csv".to_string());
5867                }
5868                if options.header {
5869                    parts.push("HEADER true".to_string());
5870                }
5871                if let Some(d) = options.delimiter {
5872                    parts.push(alloc::format!("DELIMITER '{d}'"));
5873                }
5874                if let Some(n) = &options.null_str {
5875                    parts.push(alloc::format!("NULL '{n}'"));
5876                }
5877                if let Some(q) = options.quote {
5878                    parts.push(alloc::format!("QUOTE '{q}'"));
5879                }
5880                if !parts.is_empty() {
5881                    write!(f, " WITH ({})", parts.join(", "))?;
5882                }
5883                Ok(())
5884            }
5885            Self::AlterDomain { name, action } => {
5886                write!(f, "ALTER DOMAIN {name} ")?;
5887                match action {
5888                    AlterDomainAction::AddConstraint { name: cn, check } => match cn {
5889                        Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
5890                        None => write!(f, "ADD CHECK ({check})"),
5891                    },
5892                    AlterDomainAction::DropConstraint {
5893                        name: cn,
5894                        if_exists,
5895                    } => {
5896                        if *if_exists {
5897                            write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
5898                        } else {
5899                            write!(f, "DROP CONSTRAINT {cn}")
5900                        }
5901                    }
5902                    AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
5903                    AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
5904                    AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
5905                    AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
5906                    AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
5907                }
5908            }
5909            Self::Truncate {
5910                tables,
5911                restart_identity,
5912                cascade,
5913                only,
5914            } => {
5915                f.write_str("TRUNCATE TABLE ")?;
5916                if *only {
5917                    f.write_str("ONLY ")?;
5918                }
5919                for (i, t) in tables.iter().enumerate() {
5920                    if i > 0 {
5921                        f.write_str(", ")?;
5922                    }
5923                    f.write_str(t)?;
5924                }
5925                if *restart_identity {
5926                    f.write_str(" RESTART IDENTITY")?;
5927                }
5928                if *cascade {
5929                    f.write_str(" CASCADE")?;
5930                }
5931                Ok(())
5932            }
5933            Self::DropTable { names, if_exists } => {
5934                f.write_str("DROP TABLE ")?;
5935                if *if_exists {
5936                    f.write_str("IF EXISTS ")?;
5937                }
5938                for (i, n) in names.iter().enumerate() {
5939                    if i > 0 {
5940                        f.write_str(", ")?;
5941                    }
5942                    write!(f, "{}", quote_ident(n))?;
5943                }
5944                Ok(())
5945            }
5946            Self::DropIndex { name, if_exists } => {
5947                f.write_str("DROP INDEX ")?;
5948                if *if_exists {
5949                    f.write_str("IF EXISTS ")?;
5950                }
5951                write!(f, "{}", quote_ident(name))
5952            }
5953            Self::Select(s) => s.fmt(f),
5954            Self::CreateTable(s) => s.fmt(f),
5955            Self::CreateIndex(s) => s.fmt(f),
5956            Self::Insert(s) => s.fmt(f),
5957            Self::Update(s) => s.fmt(f),
5958            Self::Delete(s) => s.fmt(f),
5959            Self::Merge(s) => s.fmt(f),
5960            Self::Vacuum { table, analyze } => {
5961                f.write_str("VACUUM")?;
5962                if *analyze {
5963                    f.write_str(" ANALYZE")?;
5964                }
5965                if let Some(t) = table {
5966                    write!(f, " {}", quote_ident(t))?;
5967                }
5968                Ok(())
5969            }
5970            Self::Begin(modes) => {
5971                f.write_str("BEGIN")?;
5972                if let Some(level) = modes.isolation {
5973                    write!(f, " ISOLATION LEVEL {level}")?;
5974                }
5975                match modes.read_only {
5976                    Some(true) => f.write_str(" READ ONLY")?,
5977                    Some(false) => f.write_str(" READ WRITE")?,
5978                    None => {}
5979                }
5980                Ok(())
5981            }
5982            Self::Commit => f.write_str("COMMIT"),
5983            Self::Rollback => f.write_str("ROLLBACK"),
5984            Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
5985            Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
5986            Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
5987            Self::ShowTables => f.write_str("SHOW TABLES"),
5988            Self::ShowDatabases => f.write_str("SHOW DATABASES"),
5989            Self::UseDatabase(n) => write!(f, "USE {}", quote_ident(n)),
5990            Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
5991            Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
5992            Self::ShowStatus => f.write_str("SHOW STATUS"),
5993            Self::ShowVariables => f.write_str("SHOW VARIABLES"),
5994            Self::ShowVariablesLike(p) => {
5995                write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
5996            }
5997            Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
5998            Self::Discard(t) => write!(f, "DISCARD {t}"),
5999            Self::Kill { query_only, id } => {
6000                if *query_only {
6001                    write!(f, "KILL QUERY {id}")
6002                } else {
6003                    write!(f, "KILL CONNECTION {id}")
6004                }
6005            }
6006            Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
6007            Self::CreateUser(s) => write!(
6008                f,
6009                "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
6010                quote_ident(&s.name),
6011                s.role
6012            ),
6013            Self::DropUser { name, if_exists } => {
6014                let ie = if *if_exists { "IF EXISTS " } else { "" };
6015                write!(f, "DROP USER {ie}{}", quote_ident(name))
6016            }
6017            Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
6018            Self::SetRole(None) => f.write_str("RESET ROLE"),
6019            Self::Grant(g) => write!(f, "{}", g.render(true)),
6020            Self::Revoke(g) => write!(f, "{}", g.render(false)),
6021            Self::CreatePolicy(s) => {
6022                write!(
6023                    f,
6024                    "CREATE POLICY {} ON {}",
6025                    quote_ident(&s.name),
6026                    quote_ident(&s.table)
6027                )?;
6028                if !s.permissive {
6029                    f.write_str(" AS RESTRICTIVE")?;
6030                }
6031                if !matches!(s.cmd, PolicyCmd::All) {
6032                    let w = match s.cmd {
6033                        PolicyCmd::Select => "SELECT",
6034                        PolicyCmd::Insert => "INSERT",
6035                        PolicyCmd::Update => "UPDATE",
6036                        PolicyCmd::Delete => "DELETE",
6037                        PolicyCmd::All => unreachable!(),
6038                    };
6039                    write!(f, " FOR {w}")?;
6040                }
6041                if !s.roles.is_empty() {
6042                    write!(f, " TO {}", s.roles.join(", "))?;
6043                }
6044                if let Some(u) = &s.using {
6045                    write!(f, " USING ({u})")?;
6046                }
6047                if let Some(c) = &s.with_check {
6048                    write!(f, " WITH CHECK ({c})")?;
6049                }
6050                Ok(())
6051            }
6052            Self::AlterPolicy(s) => {
6053                write!(
6054                    f,
6055                    "ALTER POLICY {} ON {}",
6056                    quote_ident(&s.name),
6057                    quote_ident(&s.table)
6058                )?;
6059                if let Some(nn) = &s.rename_to {
6060                    return write!(f, " RENAME TO {}", quote_ident(nn));
6061                }
6062                if let Some(roles) = &s.roles {
6063                    write!(f, " TO {}", roles.join(", "))?;
6064                }
6065                if let Some(u) = &s.using {
6066                    write!(f, " USING ({u})")?;
6067                }
6068                if let Some(c) = &s.with_check {
6069                    write!(f, " WITH CHECK ({c})")?;
6070                }
6071                Ok(())
6072            }
6073            Self::DropPolicy(s) => {
6074                f.write_str("DROP POLICY ")?;
6075                if s.if_exists {
6076                    f.write_str("IF EXISTS ")?;
6077                }
6078                write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
6079            }
6080            Self::ShowUsers => f.write_str("SHOW USERS"),
6081            Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
6082            Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
6083            Self::CreateSubscription(s) => {
6084                write!(
6085                    f,
6086                    "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
6087                    quote_ident(&s.name),
6088                    s.conn_str.replace('\'', "''")
6089                )?;
6090                for (i, p) in s.publications.iter().enumerate() {
6091                    if i > 0 {
6092                        f.write_str(", ")?;
6093                    }
6094                    write!(f, "{}", quote_ident(p))?;
6095                }
6096                Ok(())
6097            }
6098            Self::DropSubscription { name, if_exists } => {
6099                let opt = if *if_exists { "IF EXISTS " } else { "" };
6100                write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
6101            }
6102            Self::WaitForWalPosition { pos, timeout_ms } => {
6103                write!(f, "WAIT FOR WAL POSITION {pos}")?;
6104                if let Some(ms) = timeout_ms {
6105                    write!(f, " WITH TIMEOUT {ms}")?;
6106                }
6107                Ok(())
6108            }
6109            Self::Analyze(None) => f.write_str("ANALYZE"),
6110            Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
6111            Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
6112            Self::Explain(e) => {
6113                if e.suggest {
6114                    write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
6115                } else if e.analyze {
6116                    write!(f, "EXPLAIN ANALYZE {}", e.inner)
6117                } else {
6118                    write!(f, "EXPLAIN {}", e.inner)
6119                }
6120            }
6121            Self::AlterIndex(a) => {
6122                write!(f, "ALTER INDEX ")?;
6123                match &a.target {
6124                    // Parameters are consumed, not stored; the shortest
6125                    // faithful spelling.
6126                    AlterIndexTarget::StorageParams => {
6127                        write!(f, "{} SET ()", quote_ident(&a.name))
6128                    }
6129                    AlterIndexTarget::Rebuild { encoding } => {
6130                        write!(f, "{} REBUILD", quote_ident(&a.name))?;
6131                        if let Some(enc) = encoding {
6132                            write!(f, " WITH (encoding = {enc})")?;
6133                        }
6134                        Ok(())
6135                    }
6136                    AlterIndexTarget::Rename { new, if_exists } => {
6137                        if *if_exists {
6138                            f.write_str("IF EXISTS ")?;
6139                        }
6140                        write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
6141                    }
6142                }
6143            }
6144            Self::AlterTable(a) => {
6145                write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
6146                for (i, t) in a.targets.iter().enumerate() {
6147                    if i > 0 {
6148                        f.write_str(", ")?;
6149                    }
6150                    fmt_alter_target(f, t)?;
6151                }
6152                Ok(())
6153            }
6154            Self::CreatePublication(p) => {
6155                write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
6156                match &p.scope {
6157                    PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
6158                    PublicationScope::ForTables(ts) => {
6159                        f.write_str(" FOR TABLE ")?;
6160                        for (i, t) in ts.iter().enumerate() {
6161                            if i > 0 {
6162                                f.write_str(", ")?;
6163                            }
6164                            write!(f, "{}", quote_ident(t))?;
6165                        }
6166                        Ok(())
6167                    }
6168                    PublicationScope::TablesInSchema(schema) => {
6169                        write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
6170                        Ok(())
6171                    }
6172                    PublicationScope::AllTablesExcept(ts) => {
6173                        f.write_str(" FOR ALL TABLES EXCEPT ")?;
6174                        for (i, t) in ts.iter().enumerate() {
6175                            if i > 0 {
6176                                f.write_str(", ")?;
6177                            }
6178                            write!(f, "{}", quote_ident(t))?;
6179                        }
6180                        Ok(())
6181                    }
6182                }
6183            }
6184            Self::CreateExtension(name) => {
6185                write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
6186            }
6187            Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
6188            Self::DropPublication { name, if_exists } => {
6189                let opt = if *if_exists { "IF EXISTS " } else { "" };
6190                write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
6191            }
6192            Self::SetParameter { name, value, local } => {
6193                write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
6194                match value {
6195                    SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
6196                    SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
6197                    SetValue::Default => f.write_str("DEFAULT"),
6198                }
6199            }
6200            Self::SetTransaction { modes } => {
6201                f.write_str("SET TRANSACTION")?;
6202                if let Some(isolation) = modes.isolation {
6203                    let name = match isolation {
6204                        IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
6205                        IsolationLevel::ReadCommitted => "READ COMMITTED",
6206                        IsolationLevel::RepeatableRead => "REPEATABLE READ",
6207                        IsolationLevel::Serializable => "SERIALIZABLE",
6208                    };
6209                    write!(f, " ISOLATION LEVEL {name}")?;
6210                }
6211                match modes.read_only {
6212                    Some(true) => f.write_str(" READ ONLY")?,
6213                    Some(false) => f.write_str(" READ WRITE")?,
6214                    None => {}
6215                }
6216                Ok(())
6217            }
6218            Self::ShowParameter(name) => write!(f, "SHOW {name}"),
6219            Self::SetUserVars(assigns, _) => {
6220                f.write_str("SET ")?;
6221                for (i, (name, value)) in assigns.iter().enumerate() {
6222                    if i > 0 {
6223                        f.write_str(", ")?;
6224                    }
6225                    write!(f, "@{name} = {value}")?;
6226                }
6227                Ok(())
6228            }
6229            Self::SetParameterList(pairs) => {
6230                f.write_str("SET ")?;
6231                for (i, (name, value)) in pairs.iter().enumerate() {
6232                    if i > 0 {
6233                        f.write_str(", ")?;
6234                    }
6235                    write!(f, "{name} = ")?;
6236                    match value {
6237                        SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
6238                        SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
6239                        SetValue::Default => f.write_str("DEFAULT")?,
6240                    }
6241                }
6242                Ok(())
6243            }
6244            Self::ResetParameter(None) => f.write_str("RESET ALL"),
6245            Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
6246            Self::CreateFunction(s) => s.fmt(f),
6247            Self::CreateTrigger(s) => s.fmt(f),
6248            Self::DropTrigger {
6249                name,
6250                table,
6251                if_exists,
6252            } => {
6253                f.write_str("DROP TRIGGER ")?;
6254                if *if_exists {
6255                    f.write_str("IF EXISTS ")?;
6256                }
6257                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6258            }
6259            Self::DropFunction {
6260                name,
6261                args,
6262                if_exists,
6263            } => {
6264                f.write_str("DROP FUNCTION ")?;
6265                if *if_exists {
6266                    f.write_str("IF EXISTS ")?;
6267                }
6268                write!(f, "{}", quote_ident(name))?;
6269                if let Some(a) = args {
6270                    write!(f, "({})", a.join(", "))?;
6271                }
6272                Ok(())
6273            }
6274            Self::CreateSequence(s) => s.fmt(f),
6275            Self::AlterSequence(s) => s.fmt(f),
6276            Self::DropSequence { names, if_exists } => {
6277                f.write_str("DROP SEQUENCE ")?;
6278                if *if_exists {
6279                    f.write_str("IF EXISTS ")?;
6280                }
6281                for (i, n) in names.iter().enumerate() {
6282                    if i > 0 {
6283                        f.write_str(", ")?;
6284                    }
6285                    write!(f, "{}", quote_ident(n))?;
6286                }
6287                Ok(())
6288            }
6289            Self::CreateView(v) => v.fmt(f),
6290            Self::DropView { names, if_exists } => {
6291                f.write_str("DROP VIEW ")?;
6292                if *if_exists {
6293                    f.write_str("IF EXISTS ")?;
6294                }
6295                for (i, n) in names.iter().enumerate() {
6296                    if i > 0 {
6297                        f.write_str(", ")?;
6298                    }
6299                    write!(f, "{}", quote_ident(n))?;
6300                }
6301                Ok(())
6302            }
6303            Self::CreateMaterializedView(v) => v.fmt(f),
6304            Self::RefreshMaterializedView { name, with_data } => {
6305                write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6306                if !*with_data {
6307                    f.write_str(" WITH NO DATA")?;
6308                }
6309                Ok(())
6310            }
6311            Self::DropMaterializedView { names, if_exists } => {
6312                f.write_str("DROP MATERIALIZED VIEW ")?;
6313                if *if_exists {
6314                    f.write_str("IF EXISTS ")?;
6315                }
6316                for (i, n) in names.iter().enumerate() {
6317                    if i > 0 {
6318                        f.write_str(", ")?;
6319                    }
6320                    write!(f, "{}", quote_ident(n))?;
6321                }
6322                Ok(())
6323            }
6324            Self::CreateType(t) => t.fmt(f),
6325            Self::CommentOn {
6326                kind,
6327                name,
6328                comment,
6329            } => {
6330                let body = match comment {
6331                    Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6332                    None => "NULL".into(),
6333                };
6334                write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6335            }
6336            Self::AlterTypeRenameValue {
6337                type_name,
6338                old,
6339                new,
6340            } => write!(
6341                f,
6342                "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6343                quote_ident(type_name),
6344                old.replace('\'', "''"),
6345                new.replace('\'', "''")
6346            ),
6347            Self::AlterTypeAddValue {
6348                type_name,
6349                label,
6350                if_not_exists,
6351                position,
6352            } => {
6353                write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6354                if *if_not_exists {
6355                    write!(f, "IF NOT EXISTS ")?;
6356                }
6357                write!(f, "'{label}'")?;
6358                if let Some((is_before, anchor)) = position {
6359                    write!(
6360                        f,
6361                        " {} '{anchor}'",
6362                        if *is_before { "BEFORE" } else { "AFTER" }
6363                    )?;
6364                }
6365                Ok(())
6366            }
6367            Self::DropType { names, if_exists } => {
6368                f.write_str("DROP TYPE ")?;
6369                if *if_exists {
6370                    f.write_str("IF EXISTS ")?;
6371                }
6372                for (i, n) in names.iter().enumerate() {
6373                    if i > 0 {
6374                        f.write_str(", ")?;
6375                    }
6376                    write!(f, "{}", quote_ident(n))?;
6377                }
6378                Ok(())
6379            }
6380            Self::CreateDomain(d) => d.fmt(f),
6381            Self::DropDomain { names, if_exists } => {
6382                f.write_str("DROP DOMAIN ")?;
6383                if *if_exists {
6384                    f.write_str("IF EXISTS ")?;
6385                }
6386                for (i, n) in names.iter().enumerate() {
6387                    if i > 0 {
6388                        f.write_str(", ")?;
6389                    }
6390                    write!(f, "{}", quote_ident(n))?;
6391                }
6392                Ok(())
6393            }
6394            Self::CreateSchema {
6395                name,
6396                if_not_exists,
6397            } => {
6398                f.write_str("CREATE SCHEMA ")?;
6399                if *if_not_exists {
6400                    f.write_str("IF NOT EXISTS ")?;
6401                }
6402                write!(f, "{}", quote_ident(name))
6403            }
6404            Self::DropSchema { names, if_exists } => {
6405                f.write_str("DROP SCHEMA ")?;
6406                if *if_exists {
6407                    f.write_str("IF EXISTS ")?;
6408                }
6409                for (i, n) in names.iter().enumerate() {
6410                    if i > 0 {
6411                        f.write_str(", ")?;
6412                    }
6413                    write!(f, "{}", quote_ident(n))?;
6414                }
6415                Ok(())
6416            }
6417            Self::CreateRule(r) => {
6418                f.write_str("CREATE ")?;
6419                if r.or_replace {
6420                    f.write_str("OR REPLACE ")?;
6421                }
6422                write!(
6423                    f,
6424                    "RULE {} AS ON {} TO {}",
6425                    quote_ident(&r.name),
6426                    r.event,
6427                    quote_ident(&r.table)
6428                )?;
6429                if let Some(w) = &r.when_condition {
6430                    write!(f, " WHERE {w}")?;
6431                }
6432                f.write_str(if r.instead {
6433                    " DO INSTEAD "
6434                } else {
6435                    " DO ALSO "
6436                })?;
6437                if r.commands.is_empty() {
6438                    f.write_str("NOTHING")?;
6439                } else if r.commands.len() == 1 {
6440                    write!(f, "{}", r.commands[0])?;
6441                } else {
6442                    f.write_str("(")?;
6443                    for (i, c) in r.commands.iter().enumerate() {
6444                        if i > 0 {
6445                            f.write_str("; ")?;
6446                        }
6447                        write!(f, "{c}")?;
6448                    }
6449                    f.write_str(")")?;
6450                }
6451                Ok(())
6452            }
6453            Self::DropRule {
6454                name,
6455                table,
6456                if_exists,
6457            } => {
6458                f.write_str("DROP RULE ")?;
6459                if *if_exists {
6460                    f.write_str("IF EXISTS ")?;
6461                }
6462                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6463            }
6464        }
6465    }
6466}
6467
6468impl fmt::Display for CreateDomainStatement {
6469    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6470        write!(
6471            f,
6472            "CREATE DOMAIN {} AS {}",
6473            quote_ident(&self.name),
6474            self.base_type
6475        )?;
6476        if let Some(d) = &self.default {
6477            write!(f, " DEFAULT {d}")?;
6478        }
6479        if self.not_null {
6480            f.write_str(" NOT NULL")?;
6481        }
6482        for c in &self.checks {
6483            write!(f, " CHECK ({c})")?;
6484        }
6485        Ok(())
6486    }
6487}
6488
6489impl fmt::Display for CreateTypeStatement {
6490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6491        write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
6492        match &self.kind {
6493            TypeKind::Enum { labels } => {
6494                f.write_str("ENUM (")?;
6495                for (i, l) in labels.iter().enumerate() {
6496                    if i > 0 {
6497                        f.write_str(", ")?;
6498                    }
6499                    write!(f, "'{}'", l.replace('\'', "''"))?;
6500                }
6501                f.write_str(")")
6502            }
6503            TypeKind::Composite { fields, .. } => {
6504                f.write_str("(")?;
6505                for (i, (n, t)) in fields.iter().enumerate() {
6506                    if i > 0 {
6507                        f.write_str(", ")?;
6508                    }
6509                    write!(f, "{} {}", quote_ident(n), t)?;
6510                }
6511                f.write_str(")")
6512            }
6513        }
6514    }
6515}
6516
6517impl fmt::Display for CreateMaterializedViewStatement {
6518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6519        f.write_str("CREATE MATERIALIZED VIEW ")?;
6520        if self.if_not_exists {
6521            f.write_str("IF NOT EXISTS ")?;
6522        }
6523        write!(f, "{}", quote_ident(&self.name))?;
6524        if !self.columns.is_empty() {
6525            f.write_str(" (")?;
6526            for (i, c) in self.columns.iter().enumerate() {
6527                if i > 0 {
6528                    f.write_str(", ")?;
6529                }
6530                write!(f, "{}", quote_ident(c))?;
6531            }
6532            f.write_str(")")?;
6533        }
6534        write!(f, " AS {}", self.body)?;
6535        if !self.with_data {
6536            f.write_str(" WITH NO DATA")?;
6537        }
6538        Ok(())
6539    }
6540}
6541
6542impl fmt::Display for CreateViewStatement {
6543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6544        f.write_str("CREATE ")?;
6545        if self.or_replace {
6546            f.write_str("OR REPLACE ")?;
6547        }
6548        if self.temporary {
6549            f.write_str("TEMPORARY ")?;
6550        }
6551        f.write_str("VIEW ")?;
6552        if self.if_not_exists {
6553            f.write_str("IF NOT EXISTS ")?;
6554        }
6555        write!(f, "{}", quote_ident(&self.name))?;
6556        if !self.columns.is_empty() {
6557            f.write_str(" (")?;
6558            for (i, c) in self.columns.iter().enumerate() {
6559                if i > 0 {
6560                    f.write_str(", ")?;
6561                }
6562                write!(f, "{}", quote_ident(c))?;
6563            }
6564            f.write_str(")")?;
6565        }
6566        write!(f, " AS {}", self.body)?;
6567        match self.check_option {
6568            Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
6569            Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
6570            None => Ok(()),
6571        }
6572    }
6573}
6574
6575impl fmt::Display for CreateSequenceStatement {
6576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6577        f.write_str("CREATE ")?;
6578        if self.temporary {
6579            f.write_str("TEMPORARY ")?;
6580        }
6581        f.write_str("SEQUENCE ")?;
6582        if self.if_not_exists {
6583            f.write_str("IF NOT EXISTS ")?;
6584        }
6585        write!(f, "{}", quote_ident(&self.name))?;
6586        if let Some(dt) = self.data_type {
6587            write!(f, " AS {dt}")?;
6588        }
6589        write_sequence_options(f, &self.options)
6590    }
6591}
6592
6593impl fmt::Display for AlterSequenceStatement {
6594    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6595        f.write_str("ALTER SEQUENCE ")?;
6596        if self.if_exists {
6597            f.write_str("IF EXISTS ")?;
6598        }
6599        write!(f, "{}", quote_ident(&self.name))?;
6600        write_sequence_options(f, &self.options)
6601    }
6602}
6603
6604impl fmt::Display for SequenceDataType {
6605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6606        f.write_str(match self {
6607            Self::SmallInt => "smallint",
6608            Self::Int => "integer",
6609            Self::BigInt => "bigint",
6610        })
6611    }
6612}
6613
6614fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
6615    if let Some(n) = o.increment {
6616        write!(f, " INCREMENT BY {n}")?;
6617    }
6618    match o.min_value {
6619        Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
6620        Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
6621        None => {}
6622    }
6623    match o.max_value {
6624        Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
6625        Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
6626        None => {}
6627    }
6628    if let Some(n) = o.start {
6629        write!(f, " START WITH {n}")?;
6630    }
6631    match o.restart {
6632        Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
6633        Some(None) => f.write_str(" RESTART")?,
6634        None => {}
6635    }
6636    if let Some(n) = o.cache {
6637        write!(f, " CACHE {n}")?;
6638    }
6639    match o.cycle {
6640        Some(true) => f.write_str(" CYCLE")?,
6641        Some(false) => f.write_str(" NO CYCLE")?,
6642        None => {}
6643    }
6644    if let Some(ob) = &o.owned_by {
6645        match ob {
6646            SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
6647            SequenceOwnedBy::Column { table, column } => {
6648                write!(
6649                    f,
6650                    " OWNED BY {}.{}",
6651                    quote_ident(table),
6652                    quote_ident(column)
6653                )?;
6654            }
6655        }
6656    }
6657    Ok(())
6658}
6659
6660impl fmt::Display for CreateFunctionStatement {
6661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6662        f.write_str("CREATE ")?;
6663        if self.or_replace {
6664            f.write_str("OR REPLACE ")?;
6665        }
6666        write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
6667        for (i, arg) in self.args.iter().enumerate() {
6668            if i > 0 {
6669                f.write_str(", ")?;
6670            }
6671            match arg.mode {
6672                FunctionArgMode::In => {}
6673                FunctionArgMode::Out => f.write_str("OUT ")?,
6674                FunctionArgMode::InOut => f.write_str("INOUT ")?,
6675            }
6676            if let Some(name) = &arg.name {
6677                write!(f, "{} ", quote_ident(name))?;
6678            }
6679            match &arg.ty {
6680                FunctionArgType::Typed(t) => write!(f, "{t}")?,
6681                FunctionArgType::Raw(s) => f.write_str(s)?,
6682            }
6683        }
6684        f.write_str(") RETURNS ")?;
6685        match &self.returns {
6686            FunctionReturn::Trigger => f.write_str("TRIGGER")?,
6687            FunctionReturn::Void => f.write_str("VOID")?,
6688            FunctionReturn::Type(t) => write!(f, "{t}")?,
6689            FunctionReturn::Other(s) => f.write_str(s)?,
6690        }
6691        write!(f, " LANGUAGE {} AS $$", self.language)?;
6692        match &self.body {
6693            FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
6694            FunctionBody::Raw(s) => f.write_str(s)?,
6695        }
6696        f.write_str("$$")
6697    }
6698}
6699
6700impl fmt::Display for PlPgSqlBlock {
6701    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6702        if !self.declarations.is_empty() {
6703            f.write_str("DECLARE\n")?;
6704            for d in &self.declarations {
6705                write!(f, "  {} ", quote_ident(&d.name))?;
6706                match &d.ty {
6707                    FunctionArgType::Typed(t) => write!(f, "{t}")?,
6708                    FunctionArgType::Raw(s) => f.write_str(s)?,
6709                }
6710                if let Some(e) = &d.default {
6711                    write!(f, " := {e}")?;
6712                }
6713                f.write_str(";\n")?;
6714            }
6715        }
6716        f.write_str("BEGIN\n")?;
6717        for stmt in &self.statements {
6718            writeln!(f, "  {stmt};")?;
6719        }
6720        // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
6721        // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
6722        // parsed block through it — so every exception handler a function
6723        // declared was thrown away AT STORE TIME. The block executed fine while
6724        // it was still an AST (a DO block never round-trips through text), which
6725        // is why only functions and triggers lost theirs.
6726        if !self.exception_handlers.is_empty() {
6727            f.write_str("EXCEPTION\n")?;
6728            for h in &self.exception_handlers {
6729                writeln!(f, "  WHEN {} THEN", h.conditions.join(" OR "))?;
6730                for stmt in &h.body {
6731                    writeln!(f, "    {stmt};")?;
6732                }
6733            }
6734        }
6735        f.write_str("END")
6736    }
6737}
6738
6739impl fmt::Display for PlPgSqlStmt {
6740    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6741        match self {
6742            Self::Assign { target, value } => write!(f, "{target} := {value}"),
6743            Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
6744            Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
6745            Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
6746            Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
6747            Self::Return(t) => match t {
6748                ReturnTarget::New => f.write_str("RETURN NEW"),
6749                ReturnTarget::Old => f.write_str("RETURN OLD"),
6750                ReturnTarget::Null => f.write_str("RETURN NULL"),
6751                ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
6752            },
6753            Self::If {
6754                branches,
6755                else_branch,
6756            } => {
6757                for (i, (cond, body)) in branches.iter().enumerate() {
6758                    if i == 0 {
6759                        write!(f, "IF {cond} THEN ")?;
6760                    } else {
6761                        write!(f, " ELSIF {cond} THEN ")?;
6762                    }
6763                    for (j, s) in body.iter().enumerate() {
6764                        if j > 0 {
6765                            f.write_str("; ")?;
6766                        }
6767                        write!(f, "{s}")?;
6768                    }
6769                }
6770                if !else_branch.is_empty() {
6771                    f.write_str(" ELSE ")?;
6772                    for (j, s) in else_branch.iter().enumerate() {
6773                        if j > 0 {
6774                            f.write_str("; ")?;
6775                        }
6776                        write!(f, "{s}")?;
6777                    }
6778                }
6779                f.write_str(" END IF")
6780            }
6781            Self::Raise {
6782                level,
6783                message,
6784                args,
6785            } => {
6786                let lvl = match level {
6787                    RaiseLevel::Notice => "NOTICE",
6788                    RaiseLevel::Warning => "WARNING",
6789                    RaiseLevel::Info => "INFO",
6790                    RaiseLevel::Log => "LOG",
6791                    RaiseLevel::Debug => "DEBUG",
6792                    RaiseLevel::Exception => "EXCEPTION",
6793                };
6794                write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
6795                for a in args {
6796                    write!(f, ", {a}")?;
6797                }
6798                Ok(())
6799            }
6800            Self::EmbeddedSql(s) => write!(f, "{s}"),
6801            Self::Assert { condition, message } => {
6802                write!(f, "ASSERT {condition}")?;
6803                if let Some(m) = message {
6804                    write!(f, ", {m}")?;
6805                }
6806                Ok(())
6807            }
6808            Self::While { condition, body } => {
6809                writeln!(f, "WHILE {condition} LOOP")?;
6810                for s in body {
6811                    writeln!(f, "  {s};")?;
6812                }
6813                f.write_str("END LOOP")
6814            }
6815            Self::ForRange {
6816                var,
6817                start,
6818                end,
6819                reverse,
6820                body,
6821            } => {
6822                write!(f, "FOR {var} IN ")?;
6823                if *reverse {
6824                    f.write_str("REVERSE ")?;
6825                }
6826                writeln!(f, "{start}..{end} LOOP")?;
6827                for s in body {
6828                    writeln!(f, "  {s};")?;
6829                }
6830                f.write_str("END LOOP")
6831            }
6832            Self::Loop { body } => {
6833                writeln!(f, "LOOP")?;
6834                for s in body {
6835                    writeln!(f, "  {s};")?;
6836                }
6837                f.write_str("END LOOP")
6838            }
6839            Self::Exit { when } => {
6840                f.write_str("EXIT")?;
6841                if let Some(c) = when {
6842                    write!(f, " WHEN {c}")?;
6843                }
6844                Ok(())
6845            }
6846            Self::Continue { when } => {
6847                f.write_str("CONTINUE")?;
6848                if let Some(c) = when {
6849                    write!(f, " WHEN {c}")?;
6850                }
6851                Ok(())
6852            }
6853            Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
6854            Self::ForQuery { var, query, body } => {
6855                writeln!(f, "FOR {var} IN ({query}) LOOP")?;
6856                for s in body {
6857                    writeln!(f, "  {s};")?;
6858                }
6859                f.write_str("END LOOP")
6860            }
6861            Self::ForExecute {
6862                var,
6863                sql_expr,
6864                body,
6865            } => {
6866                writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
6867                for s in body {
6868                    writeln!(f, "  {s};")?;
6869                }
6870                f.write_str("END LOOP")
6871            }
6872        }
6873    }
6874}
6875
6876impl fmt::Display for AssignTarget {
6877    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6878        match self {
6879            Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
6880            Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
6881            Self::Local(n) => f.write_str(n),
6882        }
6883    }
6884}
6885
6886impl fmt::Display for CreateTriggerStatement {
6887    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6888        f.write_str("CREATE ")?;
6889        if self.or_replace {
6890            f.write_str("OR REPLACE ")?;
6891        }
6892        write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
6893        match self.timing {
6894            TriggerTiming::Before => f.write_str("BEFORE")?,
6895            TriggerTiming::After => f.write_str("AFTER")?,
6896            TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
6897        }
6898        for (i, e) in self.events.iter().enumerate() {
6899            if i == 0 {
6900                f.write_str(" ")?;
6901            } else {
6902                f.write_str(" OR ")?;
6903            }
6904            match e {
6905                TriggerEvent::Insert => f.write_str("INSERT")?,
6906                TriggerEvent::Update => {
6907                    f.write_str("UPDATE")?;
6908                    if !self.update_columns.is_empty() {
6909                        f.write_str(" OF ")?;
6910                        for (j, col) in self.update_columns.iter().enumerate() {
6911                            if j > 0 {
6912                                f.write_str(", ")?;
6913                            }
6914                            f.write_str(&quote_ident(col))?;
6915                        }
6916                    }
6917                }
6918                TriggerEvent::Delete => f.write_str("DELETE")?,
6919                TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
6920            }
6921        }
6922        write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
6923        match self.for_each {
6924            TriggerForEach::Row => f.write_str("ROW")?,
6925            TriggerForEach::Statement => f.write_str("STATEMENT")?,
6926        }
6927        write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
6928    }
6929}
6930
6931impl fmt::Display for CreateIndexStatement {
6932    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6933        if self.is_unique {
6934            f.write_str("CREATE UNIQUE INDEX ")?;
6935        } else {
6936            f.write_str("CREATE INDEX ")?;
6937        }
6938        if self.if_not_exists {
6939            f.write_str("IF NOT EXISTS ")?;
6940        }
6941        write!(
6942            f,
6943            "{} ON {} ",
6944            quote_ident(&self.name),
6945            quote_ident(&self.table)
6946        )?;
6947        match self.method {
6948            IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
6949            IndexMethod::Brin => f.write_str("USING brin ")?,
6950            IndexMethod::Gin => f.write_str("USING gin ")?,
6951            IndexMethod::BTree => {}
6952        }
6953        if let Some(expr) = &self.expression {
6954            write!(f, "({})", expr)?;
6955        } else if self.extra_columns.is_empty() {
6956            // v7.15.0 — preserve operator class on round-trip
6957            // (`(col opclass)`) so WAL replay reconstructs the
6958            // engine-routing intent (e.g. `gin_trgm_ops` →
6959            // trigram-GIN build path).
6960            if let Some(op) = &self.opclass {
6961                write!(f, "({} {})", quote_ident(&self.column), op)?;
6962            } else {
6963                write!(f, "({})", quote_ident(&self.column))?;
6964            }
6965        } else {
6966            // v7.9.14 — multi-column key. Emit each column quoted
6967            // so the round-tripped form re-parses to identical AST.
6968            f.write_str("(")?;
6969            write!(f, "{}", quote_ident(&self.column))?;
6970            for c in &self.extra_columns {
6971                write!(f, ", {}", quote_ident(c))?;
6972            }
6973            f.write_str(")")?;
6974        }
6975        if !self.included_columns.is_empty() {
6976            f.write_str(" INCLUDE (")?;
6977            for (i, c) in self.included_columns.iter().enumerate() {
6978                if i > 0 {
6979                    f.write_str(", ")?;
6980                }
6981                write!(f, "{}", quote_ident(c))?;
6982            }
6983            f.write_str(")")?;
6984        }
6985        if let Some(pred) = &self.partial_predicate {
6986            write!(f, " WHERE {}", pred)?;
6987        }
6988        Ok(())
6989    }
6990}
6991
6992impl fmt::Display for CreateTableStatement {
6993    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6994        f.write_str("CREATE TABLE ")?;
6995        if self.if_not_exists {
6996            f.write_str("IF NOT EXISTS ")?;
6997        }
6998        write!(f, "{}", quote_ident(&self.name))?;
6999        // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
7000        // no column list and no constraints; the table inherits its
7001        // columns from the parent at engine-DDL time.
7002        if let Some(spec) = &self.partition_of {
7003            write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
7004            return match &spec.bounds {
7005                PartitionOfBoundsAst::Range { lower, upper } => {
7006                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7007                }
7008                PartitionOfBoundsAst::List { values } => {
7009                    f.write_str("FOR VALUES IN (")?;
7010                    for (i, v) in values.iter().enumerate() {
7011                        if i > 0 {
7012                            f.write_str(", ")?;
7013                        }
7014                        write!(f, "{}", v)?;
7015                    }
7016                    f.write_str(")")
7017                }
7018                PartitionOfBoundsAst::Hash { modulus, remainder } => {
7019                    write!(
7020                        f,
7021                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7022                        modulus, remainder
7023                    )
7024                }
7025                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7026            };
7027        }
7028        f.write_str(" (")?;
7029        for (i, col) in self.columns.iter().enumerate() {
7030            if i > 0 {
7031                f.write_str(", ")?;
7032            }
7033            write!(f, "{col}")?;
7034        }
7035        // v7.6.0 — render FK constraints in table-level form, after
7036        // the column list. WAL replay round-trips through Display, so
7037        // every FK must serialise here for replay to reconstruct the
7038        // schema bit-for-bit.
7039        for fk in &self.foreign_keys {
7040            f.write_str(", ")?;
7041            write!(f, "{fk}")?;
7042        }
7043        // v7.13.0 — render table-level constraints (PRIMARY KEY /
7044        // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
7045        // column-level UNIQUE / CHECK get lifted to this list at
7046        // parse time, so emitting only here avoids double-counting.
7047        for tc in &self.table_constraints {
7048            f.write_str(", ")?;
7049            write!(f, "{tc}")?;
7050        }
7051        f.write_str(")")?;
7052        // v7.37.6-B — partition-parent suffix renders after the
7053        // closing column-list paren, before the optional MySQL
7054        // table-options tail (which Display doesn't currently emit).
7055        if let Some(spec) = &self.partition_by {
7056            f.write_str(" PARTITION BY ")?;
7057            match spec.kind {
7058                PartitionKindAst::Range => f.write_str("RANGE ")?,
7059                PartitionKindAst::List => f.write_str("LIST ")?,
7060                PartitionKindAst::Hash => f.write_str("HASH ")?,
7061            }
7062            f.write_str("(")?;
7063            for (i, col) in spec.key_columns.iter().enumerate() {
7064                if i > 0 {
7065                    f.write_str(", ")?;
7066                }
7067                f.write_str(&quote_ident(col))?;
7068            }
7069            f.write_str(")")?;
7070        }
7071        Ok(())
7072    }
7073}
7074
7075fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
7076    match t {
7077        AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
7078        AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
7079            write!(f, "REPLICA IDENTITY USING INDEX {index}")
7080        }
7081        AlterTableTarget::Inherit { parent, detach } => {
7082            if *detach {
7083                write!(f, "NO INHERIT {parent}")
7084            } else {
7085                write!(f, "INHERIT {parent}")
7086            }
7087        }
7088        AlterTableTarget::SetHotTierBytes(n) => {
7089            write!(f, "SET hot_tier_bytes = {n}")
7090        }
7091        AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
7092        AlterTableTarget::DropForeignKey { name, if_exists } => {
7093            f.write_str("DROP CONSTRAINT ")?;
7094            if *if_exists {
7095                f.write_str("IF EXISTS ")?;
7096            }
7097            write!(f, "{}", quote_ident(name))
7098        }
7099        AlterTableTarget::DropIndex { name, if_exists } => {
7100            f.write_str("DROP INDEX ")?;
7101            if *if_exists {
7102                f.write_str("IF EXISTS ")?;
7103            }
7104            write!(f, "{}", quote_ident(name))
7105        }
7106        AlterTableTarget::AddColumn {
7107            column,
7108            if_not_exists,
7109        } => {
7110            f.write_str("ADD COLUMN ")?;
7111            if *if_not_exists {
7112                f.write_str("IF NOT EXISTS ")?;
7113            }
7114            write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
7115            if !column.nullable {
7116                f.write_str(" NOT NULL")?;
7117            }
7118            if let Some(d) = &column.default {
7119                write!(f, " DEFAULT {d}")?;
7120            }
7121            if column.auto_increment {
7122                f.write_str(" AUTO_INCREMENT")?;
7123            }
7124            if column.is_primary_key {
7125                f.write_str(" PRIMARY KEY")?;
7126            }
7127            Ok(())
7128        }
7129        AlterTableTarget::AlterColumnType {
7130            column,
7131            new_type,
7132            using,
7133            collation,
7134        } => {
7135            write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
7136            if let Some((_, name)) = collation {
7137                write!(f, " COLLATE {}", quote_ident(name))?;
7138            }
7139            if let Some(u) = using {
7140                write!(f, " USING {u}")?;
7141            }
7142            Ok(())
7143        }
7144        AlterTableTarget::DropColumn {
7145            column,
7146            if_exists,
7147            cascade,
7148        } => {
7149            f.write_str("DROP COLUMN ")?;
7150            if *if_exists {
7151                f.write_str("IF EXISTS ")?;
7152            }
7153            write!(f, "{}", quote_ident(column))?;
7154            if *cascade {
7155                f.write_str(" CASCADE")?;
7156            }
7157            Ok(())
7158        }
7159        AlterTableTarget::AddTableConstraint(tc) => {
7160            write!(f, "ADD {tc}")
7161        }
7162        AlterTableTarget::ValidateConstraint { name } => {
7163            write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
7164        }
7165        AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
7166        AlterTableTarget::ClusterOn { index } => match index {
7167            Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
7168            None => f.write_str("SET WITHOUT CLUSTER"),
7169        },
7170        AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
7171            // Round-trip-safe spelling: re-parsing this form lowers
7172            // back to SetColumnAutoIncrement (the nextval default is
7173            // how pg_dump says "serial").
7174            let seq = seq_name
7175                .clone()
7176                .unwrap_or_else(|| alloc::format!("{column}_seq"));
7177            write!(
7178                f,
7179                "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
7180                quote_ident(column)
7181            )
7182        }
7183        AlterTableTarget::RenameColumn { old, new } => {
7184            write!(
7185                f,
7186                "RENAME COLUMN {} TO {}",
7187                quote_ident(old),
7188                quote_ident(new)
7189            )
7190        }
7191        AlterTableTarget::RenameConstraint { old, new } => {
7192            write!(
7193                f,
7194                "RENAME CONSTRAINT {} TO {}",
7195                quote_ident(old),
7196                quote_ident(new)
7197            )
7198        }
7199        AlterTableTarget::RenameTable { new } => {
7200            write!(f, "RENAME TO {}", quote_ident(new))
7201        }
7202        AlterTableTarget::SetTriggerEnabled { which, enabled } => {
7203            f.write_str(if *enabled {
7204                "ENABLE TRIGGER "
7205            } else {
7206                "DISABLE TRIGGER "
7207            })?;
7208            match which {
7209                TriggerSelector::All => f.write_str("ALL"),
7210                TriggerSelector::Named(n) => f.write_str(&quote_ident(n)),
7211            }
7212        }
7213        AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
7214            (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
7215            (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
7216            (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
7217            (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
7218            (None, None) => Ok(()),
7219        },
7220        AlterTableTarget::AttachPartition { child, bounds } => {
7221            write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
7222            match bounds {
7223                PartitionOfBoundsAst::Range { lower, upper } => {
7224                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7225                }
7226                PartitionOfBoundsAst::List { values } => {
7227                    f.write_str("FOR VALUES IN (")?;
7228                    for (i, v) in values.iter().enumerate() {
7229                        if i > 0 {
7230                            f.write_str(", ")?;
7231                        }
7232                        write!(f, "{}", v)?;
7233                    }
7234                    f.write_str(")")
7235                }
7236                PartitionOfBoundsAst::Hash { modulus, remainder } => {
7237                    write!(
7238                        f,
7239                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7240                        modulus, remainder
7241                    )
7242                }
7243                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7244            }
7245        }
7246        AlterTableTarget::DetachPartition {
7247            child,
7248            concurrently,
7249            finalize,
7250        } => {
7251            write!(f, "DETACH PARTITION {}", quote_ident(child))?;
7252            if *concurrently {
7253                f.write_str(" CONCURRENTLY")?;
7254            }
7255            if *finalize {
7256                f.write_str(" FINALIZE")?;
7257            }
7258            Ok(())
7259        }
7260        AlterTableTarget::AlterColumnSetDefault {
7261            column,
7262            default_expr,
7263        } => write!(
7264            f,
7265            "ALTER COLUMN {} SET DEFAULT {}",
7266            quote_ident(column),
7267            default_expr
7268        ),
7269        AlterTableTarget::AlterColumnDropDefault { column } => {
7270            write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
7271        }
7272        AlterTableTarget::AlterColumnSetNotNull { column } => {
7273            write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
7274        }
7275        AlterTableTarget::AlterColumnDropNotNull { column } => {
7276            write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
7277        }
7278        AlterTableTarget::AlterColumnRestart { column, with } => {
7279            write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
7280            if let Some(n) = with {
7281                write!(f, " WITH {n}")?;
7282            }
7283            Ok(())
7284        }
7285        AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
7286            write!(
7287                f,
7288                "ALTER COLUMN {} DROP EXPRESSION{}",
7289                quote_ident(column),
7290                if *if_exists { " IF EXISTS" } else { "" }
7291            )
7292        }
7293        AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7294            write!(
7295                f,
7296                "ALTER COLUMN {} DROP IDENTITY{}",
7297                quote_ident(column),
7298                if *if_exists { " IF EXISTS" } else { "" }
7299            )
7300        }
7301        AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7302            write!(
7303                f,
7304                "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7305                quote_ident(column)
7306            )
7307        }
7308    }
7309}
7310
7311impl fmt::Display for TableConstraint {
7312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7313        match self {
7314            Self::PrimaryKey { name, columns, .. } => {
7315                if let Some(n) = name {
7316                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7317                }
7318                f.write_str("PRIMARY KEY (")?;
7319                for (i, c) in columns.iter().enumerate() {
7320                    if i > 0 {
7321                        f.write_str(", ")?;
7322                    }
7323                    f.write_str(&quote_ident(c))?;
7324                }
7325                f.write_str(")")
7326            }
7327            Self::Unique {
7328                name,
7329                columns,
7330                nulls_not_distinct,
7331                ..
7332            } => {
7333                if let Some(n) = name {
7334                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7335                }
7336                f.write_str("UNIQUE ")?;
7337                if *nulls_not_distinct {
7338                    f.write_str("NULLS NOT DISTINCT ")?;
7339                }
7340                f.write_str("(")?;
7341                for (i, c) in columns.iter().enumerate() {
7342                    if i > 0 {
7343                        f.write_str(", ")?;
7344                    }
7345                    f.write_str(&quote_ident(c))?;
7346                }
7347                f.write_str(")")
7348            }
7349            Self::Check {
7350                name,
7351                expr,
7352                not_valid,
7353            } => {
7354                if let Some(n) = name {
7355                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7356                }
7357                write!(f, "CHECK ({expr})")?;
7358                if *not_valid {
7359                    write!(f, " NOT VALID")?;
7360                }
7361                Ok(())
7362            }
7363            Self::Index { name, columns } => {
7364                f.write_str("KEY ")?;
7365                if let Some(n) = name {
7366                    write!(f, "{} ", quote_ident(n))?;
7367                }
7368                f.write_str("(")?;
7369                for (i, c) in columns.iter().enumerate() {
7370                    if i > 0 {
7371                        f.write_str(", ")?;
7372                    }
7373                    f.write_str(&quote_ident(c))?;
7374                }
7375                f.write_str(")")
7376            }
7377            Self::FulltextIndex { name, columns } => {
7378                // Mysqldump emits `FULLTEXT KEY name (cols)` —
7379                // Display rounds back to that shape so dump
7380                // replay reproduces the input verbatim.
7381                f.write_str("FULLTEXT KEY ")?;
7382                if let Some(n) = name {
7383                    write!(f, "{} ", quote_ident(n))?;
7384                }
7385                f.write_str("(")?;
7386                for (i, c) in columns.iter().enumerate() {
7387                    if i > 0 {
7388                        f.write_str(", ")?;
7389                    }
7390                    f.write_str(&quote_ident(c))?;
7391                }
7392                f.write_str(")")
7393            }
7394            Self::Exclude {
7395                name,
7396                method,
7397                elements,
7398            } => {
7399                if let Some(n) = name {
7400                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7401                }
7402                f.write_str("EXCLUDE ")?;
7403                if let Some(m) = method {
7404                    write!(f, "USING {m} ")?;
7405                }
7406                f.write_str("(")?;
7407                for (i, (col, op)) in elements.iter().enumerate() {
7408                    if i > 0 {
7409                        f.write_str(", ")?;
7410                    }
7411                    write!(f, "{} WITH {op}", quote_ident(col))?;
7412                }
7413                f.write_str(")")
7414            }
7415        }
7416    }
7417}
7418
7419impl fmt::Display for ForeignKeyConstraint {
7420    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7421        if let Some(name) = &self.name {
7422            write!(f, "CONSTRAINT {} ", quote_ident(name))?;
7423        }
7424        f.write_str("FOREIGN KEY (")?;
7425        for (i, c) in self.columns.iter().enumerate() {
7426            if i > 0 {
7427                f.write_str(", ")?;
7428            }
7429            f.write_str(&quote_ident(c))?;
7430        }
7431        write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
7432        if !self.parent_columns.is_empty() {
7433            f.write_str(" (")?;
7434            for (i, c) in self.parent_columns.iter().enumerate() {
7435                if i > 0 {
7436                    f.write_str(", ")?;
7437                }
7438                f.write_str(&quote_ident(c))?;
7439            }
7440            f.write_str(")")?;
7441        }
7442        // Only render non-default actions to keep Display output
7443        // close to user input. SPG's default is RESTRICT (matches
7444        // SQL spec).
7445        if self.on_delete != FkAction::Restrict {
7446            write!(f, " ON DELETE {}", self.on_delete)?;
7447        }
7448        if self.on_update != FkAction::Restrict {
7449            write!(f, " ON UPDATE {}", self.on_update)?;
7450        }
7451        Ok(())
7452    }
7453}
7454
7455impl fmt::Display for FkAction {
7456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7457        match self {
7458            Self::Restrict => f.write_str("RESTRICT"),
7459            Self::Cascade => f.write_str("CASCADE"),
7460            Self::SetNull => f.write_str("SET NULL"),
7461            Self::SetDefault => f.write_str("SET DEFAULT"),
7462            Self::NoAction => f.write_str("NO ACTION"),
7463        }
7464    }
7465}
7466
7467impl fmt::Display for ColumnDef {
7468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7469        // v7.30.1 (mailrs round-24 class audit) — the type position
7470        // must re-parse to the same ColumnDef: a user-defined type
7471        // reference and the MySQL inline ENUM / SET value lists all
7472        // lower `ty` to Text, so rendering `ty` lost them.
7473        write!(f, "{}", quote_ident(&self.name))?;
7474        if let Some(ut) = &self.user_type_ref {
7475            write!(f, " {}", quote_ident(ut))?;
7476        } else if let Some(variants) = &self.inline_enum_variants {
7477            write_variant_list(f, "ENUM", variants)?;
7478        } else if let Some(variants) = &self.inline_set_variants {
7479            write_variant_list(f, "SET", variants)?;
7480        } else {
7481            write!(f, " {}", self.ty)?;
7482        }
7483        if self.is_unsigned {
7484            f.write_str(" UNSIGNED")?;
7485        }
7486        // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
7487        // DDL. Only emits when non-default so the typical output
7488        // stays unchanged.
7489        match self.collation {
7490            Collation::Binary => {}
7491            Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
7492        }
7493        if let Some(d) = &self.default {
7494            write!(f, " DEFAULT {d}")?;
7495        }
7496        if self.auto_increment {
7497            f.write_str(" AUTO_INCREMENT")?;
7498        }
7499        if !self.nullable {
7500            f.write_str(" NOT NULL")?;
7501        }
7502        // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
7503        // is NOT lifted to a table-level constraint at parse time
7504        // (unlike UNIQUE / CHECK), so the WAL round trip of a
7505        // prepared CREATE TABLE silently dropped the primary key.
7506        if self.is_primary_key {
7507            f.write_str(" PRIMARY KEY")?;
7508        }
7509        // The parser accepts only CURRENT_TIMESTAMP here (stored as
7510        // now()), so that spelling is the lossless round trip.
7511        if self.on_update_runtime.is_some() {
7512            f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
7513        }
7514        // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
7515        // replay reconstructs the computed-column declaration. The
7516        // expression sits inside a single set of parens; STORED is
7517        // the only variant the parser accepts.
7518        if let Some(gen_expr) = &self.generated_stored_expr {
7519            write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
7520        }
7521        Ok(())
7522    }
7523}
7524
7525/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
7526/// types (MySQL flavour; `ty` is Text underneath).
7527fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
7528    write!(f, " {kw}(")?;
7529    for (i, v) in variants.iter().enumerate() {
7530        if i > 0 {
7531            f.write_str(", ")?;
7532        }
7533        write!(f, "'{}'", v.replace('\'', "''"))?;
7534    }
7535    f.write_str(")")
7536}
7537
7538impl fmt::Display for InsertStatement {
7539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7540        write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
7541        if let Some(cols) = &self.columns {
7542            f.write_str(" (")?;
7543            for (i, c) in cols.iter().enumerate() {
7544                if i > 0 {
7545                    f.write_str(", ")?;
7546                }
7547                f.write_str(&quote_ident(c))?;
7548            }
7549            f.write_str(")")?;
7550        }
7551        // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
7552        // skipping the VALUES list (mailrs round-5 G4).
7553        if let Some(sel) = &self.select_source {
7554            write!(f, " {sel}")?;
7555        } else {
7556            f.write_str(" VALUES ")?;
7557            for (ri, row) in self.rows.iter().enumerate() {
7558                if ri > 0 {
7559                    f.write_str(", ")?;
7560                }
7561                f.write_str("(")?;
7562                for (i, v) in row.iter().enumerate() {
7563                    if i > 0 {
7564                        f.write_str(", ")?;
7565                    }
7566                    write!(f, "{v}")?;
7567                }
7568                f.write_str(")")?;
7569            }
7570        }
7571        // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
7572        // Display round trip: WAL persistence renders the bind-final
7573        // AST through this impl, and a replayed bare INSERT turns a
7574        // legal upsert no-op into a UNIQUE violation that refuses to
7575        // open the catalog.
7576        if let Some(oc) = &self.on_conflict {
7577            write!(f, " {oc}")?;
7578        }
7579        write_returning(self.returning.as_deref(), f)?;
7580        Ok(())
7581    }
7582}
7583
7584/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
7585/// parser produced, so the AST→SQL round trip preserves upsert
7586/// semantics (WAL replay depends on it).
7587impl fmt::Display for OnConflictClause {
7588    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7589        f.write_str("ON CONFLICT")?;
7590        if let Some(name) = &self.constraint_name {
7591            write!(f, " ON CONSTRAINT {name}")?;
7592        }
7593        if !self.target_columns.is_empty() {
7594            f.write_str(" (")?;
7595            for (i, c) in self.target_columns.iter().enumerate() {
7596                if i > 0 {
7597                    f.write_str(", ")?;
7598                }
7599                f.write_str(&quote_ident(c))?;
7600            }
7601            f.write_str(")")?;
7602        }
7603        if let Some(w) = &self.index_where {
7604            write!(f, " WHERE {w}")?;
7605        }
7606        match &self.action {
7607            OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
7608            OnConflictAction::Update {
7609                assignments,
7610                where_,
7611            } => {
7612                f.write_str(" DO UPDATE SET ")?;
7613                for (i, (col, expr)) in assignments.iter().enumerate() {
7614                    if i > 0 {
7615                        f.write_str(", ")?;
7616                    }
7617                    write!(f, "{} = {expr}", quote_ident(col))?;
7618                }
7619                if let Some(w) = where_ {
7620                    write!(f, " WHERE {w}")?;
7621                }
7622                Ok(())
7623            }
7624        }
7625    }
7626}
7627
7628/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
7629/// tail for the three DML Display impls.
7630fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7631    let Some(items) = ret else {
7632        return Ok(());
7633    };
7634    f.write_str(" RETURNING ")?;
7635    for (i, item) in items.iter().enumerate() {
7636        if i > 0 {
7637            f.write_str(", ")?;
7638        }
7639        write!(f, "{item}")?;
7640    }
7641    Ok(())
7642}
7643
7644impl fmt::Display for UpdateStatement {
7645    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7646        write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
7647        for (i, (col, expr)) in self.assignments.iter().enumerate() {
7648            if i > 0 {
7649                f.write_str(", ")?;
7650            }
7651            write!(f, "{} = {expr}", quote_ident(col))?;
7652        }
7653        if let Some(w) = &self.where_ {
7654            write!(f, " WHERE {w}")?;
7655        }
7656        // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
7657        if let Some(ol) = self.order_limit.as_deref() {
7658            if !ol.order_by.is_empty() {
7659                f.write_str(" ORDER BY ")?;
7660                for (i, o) in ol.order_by.iter().enumerate() {
7661                    if i > 0 {
7662                        f.write_str(", ")?;
7663                    }
7664                    write!(f, "{}", o.expr)?;
7665                    if o.desc {
7666                        f.write_str(" DESC")?;
7667                    }
7668                    match o.nulls_first {
7669                        Some(true) => f.write_str(" NULLS FIRST")?,
7670                        Some(false) => f.write_str(" NULLS LAST")?,
7671                        None => {}
7672                    }
7673                }
7674            }
7675            if let Some(n) = ol.limit {
7676                write!(f, " LIMIT {n}")?;
7677            }
7678        }
7679        write_returning(self.returning.as_deref(), f)?;
7680        Ok(())
7681    }
7682}
7683
7684impl fmt::Display for DeleteStatement {
7685    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7686        write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
7687        if let Some(w) = &self.where_ {
7688            write!(f, " WHERE {w}")?;
7689        }
7690        write_returning(self.returning.as_deref(), f)?;
7691        Ok(())
7692    }
7693}
7694
7695impl fmt::Display for CteBody {
7696    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7697        match self {
7698            Self::Select(s) => write!(f, "{s}"),
7699            Self::Insert(s) => write!(f, "{s}"),
7700            Self::Update(s) => write!(f, "{s}"),
7701            Self::Delete(s) => write!(f, "{s}"),
7702            Self::Merge(s) => write!(f, "{s}"),
7703        }
7704    }
7705}
7706
7707impl fmt::Display for MergeStatement {
7708    // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
7709    // (it round-trips for the cases tests cover, not for
7710    // round-tripping every edge of the surface).
7711    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7712        fmt_with_clause(&self.ctes, f)?;
7713        f.write_str("MERGE INTO ")?;
7714        write!(f, "{}", quote_ident(&self.target))?;
7715        if let Some(a) = &self.target_alias {
7716            write!(f, " {}", quote_ident(a))?;
7717        }
7718        f.write_str(" USING ")?;
7719        if let Some(sub) = &self.source_select {
7720            write!(f, "({sub})")?;
7721        } else {
7722            write!(f, "{}", quote_ident(&self.source))?;
7723        }
7724        if let Some(a) = &self.source_alias {
7725            write!(f, " {}", quote_ident(a))?;
7726        }
7727        if !self.source_column_aliases.is_empty() {
7728            f.write_str("(")?;
7729            for (i, c) in self.source_column_aliases.iter().enumerate() {
7730                if i > 0 {
7731                    f.write_str(", ")?;
7732                }
7733                write!(f, "{}", quote_ident(c))?;
7734            }
7735            f.write_str(")")?;
7736        }
7737        write!(f, " ON {}", self.on)?;
7738        for clause in &self.clauses {
7739            f.write_str(" WHEN ")?;
7740            f.write_str(match clause.matched {
7741                MergeMatched::Matched => "MATCHED",
7742                MergeMatched::NotMatched => "NOT MATCHED",
7743                MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
7744            })?;
7745            if let Some(c) = &clause.condition {
7746                write!(f, " AND {c}")?;
7747            }
7748            f.write_str(" THEN ")?;
7749            match &clause.action {
7750                MergeAction::Insert { columns, values } => {
7751                    f.write_str("INSERT ")?;
7752                    // A column list is optional (round 146): the bare
7753                    // `INSERT VALUES (…)` form maps positionally.
7754                    if !columns.is_empty() {
7755                        f.write_str("(")?;
7756                        for (i, c) in columns.iter().enumerate() {
7757                            if i > 0 {
7758                                f.write_str(", ")?;
7759                            }
7760                            write!(f, "{}", quote_ident(c))?;
7761                        }
7762                        f.write_str(") ")?;
7763                    }
7764                    f.write_str("VALUES (")?;
7765                    for (i, v) in values.iter().enumerate() {
7766                        if i > 0 {
7767                            f.write_str(", ")?;
7768                        }
7769                        write!(f, "{v}")?;
7770                    }
7771                    f.write_str(")")?;
7772                }
7773                MergeAction::Update { assignments } => {
7774                    f.write_str("UPDATE SET ")?;
7775                    for (i, (c, e)) in assignments.iter().enumerate() {
7776                        if i > 0 {
7777                            f.write_str(", ")?;
7778                        }
7779                        write!(f, "{} = {e}", quote_ident(c))?;
7780                    }
7781                }
7782                MergeAction::Delete => f.write_str("DELETE")?,
7783                MergeAction::DoNothing => f.write_str("DO NOTHING")?,
7784            }
7785        }
7786        if let Some(items) = &self.returning {
7787            f.write_str(" RETURNING ")?;
7788            for (i, it) in items.iter().enumerate() {
7789                if i > 0 {
7790                    f.write_str(", ")?;
7791                }
7792                write!(f, "{it}")?;
7793            }
7794        }
7795        Ok(())
7796    }
7797}
7798
7799/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
7800/// carry a CTE list and must round-trip it identically.
7801fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
7802    if ctes.is_empty() {
7803        return Ok(());
7804    }
7805    f.write_str("WITH ")?;
7806    if ctes.iter().any(|c| c.recursive) {
7807        f.write_str("RECURSIVE ")?;
7808    }
7809    for (i, cte) in ctes.iter().enumerate() {
7810        if i > 0 {
7811            f.write_str(", ")?;
7812        }
7813        f.write_str(&quote_ident(&cte.name))?;
7814        if !cte.column_overrides.is_empty() {
7815            f.write_str(" (")?;
7816            for (ci, c) in cte.column_overrides.iter().enumerate() {
7817                if ci > 0 {
7818                    f.write_str(", ")?;
7819                }
7820                f.write_str(&quote_ident(c))?;
7821            }
7822            f.write_str(")")?;
7823        }
7824        write!(f, " AS ({})", cte.body)?;
7825    }
7826    f.write_str(" ")
7827}
7828
7829impl fmt::Display for SelectStatement {
7830    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7831        // v7.30.1 (mailrs round-24 class audit) — the WITH clause
7832        // must survive the round trip; a CTE-using statement
7833        // re-parsed without it references undefined tables.
7834        fmt_with_clause(&self.ctes, f)?;
7835        write_bare_select(self, f)?;
7836        for (kind, peer) in &self.unions {
7837            f.write_str(match kind {
7838                UnionKind::Distinct => " UNION ",
7839                UnionKind::All => " UNION ALL ",
7840                UnionKind::Intersect => " INTERSECT ",
7841                UnionKind::IntersectAll => " INTERSECT ALL ",
7842                UnionKind::Except => " EXCEPT ",
7843                UnionKind::ExceptAll => " EXCEPT ALL ",
7844            })?;
7845            write_bare_select(peer, f)?;
7846        }
7847        if !self.order_by.is_empty() {
7848            f.write_str(" ORDER BY ")?;
7849            for (i, o) in self.order_by.iter().enumerate() {
7850                if i > 0 {
7851                    f.write_str(", ")?;
7852                }
7853                write!(f, "{}", o.expr)?;
7854                if o.desc {
7855                    f.write_str(" DESC")?;
7856                }
7857                match o.nulls_first {
7858                    Some(true) => f.write_str(" NULLS FIRST")?,
7859                    Some(false) => f.write_str(" NULLS LAST")?,
7860                    None => {}
7861                }
7862            }
7863        }
7864        // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
7865        // exists in the FETCH FIRST spelling; rendering it as LIMIT
7866        // dropped the tie-extension semantics on replay. The parser
7867        // accepts OFFSET before FETCH, so keep that order here.
7868        if self.limit_with_ties {
7869            if let Some(o) = &self.offset {
7870                write!(f, " OFFSET {o}")?;
7871            }
7872            if let Some(n) = &self.limit {
7873                write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
7874            }
7875        } else {
7876            if let Some(n) = &self.limit {
7877                write!(f, " LIMIT {n}")?;
7878            }
7879            if let Some(o) = &self.offset {
7880                write!(f, " OFFSET {o}")?;
7881            }
7882        }
7883        Ok(())
7884    }
7885}
7886
7887fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7888    f.write_str("SELECT ")?;
7889    if s.distinct {
7890        f.write_str("DISTINCT ")?;
7891    }
7892    write_bare_select_body(s, f)
7893}
7894
7895fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7896    for (i, item) in s.items.iter().enumerate() {
7897        if i > 0 {
7898            f.write_str(", ")?;
7899        }
7900        write!(f, "{item}")?;
7901    }
7902    if let Some(t) = &s.from {
7903        write!(f, " FROM {t}")?;
7904    }
7905    if let Some(e) = &s.where_ {
7906        write!(f, " WHERE {e}")?;
7907    }
7908    if let Some(gs) = &s.group_by {
7909        f.write_str(" GROUP BY ")?;
7910        for (i, g) in gs.iter().enumerate() {
7911            if i > 0 {
7912                f.write_str(", ")?;
7913            }
7914            write!(f, "{g}")?;
7915        }
7916    } else if s.group_by_all {
7917        // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
7918        // shortcut parses to group_by: None + this flag; dropping
7919        // it turned an aggregate query into a bare projection on
7920        // re-parse.
7921        f.write_str(" GROUP BY ALL")?;
7922    }
7923    if let Some(h) = &s.having {
7924        write!(f, " HAVING {h}")?;
7925    }
7926    Ok(())
7927}
7928
7929impl fmt::Display for SelectItem {
7930    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7931        match self {
7932            Self::Wildcard => f.write_str("*"),
7933            Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
7934            Self::Expr { expr, alias } => {
7935                write!(f, "{expr}")?;
7936                if let Some(a) = alias {
7937                    write!(f, " AS {}", quote_ident(a))?;
7938                }
7939                Ok(())
7940            }
7941        }
7942    }
7943}
7944
7945impl fmt::Display for FromClause {
7946    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7947        write!(f, "{}", self.primary)?;
7948        for j in &self.joins {
7949            match j.kind {
7950                JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
7951                JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
7952                JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
7953                JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
7954                JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
7955                JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
7956            }
7957            if let Some(on) = &j.on {
7958                write!(f, " ON {on}")?;
7959            }
7960        }
7961        Ok(())
7962    }
7963}
7964
7965/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
7966/// for NESTED). Kept close to the parser's grammar so it re-parses.
7967fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
7968    for (i, c) in cols.iter().enumerate() {
7969        if i > 0 {
7970            f.write_str(", ")?;
7971        }
7972        match c {
7973            JsonTableColumn::Ordinality { name } => {
7974                write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
7975            }
7976            JsonTableColumn::Nested { path, columns } => {
7977                write!(f, "NESTED PATH '{path}' COLUMNS (")?;
7978                fmt_json_table_columns(f, columns)?;
7979                f.write_str(")")?;
7980            }
7981            JsonTableColumn::Regular {
7982                name,
7983                ty,
7984                path,
7985                exists,
7986                format_json,
7987                wrapper,
7988                on_empty,
7989                on_error,
7990            } => {
7991                write!(f, "{} {ty}", quote_ident(name))?;
7992                if *format_json {
7993                    f.write_str(" FORMAT JSON")?;
7994                }
7995                if *exists {
7996                    write!(f, " EXISTS PATH '{path}'")?;
7997                } else {
7998                    write!(f, " PATH '{path}'")?;
7999                }
8000                if *wrapper {
8001                    f.write_str(" WITH WRAPPER")?;
8002                }
8003                if let JsonTableOnBehavior::Error = on_empty {
8004                    f.write_str(" ERROR ON EMPTY")?;
8005                } else if let JsonTableOnBehavior::Default(e) = on_empty {
8006                    write!(f, " DEFAULT {e} ON EMPTY")?;
8007                }
8008                if let JsonTableOnBehavior::Error = on_error {
8009                    f.write_str(" ERROR ON ERROR")?;
8010                } else if let JsonTableOnBehavior::Default(e) = on_error {
8011                    write!(f, " DEFAULT {e} ON ERROR")?;
8012                }
8013            }
8014        }
8015    }
8016    Ok(())
8017}
8018
8019impl fmt::Display for TableRef {
8020    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8021        // v7.30.1 (mailrs round-24 class audit) — the dynamic
8022        // table-ref shapes must round-trip: rendering only the
8023        // (synthetic) name turned LATERAL / unnest() /
8024        // generate_series() into references to nonexistent tables
8025        // on re-parse.
8026        // v7.39 (round 205) — JSON_TABLE round-trips through Display
8027        // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
8028        if let Some(jt) = &self.json_table {
8029            write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
8030            if !jt.passing.is_empty() {
8031                f.write_str(" PASSING ")?;
8032                for (i, (n, e)) in jt.passing.iter().enumerate() {
8033                    if i > 0 {
8034                        f.write_str(", ")?;
8035                    }
8036                    write!(f, "{e} AS {}", quote_ident(n))?;
8037                }
8038            }
8039            f.write_str(" COLUMNS (")?;
8040            fmt_json_table_columns(f, &jt.columns)?;
8041            f.write_str(")")?;
8042            if let Some(a) = &self.alias {
8043                write!(f, " AS {}", quote_ident(a))?;
8044            }
8045            return Ok(());
8046        }
8047        if let Some(inner) = &self.lateral_subquery {
8048            write!(f, "LATERAL ({inner})")?;
8049            if let Some(a) = &self.alias {
8050                write!(f, " AS {}", quote_ident(a))?;
8051                // v7.37 D.28 — a derived table on the lateral_subquery channel
8052                // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
8053                // lowers here). Rendering the alias without the column list lost
8054                // the column names on re-parse (a view body round-trips through
8055                // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
8056                if !self.unnest_column_aliases.is_empty() {
8057                    f.write_str(" (")?;
8058                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8059                        if i > 0 {
8060                            f.write_str(", ")?;
8061                        }
8062                        f.write_str(&quote_ident(c))?;
8063                    }
8064                    f.write_str(")")?;
8065                }
8066            }
8067            return Ok(());
8068        }
8069        if let Some(expr) = &self.unnest_expr {
8070            write!(f, "UNNEST({expr})")?;
8071            if let Some(a) = &self.alias {
8072                write!(f, " AS {}", quote_ident(a))?;
8073                if !self.unnest_column_aliases.is_empty() {
8074                    f.write_str(" (")?;
8075                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8076                        if i > 0 {
8077                            f.write_str(", ")?;
8078                        }
8079                        f.write_str(&quote_ident(c))?;
8080                    }
8081                    f.write_str(")")?;
8082                }
8083            }
8084            return Ok(());
8085        }
8086        // 7.38.1 S5.1 — a FROM-position table function must re-render
8087        // as the CALL, not its bare name: ARRAY(subquery) desugars by
8088        // re-parsing the subquery's canonical text, and a dropped
8089        // argument list turned `pg_options_to_table(x)` into a
8090        // relation lookup that does not exist.
8091        if let Some(call) = &self.table_fn_call {
8092            let (fn_name, args) = call.as_ref();
8093            write!(f, "{fn_name}(")?;
8094            for (i, a) in args.iter().enumerate() {
8095                if i > 0 {
8096                    f.write_str(", ")?;
8097                }
8098                write!(f, "{a}")?;
8099            }
8100            f.write_str(")")?;
8101            if let Some(a) = &self.alias {
8102                write!(f, " AS {}", quote_ident(a))?;
8103                if !self.unnest_column_aliases.is_empty() {
8104                    f.write_str("(")?;
8105                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8106                        if i > 0 {
8107                            f.write_str(", ")?;
8108                        }
8109                        write!(f, "{}", quote_ident(c))?;
8110                    }
8111                    f.write_str(")")?;
8112                }
8113            }
8114            return Ok(());
8115        }
8116        if let Some(args) = &self.generate_series_args {
8117            f.write_str("generate_series(")?;
8118            for (i, a) in args.iter().enumerate() {
8119                if i > 0 {
8120                    f.write_str(", ")?;
8121                }
8122                write!(f, "{a}")?;
8123            }
8124            f.write_str(")")?;
8125            if let Some(a) = &self.alias {
8126                write!(f, " AS {}", quote_ident(a))?;
8127            }
8128            return Ok(());
8129        }
8130        write!(f, "{}", quote_ident(&self.name))?;
8131        if let Some(seg) = self.as_of_segment {
8132            write!(f, " AS OF SEGMENT {seg}")?;
8133        }
8134        if let Some(a) = &self.alias {
8135            write!(f, " AS {}", quote_ident(a))?;
8136        }
8137        Ok(())
8138    }
8139}
8140
8141impl fmt::Display for ColumnName {
8142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8143        if let Some(q) = &self.qualifier {
8144            write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
8145        } else {
8146            write!(f, "{}", quote_ident(&self.name))
8147        }
8148    }
8149}
8150
8151/// v7.39 (round 311) — render the left spine of an AND / OR chain
8152/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
8153/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
8154/// SAME operator flattens; anything else is an ordinary operand.
8155fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
8156    if let Expr::Binary {
8157        lhs,
8158        op: inner,
8159        rhs,
8160    } = e
8161        && *inner == op
8162    {
8163        write_bool_chain(f, lhs, op)?;
8164        return write!(f, " {op} {rhs}");
8165    }
8166    write!(f, "{e}")
8167}
8168
8169/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
8170/// form `pg_get_constraintdef(oid, true)` and friends return.
8171///
8172/// The default [`fmt::Display`] parenthesises every operator node, which
8173/// is what PG's non-pretty deparse does and what makes the text
8174/// round-trip. Pretty drops the pairs the grammar can put back, and the
8175/// rule is NOT plain precedence minimisation — measured against PG 18.4
8176/// across 37 shapes:
8177///
8178///   * the boolean layer follows precedence (NOT > AND > OR): an OR
8179///     under an AND keeps its parens, an AND under an OR does not, and a
8180///     comparison under any of them does not (`NOT a > 1`);
8181///   * an associative chain flattens completely, even where the source
8182///     nested it to the right (`a AND (b AND c)` prints as one chain);
8183///   * but an operand of a comparison or arithmetic operator keeps its
8184///     parens whenever it is itself an operator expression — so
8185///     `(a + b) > 10` and `(- a) + b`, even though precedence alone
8186///     would not require either. A cast, function call, column or
8187///     literal in that position does not (`a::text = t`,
8188///     `length(code) > 2`); a cast counts as compound exactly when the
8189///     thing it casts is (`((a + b)::text) = t`).
8190///
8191/// Anything outside that layer defers to `Display`, which is never
8192/// wrong — only more parenthesised than PG would print.
8193#[must_use]
8194pub fn pretty_expr(e: &Expr) -> String {
8195    let mut out = String::new();
8196    write_pretty(&mut out, e, PrettyParent::None, false, false);
8197    out
8198}
8199
8200/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
8201/// writes it.
8202///
8203/// MariaDB names the offending expression in its out-of-range message
8204/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
8205/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
8206/// MySQL client, for a cast the client had just written the other way.
8207#[must_use]
8208pub fn pretty_expr_mysql(e: &Expr) -> String {
8209    let mut out = String::new();
8210    write_pretty(&mut out, e, PrettyParent::None, false, true);
8211    out
8212}
8213
8214/// v7.39 (round 505) — how strongly an expression suggests its own column
8215/// name. A cast keeps its argument's name only when that name is STRONG;
8216/// otherwise the cast reports the type it casts to.
8217///
8218/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
8219/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
8220/// itself `text` — so `case` and a function name cannot be the same kind of
8221/// answer, even though a bare `CASE …` does report `case`.
8222#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8223enum NameStrength {
8224    /// Nothing to go on — PG reports `?column?`.
8225    None,
8226    /// A name, but one a cast overrides: `case`, or a type name.
8227    Weak,
8228    /// A name a cast keeps: a column, or the function that produced it.
8229    Strong,
8230}
8231
8232/// v7.39 (round 505) — the column name PG18 gives a projected expression
8233/// that carries no `AS` alias. `None` means `?column?`.
8234///
8235/// SPG used to print the parsed expression back out, which matched neither
8236/// oracle and made name-keyed row access miss on both wires:
8237///
8238/// | query        | PG18       | SPG (before) |
8239/// |--------------|------------|--------------|
8240/// | `upper(s)`   | `upper`    | `upper(s)`   |
8241/// | `a+b`        | `?column?` | `(a + b)`    |
8242/// | `'lit'`      | `?column?` | `'lit'`      |
8243/// | `CASE …`     | `case`     | `CASE WHEN (a = 1) THEN …` |
8244///
8245/// Every rule below is one of those measurements, taken with `\gdesc`
8246/// against PG18: a call is named for its function, a cast recurses into its
8247/// argument and falls back to the type, a scalar subquery takes the name of
8248/// the column it selects, and operators have no name at all.
8249#[must_use]
8250pub fn figure_column_name(expr: &Expr) -> Option<String> {
8251    let (name, _) = figure_name_inner(expr);
8252    name
8253}
8254
8255/// The name a function reports, which is not always the name SPG parsed it
8256/// under: `count(*)` is held as `count_star` so the star arity survives the
8257/// AST, and that internal spelling must not reach a client. PG18 reports
8258/// `count`.
8259fn canonical_function_name(name: &str) -> String {
8260    match name {
8261        "count_star" => "count".to_string(),
8262        other => other.to_ascii_lowercase(),
8263    }
8264}
8265
8266/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
8267/// reports when its operand has none of its own. Only the spellings that
8268/// differ from what the user writes need an entry; everything else is
8269/// already its own typname.
8270fn cast_target_typname(target: &CastTarget) -> String {
8271    let written = target.to_string().to_ascii_lowercase();
8272    let base = written.strip_suffix("[]").unwrap_or(&written);
8273    let mapped = match base {
8274        "bigint" => "int8",
8275        "integer" | "int" => "int4",
8276        "smallint" => "int2",
8277        "boolean" => "bool",
8278        "double precision" => "float8",
8279        "real" => "float4",
8280        "character varying" => "varchar",
8281        "character" => "bpchar",
8282        "timestamp with time zone" => "timestamptz",
8283        "timestamp without time zone" => "timestamp",
8284        "time without time zone" => "time",
8285        "decimal" => "numeric",
8286        other => other,
8287    };
8288    if written.ends_with("[]") {
8289        alloc::format!("_{mapped}")
8290    } else {
8291        String::from(mapped)
8292    }
8293}
8294
8295fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8296    let strong = |n: String| (Some(n), NameStrength::Strong);
8297    match expr {
8298        // A column keeps its own name, qualifier and all discarded:
8299        // `lbl.a` reports `a`.
8300        Expr::Column(c) => strong(c.name.clone()),
8301        // Calls are named for the function. This covers the shapes that
8302        // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8303        // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8304        // because PG resolves them to functions before naming them.
8305        Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8306            strong(canonical_function_name(name))
8307        }
8308        Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8309        Expr::Extract { .. } => strong("extract".to_string()),
8310        Expr::Exists { .. } => strong("exists".to_string()),
8311        Expr::Array(_) => strong("array".to_string()),
8312        // `(expr).field` is named for the field, as a column would be.
8313        Expr::FieldAccess { field, .. } => strong(field.clone()),
8314        // A cast prefers its argument's name and settles for the type:
8315        // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8316        Expr::Cast {
8317            expr: inner,
8318            target,
8319        } => match figure_name_inner(inner) {
8320            (Some(n), NameStrength::Strong) => strong(n),
8321            // v7.38.7 — the fallback is the target type's INTERNAL name,
8322            // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8323            // the `bigint` the user typed. Measured on PG18 alongside
8324            // `CAST(7 AS bigint)`, which answers `int8` too.
8325            _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8326        },
8327        // A scalar subquery reports whatever its single output column
8328        // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8329        Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8330        // `CASE …` names itself, but weakly — a cast around it wins.
8331        Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8332        // A literal that carries its own type names itself for that type:
8333        // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8334        // reports nothing. Weak, like any other type name.
8335        Expr::Literal(Literal::Interval { .. }) => {
8336            (Some("interval".to_string()), NameStrength::Weak)
8337        }
8338        // A wrapper that adds no name of its own.
8339        Expr::Variadic(inner) => figure_name_inner(inner),
8340        Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
8341        // Everything else — operators, comparisons, IS NULL, LIKE, IN,
8342        // literals, placeholders — reports `?column?`.
8343        _ => (None, NameStrength::None),
8344    }
8345}
8346
8347/// The name a scalar subquery's single projected column reports.
8348fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
8349    match sel.items.as_slice() {
8350        [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
8351        [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
8352        _ => (None, NameStrength::None),
8353    }
8354}
8355
8356/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
8357fn pretty_prec(e: &Expr) -> u8 {
8358    match e {
8359        Expr::Binary { op, .. } => match op {
8360            // v7.39 (round 407) — this deparse ladder mirrors the parser's:
8361            // OR < XOR < AND < NOT < comparison < additive < multiplicative.
8362            // XOR (MySQL-only) sits between OR and AND, so AND and everything
8363            // above shifted +1 to open rung 2 for it.
8364            BinOp::Or => 1,
8365            BinOp::LogicalXor => 2,
8366            BinOp::And => 3,
8367            BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
8368            BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
8369            // Everything else in this enum is a comparison-shaped
8370            // operator; they share one level, as in the grammar.
8371            _ => 5,
8372        },
8373        Expr::Unary { op, .. } => match op {
8374            UnOp::Not => 4,
8375            UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
8376        },
8377        _ => u8::MAX,
8378    }
8379}
8380
8381/// Is this node an operator expression — the thing an arithmetic or
8382/// comparison parent keeps parentheses around? A cast inherits the
8383/// answer from what it casts.
8384fn pretty_is_compound(e: &Expr) -> bool {
8385    match e {
8386        Expr::Binary { .. } | Expr::Unary { .. } => true,
8387        Expr::Cast { expr, .. } => pretty_is_compound(expr),
8388        _ => false,
8389    }
8390}
8391
8392/// `parent` describes the enclosing operator: its binding power, and
8393/// whether it is a comparison (which keeps parens around any operator
8394/// operand) or a NOT (which keeps them at equal power too).
8395#[derive(Clone, Copy, PartialEq)]
8396enum PrettyParent {
8397    /// Nothing encloses this node.
8398    None,
8399    /// A comparison-shaped operator: an operator operand always keeps
8400    /// its parens, whatever precedence would allow.
8401    Comparison,
8402    /// Arithmetic / concatenation: precedence decides.
8403    Arith(u8),
8404    /// A boolean connective: precedence decides.
8405    Bool(u8),
8406    /// `NOT`: precedence decides, but equal power still needs parens so
8407    /// `NOT (NOT a > 1)` does not collapse.
8408    Not,
8409}
8410
8411fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
8412    let prec = pretty_prec(e);
8413    let is_unary_sign = matches!(
8414        e,
8415        Expr::Unary {
8416            op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
8417            ..
8418        }
8419    );
8420    let needs = match parent {
8421        PrettyParent::None => false,
8422        PrettyParent::Comparison => pretty_is_compound(e),
8423        // A sign always keeps its parens under an operator — PG writes
8424        // `(- a) + b` even though precedence would not require it.
8425        PrettyParent::Arith(p) => {
8426            is_unary_sign
8427                || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
8428                    && (prec < p || (prec == p && is_rhs)))
8429        }
8430        PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
8431        PrettyParent::Not => {
8432            matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
8433        }
8434    };
8435    if needs {
8436        out.push('(');
8437    }
8438    match e {
8439        Expr::Binary { lhs, op, rhs } => {
8440            let child = match op {
8441                BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
8442                BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
8443                    PrettyParent::Arith(prec)
8444                }
8445                _ => PrettyParent::Comparison,
8446            };
8447            write_pretty(out, lhs, child, false, mysql);
8448            out.push(' ');
8449            out.push_str(&alloc::format!("{op}"));
8450            out.push(' ');
8451            // AND / OR are associative, so an explicitly right-nested
8452            // chain still prints as one chain.
8453            let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
8454            write_pretty(out, rhs, child, rhs_is_rhs, mysql);
8455        }
8456        Expr::Unary { op, expr } => match op {
8457            UnOp::Not => {
8458                out.push_str("NOT ");
8459                write_pretty(out, expr, PrettyParent::Not, false, mysql);
8460            }
8461            UnOp::Neg => {
8462                out.push_str("- ");
8463                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8464            }
8465            UnOp::Plus => {
8466                out.push_str("+ ");
8467                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8468            }
8469            UnOp::BitNot => {
8470                out.push('~');
8471                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8472            }
8473        },
8474        Expr::Cast { expr, target } => {
8475            if mysql {
8476                // MySQL's own spelling, which is what its error messages
8477                // quote back.
8478                out.push_str("cast(");
8479                write_pretty(out, expr, PrettyParent::None, false, mysql);
8480                out.push_str(&alloc::format!(
8481                    " as {})",
8482                    target.to_string().to_lowercase()
8483                ));
8484            } else {
8485                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8486                out.push_str(&alloc::format!("::{target}"));
8487            }
8488        }
8489        Expr::IsNull { expr, negated } => {
8490            write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8491            out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
8492        }
8493        other => out.push_str(&alloc::format!("{other}")),
8494    }
8495    if needs {
8496        out.push(')');
8497    }
8498}
8499
8500const fn pretty_prec_not() -> u8 {
8501    // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
8502    // when the XOR insertion shifted the deparse ladder up by one).
8503    4
8504}
8505
8506impl fmt::Display for Expr {
8507    #[allow(clippy::too_many_lines)]
8508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8509        match self {
8510            Self::Literal(l) => write!(f, "{l}"),
8511            Self::Column(c) => write!(f, "{c}"),
8512            Self::Placeholder(n) => write!(f, "${n}"),
8513            // Round-trips as the spelling PG's docs lead with.
8514            Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
8515            Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
8516            // Round-trips with the name quoted, which is how PG spells a
8517            // collation everywhere: `"en_US.utf8"`, `"C"`.
8518            Self::Collate { expr, collation } => {
8519                write!(f, "{expr} COLLATE {}", quote_ident(collation))
8520            }
8521            // v7.39 (round 311) — an AND / OR chain that nests to the
8522            // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
8523            // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
8524            // its parentheses, because that is a different grouping as
8525            // written. Both halves measured against PG 18.4's deparse,
8526            // which flattens a same-operator left chain at parse time and
8527            // leaves `a AND (b AND c)` alone.
8528            Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
8529                f.write_str("(")?;
8530                write_bool_chain(f, lhs, *op)?;
8531                write!(f, " {op} {rhs}")?;
8532                f.write_str(")")
8533            }
8534            Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
8535            Self::Unary { op, expr } => match op {
8536                UnOp::Not => write!(f, "(NOT {expr})"),
8537                // A space after the sign, as PG's deparse writes it.
8538                UnOp::Neg => write!(f, "(- {expr})"),
8539                UnOp::Plus => write!(f, "(+ {expr})"),
8540                UnOp::BitNot => write!(f, "(~{expr})"),
8541            },
8542            // The OPERAND carries the parentheses, not the cast:
8543            // `(a)::text`, `((a + b))::text`. PG words it this way, and
8544            // it is what keeps `a::text = t` from reading as a cast of
8545            // the comparison.
8546            Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
8547            Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
8548            Self::AggregateOrdered {
8549                call,
8550                order_by,
8551                distinct,
8552                filter,
8553            } => {
8554                let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8555                    for (i, o) in order_by.iter().enumerate() {
8556                        if i > 0 {
8557                            f.write_str(", ")?;
8558                        }
8559                        write!(f, "{}", o.expr)?;
8560                        if o.desc {
8561                            f.write_str(" DESC")?;
8562                        }
8563                        match o.nulls_first {
8564                            Some(true) => f.write_str(" NULLS FIRST")?,
8565                            Some(false) => f.write_str(" NULLS LAST")?,
8566                            None => {}
8567                        }
8568                    }
8569                    Ok(())
8570                };
8571                // Ordered-set aggregates (`percentile_cont(f) WITHIN
8572                // GROUP (ORDER BY x)`) render the in-parens args as the
8573                // direct argument and the sort spec under WITHIN GROUP —
8574                // not as an in-argument ORDER BY.
8575                let ordered_set = matches!(
8576                    call.as_ref(),
8577                    Expr::FunctionCall { name, .. }
8578                        if matches!(
8579                            name.to_ascii_lowercase().as_str(),
8580                            "percentile_cont" | "percentile_disc" | "mode"
8581                        )
8582                );
8583                if ordered_set {
8584                    write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
8585                    fmt_order_by(f)?;
8586                    f.write_str(")")?;
8587                } else {
8588                    // `name([DISTINCT ]args [ORDER BY …])` — peel the
8589                    // inner call's parens to splice modifiers.
8590                    let inner = alloc::format!("{call}");
8591                    let body = inner.strip_suffix(')').unwrap_or(&inner);
8592                    let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
8593                    write!(f, "{head}(")?;
8594                    if *distinct {
8595                        f.write_str("DISTINCT ")?;
8596                    }
8597                    write!(f, "{args_part}")?;
8598                    if !order_by.is_empty() {
8599                        f.write_str(" ORDER BY ")?;
8600                        fmt_order_by(f)?;
8601                    }
8602                    f.write_str(")")?;
8603                }
8604                if let Some(cond) = filter {
8605                    write!(f, " FILTER (WHERE {cond})")?;
8606                }
8607                Ok(())
8608            }
8609            Self::IsNull { expr, negated } => {
8610                if *negated {
8611                    write!(f, "({expr} IS NOT NULL)")
8612                } else {
8613                    write!(f, "({expr} IS NULL)")
8614                }
8615            }
8616            Self::BoolTest {
8617                expr,
8618                value,
8619                negated,
8620            } => {
8621                let word = match value {
8622                    Some(true) => "TRUE",
8623                    Some(false) => "FALSE",
8624                    None => "UNKNOWN",
8625                };
8626                if *negated {
8627                    write!(f, "({expr} IS NOT {word})")
8628                } else {
8629                    write!(f, "({expr} IS {word})")
8630                }
8631            }
8632            Self::FunctionCall { name, args } => {
8633                write!(f, "{name}(")?;
8634                for (i, a) in args.iter().enumerate() {
8635                    if i > 0 {
8636                        f.write_str(", ")?;
8637                    }
8638                    write!(f, "{a}")?;
8639                }
8640                f.write_str(")")
8641            }
8642            Self::Like {
8643                expr,
8644                pattern,
8645                negated,
8646                case_insensitive,
8647            } => {
8648                let op = match (negated, case_insensitive) {
8649                    (false, false) => "LIKE",
8650                    (true, false) => "NOT LIKE",
8651                    (false, true) => "ILIKE",
8652                    (true, true) => "NOT ILIKE",
8653                };
8654                write!(f, "({expr} {op} {pattern})")
8655            }
8656            Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
8657            Self::WindowFunction {
8658                name,
8659                args,
8660                partition_by,
8661                order_by,
8662                frame,
8663                null_treatment,
8664                filter,
8665            } => {
8666                write!(f, "{name}(")?;
8667                for (i, a) in args.iter().enumerate() {
8668                    if i > 0 {
8669                        f.write_str(", ")?;
8670                    }
8671                    write!(f, "{a}")?;
8672                }
8673                f.write_str(")")?;
8674                // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
8675                // OVER; it round-trips so a window body's Display re-parses.
8676                if let Some(cond) = filter {
8677                    write!(f, " FILTER (WHERE {cond})")?;
8678                }
8679                // v7.30.1 (mailrs round-24 class audit) — IGNORE
8680                // NULLS sits between the arg list and OVER; dropping
8681                // it reverted replayed queries to RESPECT NULLS.
8682                if matches!(null_treatment, NullTreatment::Ignore) {
8683                    f.write_str(" IGNORE NULLS")?;
8684                }
8685                f.write_str(" OVER (")?;
8686                if !partition_by.is_empty() {
8687                    f.write_str("PARTITION BY ")?;
8688                    for (i, p) in partition_by.iter().enumerate() {
8689                        if i > 0 {
8690                            f.write_str(", ")?;
8691                        }
8692                        write!(f, "{p}")?;
8693                    }
8694                }
8695                if !order_by.is_empty() {
8696                    if !partition_by.is_empty() {
8697                        f.write_str(" ")?;
8698                    }
8699                    f.write_str("ORDER BY ")?;
8700                    for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
8701                        if i > 0 {
8702                            f.write_str(", ")?;
8703                        }
8704                        write!(f, "{e}")?;
8705                        if *desc {
8706                            f.write_str(" DESC")?;
8707                        }
8708                        match nulls_first {
8709                            Some(true) => f.write_str(" NULLS FIRST")?,
8710                            Some(false) => f.write_str(" NULLS LAST")?,
8711                            None => {}
8712                        }
8713                    }
8714                }
8715                if let Some(fr) = frame {
8716                    if !partition_by.is_empty() || !order_by.is_empty() {
8717                        f.write_str(" ")?;
8718                    }
8719                    let k = match fr.kind {
8720                        FrameKind::Rows => "ROWS",
8721                        FrameKind::Range => "RANGE",
8722                        FrameKind::Groups => "GROUPS",
8723                    };
8724                    if let Some(end) = &fr.end {
8725                        write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
8726                    } else {
8727                        write!(f, "{k} {}", fr.start)?;
8728                    }
8729                }
8730                f.write_str(")")
8731            }
8732            Self::ScalarSubquery(s) => write!(f, "({s})"),
8733            Self::Exists { subquery, negated } => {
8734                if *negated {
8735                    write!(f, "NOT EXISTS ({subquery})")
8736                } else {
8737                    write!(f, "EXISTS ({subquery})")
8738                }
8739            }
8740            Self::InSubquery {
8741                expr,
8742                subquery,
8743                negated,
8744            } => {
8745                if *negated {
8746                    write!(f, "({expr} NOT IN ({subquery}))")
8747                } else {
8748                    write!(f, "({expr} IN ({subquery}))")
8749                }
8750            }
8751            Self::RowInSubquery {
8752                row,
8753                subquery,
8754                negated,
8755            } => {
8756                write!(f, "(")?;
8757                for (i, e) in row.iter().enumerate() {
8758                    if i > 0 {
8759                        write!(f, ", ")?;
8760                    }
8761                    write!(f, "{e}")?;
8762                }
8763                let kw = if *negated { ") NOT IN (" } else { ") IN (" };
8764                write!(f, "{kw}{subquery})")
8765            }
8766            Self::RowCmpSubquery { row, op, subquery } => {
8767                write!(f, "(")?;
8768                for (i, e) in row.iter().enumerate() {
8769                    if i > 0 {
8770                        write!(f, ", ")?;
8771                    }
8772                    write!(f, "{e}")?;
8773                }
8774                write!(f, ") {op} ({subquery})")
8775            }
8776            Self::InList {
8777                expr,
8778                list,
8779                negated,
8780            } => {
8781                let kw = if *negated { " NOT IN (" } else { " IN (" };
8782                write!(f, "({expr}{kw}")?;
8783                for (i, e) in list.iter().enumerate() {
8784                    if i > 0 {
8785                        f.write_str(", ")?;
8786                    }
8787                    write!(f, "{e}")?;
8788                }
8789                f.write_str("))")
8790            }
8791            Self::Array(items) => {
8792                f.write_str("ARRAY[")?;
8793                for (i, e) in items.iter().enumerate() {
8794                    if i > 0 {
8795                        f.write_str(", ")?;
8796                    }
8797                    write!(f, "{e}")?;
8798                }
8799                f.write_str("]")
8800            }
8801            Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
8802            Self::ArraySlice { target, lo, hi } => {
8803                write!(f, "({target}[")?;
8804                if let Some(l) = lo {
8805                    write!(f, "{l}")?;
8806                }
8807                write!(f, ":")?;
8808                if let Some(h) = hi {
8809                    write!(f, "{h}")?;
8810                }
8811                write!(f, "])")
8812            }
8813            Self::AnyAll {
8814                expr,
8815                op,
8816                array,
8817                is_any,
8818            } => {
8819                let kw = if *is_any { "ANY" } else { "ALL" };
8820                write!(f, "({expr} {op} {kw}({array}))")
8821            }
8822            Self::Case {
8823                operand,
8824                branches,
8825                else_branch,
8826            } => {
8827                f.write_str("CASE")?;
8828                if let Some(op) = operand {
8829                    write!(f, " {op}")?;
8830                }
8831                for (w, t) in branches {
8832                    write!(f, " WHEN {w} THEN {t}")?;
8833                }
8834                if let Some(e) = else_branch {
8835                    write!(f, " ELSE {e}")?;
8836                }
8837                f.write_str(" END")
8838            }
8839        }
8840    }
8841}
8842
8843/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
8844/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
8845pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
8846    use alloc::string::ToString;
8847    if scale == 0 {
8848        return alloc::format!("{unscaled}");
8849    }
8850    let neg = unscaled < 0;
8851    let digits = alloc::format!("{}", unscaled.unsigned_abs());
8852    let scale = scale as usize;
8853    let (int_part, frac_part) = if digits.len() > scale {
8854        (
8855            digits[..digits.len() - scale].to_string(),
8856            digits[digits.len() - scale..].to_string(),
8857        )
8858    } else {
8859        ("0".to_string(), alloc::format!("{digits:0>scale$}"))
8860    };
8861    alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
8862}
8863
8864/// A single-quoted SQL string, with an embedded quote doubled.
8865fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
8866    f.write_str("'")?;
8867    for c in s.chars() {
8868        if c == '\'' {
8869            f.write_str("''")?;
8870        } else {
8871            write!(f, "{c}")?;
8872        }
8873    }
8874    f.write_str("'")
8875}
8876
8877impl fmt::Display for Literal {
8878    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8879        match self {
8880            Self::Integer(n) => write!(f, "{n}"),
8881            Self::Float(x) => {
8882                let s = format!("{x}");
8883                // Default Display for an integral f64 (e.g. 1.0) emits "1",
8884                // which would round-trip back to Integer. Force a dot.
8885                if s.contains('.') || s.contains('e') || s.contains('E') {
8886                    f.write_str(&s)
8887                } else {
8888                    write!(f, "{s}.0")
8889                }
8890            }
8891            Self::Numeric { unscaled, scale } => {
8892                // Render the exact decimal `unscaled / 10^scale`, preserving
8893                // scale (trailing zeros) — round-trips to the same literal.
8894                f.write_str(&render_exact_decimal(*unscaled, *scale))
8895            }
8896            Self::NumericBig(s) => f.write_str(s),
8897            // Printed exactly as the text form was, so a reader cannot
8898            // tell whether the constant was decoded or not.
8899            Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
8900            Self::String(s) => write_quoted(f, s),
8901            Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
8902            Self::Null => f.write_str("NULL"),
8903            // PG external array form. Display round-trip re-enters
8904            // through the column-typed text coerce, same as pgwire.
8905            Self::TextArray(items) => {
8906                f.write_str("'{")?;
8907                for (i, it) in items.iter().enumerate() {
8908                    if i > 0 {
8909                        f.write_str(",")?;
8910                    }
8911                    match it {
8912                        None => f.write_str("NULL")?,
8913                        Some(s) => {
8914                            f.write_str("\"")?;
8915                            for c in s.chars() {
8916                                match c {
8917                                    // array-element escapes
8918                                    '"' | '\\' => write!(f, "\\{c}")?,
8919                                    // the OUTER wrapper is a SQL string
8920                                    // literal — embedded quotes must
8921                                    // double, or the rendered form
8922                                    // (WAL replay parses it back) is
8923                                    // invalid SQL
8924                                    '\'' => f.write_str("''")?,
8925                                    _ => write!(f, "{c}")?,
8926                                }
8927                            }
8928                            f.write_str("\"")?;
8929                        }
8930                    }
8931                }
8932                f.write_str("}'")
8933            }
8934            Self::IntArray(items) => {
8935                f.write_str("'{")?;
8936                for (i, it) in items.iter().enumerate() {
8937                    if i > 0 {
8938                        f.write_str(",")?;
8939                    }
8940                    match it {
8941                        None => f.write_str("NULL")?,
8942                        Some(n) => write!(f, "{n}")?,
8943                    }
8944                }
8945                f.write_str("}'")
8946            }
8947            Self::BigIntArray(items) => {
8948                f.write_str("'{")?;
8949                for (i, it) in items.iter().enumerate() {
8950                    if i > 0 {
8951                        f.write_str(",")?;
8952                    }
8953                    match it {
8954                        None => f.write_str("NULL")?,
8955                        Some(n) => write!(f, "{n}")?,
8956                    }
8957                }
8958                f.write_str("}'")
8959            }
8960            Self::Vector(v) => {
8961                f.write_str("[")?;
8962                for (i, x) in v.iter().enumerate() {
8963                    if i > 0 {
8964                        f.write_str(", ")?;
8965                    }
8966                    let s = format!("{x}");
8967                    // Mirror Float Display: force a dot so re-parse stays
8968                    // numerically literal.
8969                    if s.contains('.') || s.contains('e') || s.contains('E') {
8970                        f.write_str(&s)?;
8971                    } else {
8972                        write!(f, "{s}.0")?;
8973                    }
8974                }
8975                f.write_str("]")
8976            }
8977            Self::Interval { text, .. } => {
8978                f.write_str("INTERVAL '")?;
8979                for c in text.chars() {
8980                    if c == '\'' {
8981                        f.write_str("''")?;
8982                    } else {
8983                        write!(f, "{c}")?;
8984                    }
8985                }
8986                f.write_str("'")
8987            }
8988        }
8989    }
8990}
8991
8992impl fmt::Display for BinOp {
8993    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8994        f.write_str(match self {
8995            Self::Or => "OR",
8996            Self::And => "AND",
8997            Self::Eq => "=",
8998            Self::NotEq => "<>",
8999            Self::IsDistinctFrom => "IS DISTINCT FROM",
9000            Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
9001            Self::IntDiv => "DIV",
9002            Self::Lt => "<",
9003            Self::LtEq => "<=",
9004            Self::Gt => ">",
9005            Self::GtEq => ">=",
9006            Self::Add => "+",
9007            Self::Sub => "-",
9008            Self::Mul => "*",
9009            Self::Div => "/",
9010            Self::Mod => "%",
9011            Self::L2Distance => "<->",
9012            Self::GeomParallel => "?||",
9013            Self::OverLeft => "&<",
9014            Self::OverRight => "&>",
9015            Self::GeomPerp => "?-|",
9016            Self::GeomSameAs => "~=",
9017            Self::ClosestPoint => "##",
9018            Self::GeomHoriz => "?-",
9019            Self::InnerProduct => "<#>",
9020            Self::CosineDistance => "<=>",
9021            Self::Concat => "||",
9022            Self::BitOr => "|",
9023            Self::BitAnd => "&",
9024            Self::BitXor => "#",
9025            Self::LogicalXor => "xor",
9026            Self::JsonGet => "->",
9027            Self::JsonGetText => "->>",
9028            Self::JsonGetPath => "#>",
9029            Self::JsonGetPathText => "#>>",
9030            Self::JsonContains => "@>",
9031            Self::JsonPathExists => "@?",
9032            Self::JsonContainedBy => "<@",
9033            Self::JsonKeyExists => "?",
9034            Self::JsonKeysAny => "?|",
9035            Self::JsonKeysAll => "?&",
9036            Self::JsonDeletePath => "#-",
9037            Self::TsMatch => "@@",
9038            Self::InetContainedBy => "<<",
9039            Self::InetContainedByEq => "<<=",
9040            Self::InetContains => ">>",
9041            Self::InetContainsEq => ">>=",
9042            Self::InetOverlap => "&&",
9043            Self::Intersects => "?#",
9044            Self::IsBelow => "<^",
9045            Self::IsAbove => ">^",
9046            Self::PatternLt => "~<~",
9047            Self::PatternLtEq => "~<=~",
9048            Self::PatternGt => "~>~",
9049            Self::PatternGtEq => "~>=~",
9050        })
9051    }
9052}
9053
9054/// Quote `s` as a PG double-quoted identifier when required (keyword,
9055/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
9056/// Otherwise return it as-is. Returns an owned `String` to keep the call site
9057/// uniform.
9058pub(crate) fn quote_ident(s: &str) -> String {
9059    let needs_quote = match s.chars().next() {
9060        None => true,
9061        Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
9062        _ => {
9063            s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
9064                || s.chars().any(|c| c.is_ascii_uppercase())
9065                || is_keyword(s)
9066        }
9067    };
9068    if !needs_quote {
9069        return s.to_string();
9070    }
9071    let mut out = String::with_capacity(s.len() + 2);
9072    out.push('"');
9073    for c in s.chars() {
9074        if c == '"' {
9075            out.push_str("\"\"");
9076        } else {
9077            out.push(c);
9078        }
9079    }
9080    out.push('"');
9081    out
9082}
9083
9084fn is_keyword(s: &str) -> bool {
9085    matches!(
9086        &*s.to_ascii_lowercase(),
9087        "select"
9088            | "from"
9089            | "where"
9090            | "as"
9091            | "null"
9092            | "true"
9093            | "false"
9094            | "and"
9095            | "or"
9096            | "not"
9097            | "create"
9098            | "table"
9099            | "insert"
9100            | "into"
9101            | "values"
9102            | "index"
9103            | "on"
9104            | "begin"
9105            | "commit"
9106            | "rollback"
9107            | "is"
9108            | "between"
9109            | "in"
9110            | "like"
9111            | "group"
9112            | "distinct"
9113            | "union"
9114            | "all"
9115            | "join"
9116            | "inner"
9117            | "left"
9118            | "cross"
9119            | "outer"
9120            | "default"
9121            | "savepoint"
9122            | "release"
9123            | "to"
9124            | "having"
9125            | "show"
9126            | "extract"
9127            | "offset"
9128            | "asc"
9129            | "desc"
9130            | "interval"
9131    )
9132}
9133
9134#[cfg(test)]
9135mod tests {
9136    use super::*;
9137    use alloc::vec;
9138
9139    #[test]
9140    fn integer_literal_renders_without_dot() {
9141        assert_eq!(Literal::Integer(42).to_string(), "42");
9142    }
9143
9144    #[test]
9145    fn integral_float_keeps_dot() {
9146        assert_eq!(Literal::Float(1.0).to_string(), "1.0");
9147        assert_eq!(Literal::Float(1.5).to_string(), "1.5");
9148        assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
9149    }
9150
9151    #[test]
9152    fn string_literal_doubles_quote() {
9153        assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
9154    }
9155
9156    #[test]
9157    fn bool_and_null_render_uppercase() {
9158        assert_eq!(Literal::Bool(true).to_string(), "TRUE");
9159        assert_eq!(Literal::Bool(false).to_string(), "FALSE");
9160        assert_eq!(Literal::Null.to_string(), "NULL");
9161    }
9162
9163    #[test]
9164    fn binary_op_always_parenthesised() {
9165        let e = Expr::Binary {
9166            lhs: Box::new(Expr::Literal(Literal::Integer(1))),
9167            op: BinOp::Add,
9168            rhs: Box::new(Expr::Literal(Literal::Integer(2))),
9169        };
9170        assert_eq!(e.to_string(), "(1 + 2)");
9171    }
9172
9173    #[test]
9174    fn select_star_from_table() {
9175        let s = SelectStatement {
9176            locking: None,
9177            items: vec![SelectItem::Wildcard],
9178            from: Some(FromClause {
9179                primary: TableRef {
9180                    name: "users".into(),
9181                    alias: None,
9182                    only: false,
9183                    as_of_segment: None,
9184                    unnest_expr: None,
9185                    unnest_column_aliases: Vec::new(),
9186                    with_ordinality: false,
9187                    generate_series_args: None,
9188                    lateral_subquery: None,
9189                    jsonb_each_text_arg: None,
9190                    table_fn_call: None,
9191                    rows_from: None,
9192                    json_table: None,
9193                    scalar_fn_item: false,
9194                },
9195                joins: vec![],
9196            }),
9197            where_: None,
9198            group_by: None,
9199            group_by_all: false,
9200            having: None,
9201            unions: vec![],
9202            order_by: Vec::new(),
9203            limit: None,
9204            offset: None,
9205            limit_with_ties: false,
9206            window_check_exprs: Vec::new(),
9207            distinct: false,
9208            distinct_on: Vec::new(),
9209            ctes: vec![],
9210        };
9211        assert_eq!(s.to_string(), "SELECT * FROM users");
9212    }
9213
9214    #[test]
9215    fn quote_ident_for_uppercase_and_keyword() {
9216        assert_eq!(quote_ident("foo"), "foo");
9217        assert_eq!(quote_ident("Foo"), "\"Foo\"");
9218        assert_eq!(quote_ident("select"), "\"select\"");
9219        assert_eq!(quote_ident(""), "\"\"");
9220        assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
9221    }
9222}