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)]
18#[non_exhaustive]
19pub enum CopyFormat {
20    #[default]
21    Text,
22    Csv,
23}
24
25/// Options for `COPY … TO STDOUT [WITH] (…)`. Defaults reproduce the
26/// bare `COPY … TO STDOUT` text-format behaviour, so an empty option
27/// list is a no-op. `delimiter` / `null_str` / `quote` fall back to the
28/// per-format defaults (text: `\t` / `\N`; csv: `,` / `` / `"`) when
29/// unset.
30#[derive(Debug, Clone, PartialEq, Eq, Default)]
31pub struct CopyOptions {
32    pub format: CopyFormat,
33    pub header: bool,
34    pub delimiter: Option<char>,
35    pub null_str: Option<String>,
36    pub quote: Option<char>,
37    /// v7.39 (round 247) — CSV `ESCAPE`: the character that precedes a
38    /// quote (or itself) inside a quoted cell. Defaults to the quote
39    /// character (PG's doubling behavior).
40    pub escape: Option<char>,
41    /// v7.39 (round 247) — CSV `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`:
42    /// columns whose non-NULL cells always quote. `Some(vec![])` is the
43    /// `*` spelling (every column).
44    pub force_quote: Option<Vec<String>>,
45    /// v7.39 (round 265) — CSV `FORCE_NOT_NULL (col, …)`: for these
46    /// columns an UNQUOTED empty field reads as the empty string rather
47    /// than NULL (probed). COPY FROM only.
48    pub force_not_null: Option<Vec<String>>,
49    /// v7.39 (round 265) — CSV `FORCE_NULL (col, …)`: for these columns
50    /// a QUOTED empty field (`""`) also reads as NULL (probed). COPY
51    /// FROM only.
52    pub force_null: Option<Vec<String>>,
53}
54
55/// v7.39 (round 218) — FETCH / MOVE cursor direction. PG grammar: single-row
56/// forms (NEXT / PRIOR / FIRST / LAST / ABSOLUTE n / RELATIVE n) return at
57/// most one row; multi-row forms (bare n / ALL / FORWARD [n|ALL] /
58/// BACKWARD [n|ALL]) stream a run. A negative bare/FORWARD count means
59/// BACKWARD (normalized at execution).
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum CursorDirection {
62    Next,
63    Prior,
64    First,
65    Last,
66    Absolute(i64),
67    Relative(i64),
68    /// Bare `FETCH n` / `FORWARD n` (negative = backward n).
69    Count(i64),
70    /// `ALL` / `FORWARD ALL`.
71    All,
72    Backward(i64),
73    BackwardAll,
74}
75
76impl fmt::Display for CursorDirection {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::Next => f.write_str("NEXT"),
80            Self::Prior => f.write_str("PRIOR"),
81            Self::First => f.write_str("FIRST"),
82            Self::Last => f.write_str("LAST"),
83            Self::Absolute(n) => write!(f, "ABSOLUTE {n}"),
84            Self::Relative(n) => write!(f, "RELATIVE {n}"),
85            Self::Count(n) => write!(f, "FORWARD {n}"),
86            Self::All => f.write_str("ALL"),
87            Self::Backward(n) => write!(f, "BACKWARD {n}"),
88            Self::BackwardAll => f.write_str("BACKWARD ALL"),
89        }
90    }
91}
92
93/// v7.39 (round 320, V53) — what a `DISCARD` throws away.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum DiscardTarget {
96    All,
97    Plans,
98    Sequences,
99    Temp,
100}
101
102impl fmt::Display for DiscardTarget {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.write_str(match self {
105            Self::All => "ALL",
106            Self::Plans => "PLANS",
107            Self::Sequences => "SEQUENCES",
108            Self::Temp => "TEMP",
109        })
110    }
111}
112
113/// v7.39 (round 535) — which maintenance statement, and therefore what
114/// its target names. Measured on PG18: INDEX / TABLE / CLUSTER name a
115/// relation, SCHEMA names a schema, and SYSTEM / DATABASE name neither
116/// in a way SPG can refuse.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum MaintainKind {
120    ReindexRelation,
121    ReindexSchema,
122    /// `REINDEX SYSTEM` / `REINDEX DATABASE`, and a bare `CLUSTER`.
123    Whole,
124    ClusterRelation,
125}
126
127/// v7.39 (round 547) — see [`Statement::SetDbRoleSetting`]. Boxed in the
128/// enum so the variant costs one pointer.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct SetDbRoleSettingStatement {
131    pub database: Option<String>,
132    pub role: Option<String>,
133    pub param: Option<String>,
134    pub value: Option<String>,
135}
136
137/// v7.39 (round 696) — which operand a [`Statement::ValidateOnly`] names,
138/// and therefore which catalog answers whether it exists.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum ValidateOnlyKind {
141    /// `LOCK TABLE <t> [, …]` — the relation must exist.
142    LockTable,
143    /// Every role named must exist: `DROP OWNED BY <r> [, …]`,
144    /// `REASSIGN OWNED BY <r> [, …] TO <r>`, `ALTER ROLE <r> …`.
145    RoleName,
146    /// `SECURITY LABEL …` — PG refuses unconditionally, because no label
147    /// provider is loaded. SPG has none either.
148    SecurityLabel,
149    /// v7.39 (round 697) — `CREATE EXTENSION <e>`: the extension must be
150    /// AVAILABLE (PG: `extension "x" is not available`).
151    ExtensionAvailable,
152    /// v7.39 (round 708) — `ALTER TYPE <t> <any no-op form>`: the TYPE must
153    /// exist (PG: `type "x" does not exist`); the action itself stays a
154    /// no-op (PG genuinely renames; that residual is recorded).
155    TypeName,
156    /// v7.39 (round 708) — `ALTER AGGREGATE name(args) …`: names[0] is the
157    /// aggregate, the rest its argument type names (`*` = the `(*)` form).
158    /// Existence only; the action no-ops (PG really renames built-ins —
159    /// measured — and SPG does not model that).
160    AggregateName,
161    /// v7.39 (round 708) — `DROP CONVERSION <c>`: SPG ships no conversions,
162    /// so every name answers PG's `conversion "x" does not exist`.
163    ConversionName,
164    /// v7.39 (round 708) — `DROP LANGUAGE <l>`: an unknown language does
165    /// not exist; a shipped one is required (PG's two wordings, measured).
166    LanguageName,
167    /// v7.39 (round 709) — a collation name: performable or PG's
168    /// `collation "x" for encoding "UTF8" does not exist`.
169    CollationName,
170    /// v7.39 (round 709) — a text search configuration name.
171    TsConfigName,
172    /// v7.39 (round 709) — an event trigger name. SPG has none, so the
173    /// not-found answer is total.
174    EventTriggerName,
175    /// v7.39 (round 709) — a tablespace name. SPG has none beyond PG's two
176    /// built-ins, whose drop PG refuses with `permission denied` (measured).
177    TablespaceName,
178    /// v7.39 (round 709) — a large-object oid (names[0], decimal). The
179    /// registry is real (round 287), so the check is a lookup.
180    LargeObjectOid,
181    /// v7.39 (round 706) — `CREATE SERVER` / `CREATE FOREIGN TABLE` /
182    /// `CREATE FOREIGN DATA WRAPPER`. SPG has no foreign-data
183    /// infrastructure at all, so PG's refusals (`foreign-data wrapper "x"
184    /// does not exist`, `server "x" does not exist`) cannot be copied —
185    /// PG can refuse because the missing piece is installable there.
186    /// Accepted with a WARNING, the extension resolution (round 697):
187    /// refusing turns a dump that restores today into one that needs
188    /// editing, and silent acceptance was the actual defect.
189    ForeignInfra,
190    /// v7.39 (round 697) — `DROP EXTENSION <e>`: it must be installed
191    /// (PG: `extension "x" does not exist`).
192    ExtensionInstalled,
193    /// v7.40.12 — `SET SESSION AUTHORIZATION <r>`. APPENDED, not
194    /// inserted: putting it beside `RoleName` where it belongs read
195    /// better and shifted thirteen discriminants, which
196    /// `cargo-semver-checks` calls out as its own breaking change
197    /// (`enum_no_repr_variant_discriminant_changed`) for anyone casting
198    /// the enum. Reading order is not worth a break nobody asked for.
199    ///
200    /// It used to share `RoleName` with the three role statements, and one question separates
201    /// them: PG does NOT count this statement as a query, so a later
202    /// `SET TRANSACTION ISOLATION LEVEL` still succeeds, while after
203    /// `DROP OWNED BY`, `REASSIGN OWNED BY` or `ALTER ROLE` it is
204    /// refused with 25001. All four measured on PG 18.6. The role check
205    /// is identical; the snapshot answer is not.
206    SessionAuthorization,
207}
208
209#[derive(Debug, Clone, PartialEq)]
210#[allow(clippy::large_enum_variant)] // Statement::Select dominates; Boxing would touch every match site
211pub enum Statement {
212    /// v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET <name>`.
213    ///
214    /// It used to be swallowed with the rest of the ALTER no-ops, which meant
215    /// `ALTER SYSTEM SET nosuch_guc = 1` was ACCEPTED where PG18 answers
216    /// `unrecognized configuration parameter`. SPG still applies nothing —
217    /// there is no postgresql.auto.conf to write — but a name it does not
218    /// know is now refused rather than swallowed.
219    ///
220    /// `None` is `RESET ALL`, which names no parameter.
221    AlterSystem {
222        parameter: Option<String>,
223    },
224    /// `DROP DATABASE [IF EXISTS] <name>`. SPG is single-database, so
225    /// this never succeeds; the name and the flag are carried so the
226    /// engine can answer with PG's wording for the two cases PG itself
227    /// has — an unknown name, or the database you are connected to.
228    DropDatabase {
229        name: String,
230        if_exists: bool,
231    },
232    /// A statement SPG accepts as a no-op but PG refuses inside a
233    /// transaction block — today `CREATE DATABASE` / `DROP DATABASE`,
234    /// which are no-ops here because SPG is single-database.
235    ///
236    /// The no-op path they used to share (`Statement::Empty`) also
237    /// carries CREATE ROLE, CREATE CAST and a dozen others that PG is
238    /// happy to run inside a transaction, so the object has to be named
239    /// to refuse the right ones.
240    NoOpPreventedInTransaction {
241        what: String,
242        /// v7.38.18 — `CREATE DATABASE … LC_COLLATE 'de_DE.utf8'` is in
243        /// every PostgreSQL bootstrap script there is, and SPG threw the
244        /// whole statement away. Being single-database makes the NAME a
245        /// no-op; it does not make the collation one, and a database
246        /// that quietly sorts by the container's `LANG` instead of the
247        /// one the script asked for is a silent difference in every
248        /// `ORDER BY` it will ever run.
249        ///
250        /// `LOCALE` and `LC_COLLATE` both land here; `LC_CTYPE` does
251        /// not, because SPG has no separate ctype.
252        collation: Option<String>,
253        /// v7.38.19 — the database's name, so `pg_database` can list one
254        /// that was created and can be connected to. It was thrown away
255        /// with the rest of the statement.
256        name: Option<String>,
257    },
258    /// v7.39 (round 696) — statements SPG performs nothing for, but whose
259    /// OPERAND PG validates before performing nothing either.
260    ///
261    /// All four used to be consumed whole by `is_dump_noise_statement`,
262    /// which meant `LOCK TABLE nosuch` and `DROP OWNED BY nosuchrole` were
263    /// ACCEPTED where PG18 errors. Accepting a statement that names
264    /// something that does not exist is the F29 shape: the caller is told
265    /// their intent was understood when the object it referred to is not
266    /// there.
267    ///
268    /// They share one variant because they share one rule — resolve the
269    /// name, refuse if absent, otherwise no-op — and four variants would be
270    /// four places for that rule to drift.
271    /// v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS] name(argtypes)[, …]`.
272    /// Consumed whole by the dump-noise list before, so `DROP AGGREGATE
273    /// nosuch(int)` reported success. PG validates every named aggregate's
274    /// EXISTENCE first (measured: a list with one unknown fails on the
275    /// unknown even when an earlier entry exists), renders the signature
276    /// with canonical type names (`int` → `integer`), and refuses to drop a
277    /// built-in (`cannot drop function sum(integer) because it is required
278    /// by the database system`). Every SPG aggregate is a built-in, so the
279    /// outcome is one of those two errors — or the IF EXISTS no-op.
280    ///
281    /// `args` holds the argument type names as written; `None` is the
282    /// `(*)` spelling.
283    DropAggregate {
284        if_exists: bool,
285        items: Vec<(String, Option<Vec<String>>)>,
286    },
287    /// v7.39 (round 750) — `ALTER ROLE|USER <name> … PASSWORD 'x' |
288    /// PASSWORD NULL`. The one attribute of the no-op family with a
289    /// SECURITY consequence: it was silently dropped (ledgered r710),
290    /// so a rotated credential never rotated. `None` = PASSWORD NULL
291    /// (the role keeps existing but can no longer password-auth).
292    AlterRolePassword {
293        name: String,
294        password: Option<String>,
295    },
296    ValidateOnly {
297        kind: ValidateOnlyKind,
298        /// The names the statement referred to. Empty means the form names
299        /// nothing (`SECURITY LABEL`, whose refusal is unconditional).
300        names: Vec<String>,
301    },
302
303    /// v7.39 (round 547) — `ALTER ROLE … SET/RESET` and
304    /// `ALTER DATABASE … SET/RESET`: the GUC defaults a session picks up
305    /// when it starts. Both used to land in the pg_dump no-op tail, so
306    /// the statement reported success and changed nothing.
307    ///
308    /// `database` / `role` are `None` for PG's oid 0 — `ALTER ROLE ALL`
309    /// sets both to None. `param` is `None` for RESET ALL. `value` is
310    /// `None` for RESET of one parameter.
311    SetDbRoleSetting(Box<SetDbRoleSettingStatement>),
312    /// v7.39 (round 288) — `SET CONSTRAINTS { ALL | <name>… }
313    /// { DEFERRED | IMMEDIATE }`. `deferred` carries the timing; the
314    /// name list is not yet honoured (ALL is what pg_dump emits and
315    /// what a circular-FK restore needs), so a named form applies to
316    /// all deferrable constraints too rather than silently doing
317    /// nothing.
318    /// v7.39 (round 308) — `SET CONSTRAINTS { ALL | name [, …] }
319    /// { DEFERRED | IMMEDIATE }`. An empty `names` is the ALL form;
320    /// otherwise the timing applies only to the constraints listed.
321    SetConstraints {
322        names: Vec<String>,
323        deferred: bool,
324    },
325
326    /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
327    /// [CASCADE | RESTRICT]`. Engine removes the matching tables
328    /// (each one) from the catalog; IF EXISTS makes the drop
329    /// idempotent. CASCADE / RESTRICT trailers parsed silently
330    /// (SPG always cascades index drops on table drop).
331    DropTable {
332        names: Vec<String>,
333        if_exists: bool,
334    },
335    /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
336    /// matching index across whichever table holds it.
337    DropIndex {
338        name: String,
339        if_exists: bool,
340        /// v7.39.7 — the table named by MySQL's `DROP INDEX i ON t`.
341        ///
342        /// MySQL keys an index name inside its table and its statement
343        /// says so; PostgreSQL keys it in the schema and has no `ON`
344        /// clause at all. `None` is the PostgreSQL form, which searches
345        /// every table for the name, and is what the MySQL dialect
346        /// refuses — as MySQL does.
347        table: Option<String>,
348    },
349    /// v7.14.0 — empty / comment-only statement. The lexer strips
350    /// `--` line comments and `/* … */` block comments (including
351    /// the MySQL conditional `/*!NNNNN … */` form) before the
352    /// parser ever sees them; a SQL chunk that contains nothing
353    /// else lands here. Engine returns CommandOk no-op so
354    /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
355    /// wrapped in conditional comments, etc.) load cleanly.
356    /// v7.39 (round 277) — SQL-level `PREPARE <name> [(type, …)] AS
357    /// <stmt>`. Session-scoped; the body keeps its `$N` placeholders
358    /// and is substituted at EXECUTE time.
359    Prepare {
360        name: String,
361        /// Declared parameter type names, in order. Empty when the
362        /// `(type, …)` list was omitted (PG infers them).
363        param_types: Vec<String>,
364        body: alloc::boxed::Box<Statement>,
365        /// The statement's own source text, which
366        /// `pg_prepared_statements.statement` reports verbatim.
367        source: String,
368    },
369    /// v7.39 (round 277) — `EXECUTE <name> [(arg, …)]`.
370    Execute {
371        name: String,
372        args: Vec<Expr>,
373    },
374    /// v7.39 (round 277) — `DEALLOCATE {<name> | ALL}`. `None` = ALL.
375    Deallocate(Option<String>),
376    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
377    /// [(kind, …)] ON <col>, … FROM <table>`. SPG records the object so
378    /// dumps restore and reflection is honest; the planner does not
379    /// consult it yet.
380    CreateStatistics {
381        name: String,
382        if_not_exists: bool,
383        /// Requested kinds as PG's single letters (`d` ndistinct,
384        /// `f` dependencies, `m` mcv). Empty = PG's default set.
385        kinds: Vec<String>,
386        columns: Vec<String>,
387        table: String,
388    },
389    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
390    DropStatistics {
391        name: String,
392        if_exists: bool,
393    },
394    /// v7.39 (round 278) — `CALL <proc>(…)`. Parses; the engine
395    /// reports that the procedure does not exist, because SPG has no
396    /// procedure catalog. Carried as a statement rather than raised at
397    /// parse time so the failure is a missing OBJECT (42883), not a
398    /// syntax error.
399    Call(String),
400    /// v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'`. Same shape:
401    /// 2PC is unavailable, which PG itself reports when
402    /// `max_prepared_transactions` is 0.
403    PrepareTransaction(String),
404    Empty,
405    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE]
406    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. The
407    /// canonical driver path for streaming large result sets (psycopg2
408    /// named cursors, JDBC setFetchSize).
409    DeclareCursor {
410        name: String,
411        /// `None` = neither keyword (PG default: backward allowed when the
412        /// plan supports it — always, for SPG's materialized cursors);
413        /// `Some(true)` = SCROLL; `Some(false)` = NO SCROLL (backward
414        /// fetch errors 55000).
415        scroll: Option<bool>,
416        /// `WITH HOLD` — survives the creating transaction's COMMIT.
417        hold: bool,
418        query: Box<Statement>,
419    },
420    /// v7.39 (round 218) — `FETCH [<direction>] [FROM|IN] <name>`.
421    FetchCursor {
422        name: String,
423        direction: CursorDirection,
424    },
425    /// v7.39 (round 218) — `MOVE [<direction>] [FROM|IN] <name>`: FETCH
426    /// without returning rows; the command tag carries the move count.
427    MoveCursor {
428        name: String,
429        direction: CursorDirection,
430    },
431    /// v7.39 (round 218) — `CLOSE <name>` / `CLOSE ALL` (`None` = ALL).
432    CloseCursor {
433        name: Option<String>,
434    },
435    /// v7.39 (round 222) — `LISTEN <channel>`: subscribe this session to
436    /// async notifications on the channel.
437    Listen(String),
438    /// v7.39 (round 222) — `NOTIFY <channel> [, '<payload>']`. Delivered at
439    /// COMMIT (PG semantics: transactional, deduplicated within the tx);
440    /// immediately under autocommit.
441    Notify {
442        channel: String,
443        payload: Option<String>,
444    },
445    /// v7.39 (round 222) — `UNLISTEN <channel>` / `UNLISTEN *` (`None` = *).
446    Unlisten(Option<String>),
447    /// `COPY table [(cols)] TO STDOUT` — the engine renders the
448    /// visible rows in COPY text format (tab-separated, `\N`
449    /// nulls, backslash escapes) as a single-text-column result
450    /// set; the wire layer streams CopyData from it.
451    CopyTo {
452        table: String,
453        columns: Option<Vec<String>>,
454        /// v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT`: an
455        /// arbitrary SELECT/VALUES/CTE (a whole [`Statement`], so set-ops and
456        /// VALUES ride through unchanged) whose result set is streamed in COPY
457        /// format. `Some` overrides `table`/`columns` (which are empty then);
458        /// `None` is the classic `COPY <table> …` shape.
459        query: Option<Box<Statement>>,
460        /// v7.37.x — `WITH (FORMAT csv, HEADER, DELIMITER, NULL, QUOTE)`
461        /// and the legacy `WITH CSV HEADER …` spelling. Default =
462        /// text format, no header (bare `COPY … TO STDOUT`).
463        options: CopyOptions,
464    },
465    /// v7.39 (round 249) — `COPY table [(cols)] FROM '<path>' [(opts)]`.
466    /// The engine is no_std and cannot read the file itself: the host
467    /// (embedded / server / tooling) reads the path and hands the bytes to
468    /// `Engine::copy_from_buffer`. Dispatching this statement straight to
469    /// the engine reports that contract.
470    CopyFromFile {
471        table: String,
472        columns: Option<Vec<String>>,
473        path: String,
474        options: CopyOptions,
475    },
476    /// v7.39 (round 249/252) — `COPY <table> [(cols)] TO '<file>'` (and
477    /// the `COPY (<query>) TO '<file>'` form). The engine is no_std and
478    /// cannot write the file itself: the host renders the payload via
479    /// `Engine::copy_to_buffer` and writes the path.
480    CopyToFile {
481        table: String,
482        columns: Option<Vec<String>>,
483        query: Option<Box<Statement>>,
484        path: String,
485        options: CopyOptions,
486    },
487    Select(SelectStatement),
488    CreateTable(CreateTableStatement),
489    /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
490    /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
491    /// no-op so PG dumps that include extension declarations
492    /// (notably `pgvector`) load against SPG without splitting
493    /// init scripts. mailrs migration follow-up F3.
494    CreateExtension(String),
495    /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
496    /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
497    /// the engine executes it at top level (mailrs round-10
498    /// A.2). Pre-v7.16.2 the parser discarded the body and the
499    /// engine returned CommandOk — a SEV-1 silent no-op that
500    /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
501    /// $$` idempotent migrations into invisible no-ops.
502    DoBlock(PlPgSqlBlock),
503    CreateIndex(CreateIndexStatement),
504    Insert(InsertStatement),
505    /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
506    Update(UpdateStatement),
507    /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
508    Delete(DeleteStatement),
509    /// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ `MERGE` statement.
510    /// `MERGE INTO target [alias] USING source [alias] ON cond
511    /// WHEN MATCHED [AND cond] THEN { UPDATE SET … | DELETE | DO NOTHING }
512    /// WHEN NOT MATCHED [AND cond] THEN { INSERT (cols) VALUES (vals) | DO NOTHING }
513    /// [WHEN …]`. SPG v7.17 supports table-based source (subquery
514    /// source is a follow-up); BY SOURCE / BY TARGET and RETURNING
515    /// are also follow-ups.
516    Merge(MergeStatement),
517    /// v7.39 (round 169) — `VACUUM [(opts)] [FULL|FREEZE|VERBOSE|ANALYZE]
518    /// [<table>]`. Was a parse-time no-op from the pre-MVCC era; with the
519    /// in-place MVCC gate ON, tombstoned versions are REAL bloat and a
520    /// customer's manual VACUUM must actually reclaim. `analyze` mirrors
521    /// the `VACUUM ANALYZE` spelling.
522    Vacuum {
523        table: Option<String>,
524        analyze: bool,
525    },
526    /// `BEGIN` / `START TRANSACTION` — with an optional explicit
527    /// `ISOLATION LEVEL …` mode (`None` = use the session default). PG
528    /// applies the level for the duration of this transaction only.
529    Begin(TransactionModes),
530    Commit,
531    Rollback,
532    /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
533    /// stack so a later `ROLLBACK TO <name>` can undo just the work
534    /// since this point.
535    Savepoint(String),
536    /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
537    /// named savepoint and discard later savepoints. Does not end the
538    /// transaction.
539    RollbackToSavepoint(String),
540    /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
541    /// rolling back. Keeps the work done since then.
542    ReleaseSavepoint(String),
543    /// `SHOW TABLES` — return the list of tables in the catalog.
544    ShowTables,
545    /// v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES` /
546    /// `SHOW SCHEMAS`. SPG is single-database; the executor
547    /// returns the canonical MySQL set so the mysql / MariaDB
548    /// client populates its database selector.
549    ShowDatabases,
550    /// v7.39.2 — MySQL `USE <db>`.
551    ///
552    /// It parsed as `Empty` and did nothing at all, so `USE myapp;
553    /// SELECT DATABASE()` answered the same constant it answered before
554    /// — measured against MySQL 9.7.2, which answers `myapp`. SPG serves
555    /// ONE database and answers to any name (see `CREATE DATABASE`), so
556    /// this does not switch catalogs; it records the NAME, which is the
557    /// half a client can observe and the half the PostgreSQL wire has
558    /// tracked since v7.39 (`current_database()` names what the startup
559    /// message asked for).
560    UseDatabase(String),
561    /// v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE TABLE <t>`
562    /// returns a 2-column row `(Table, "Create Table")` carrying
563    /// the synthesized DDL. mysqldump emits this for every
564    /// table at scrape time.
565    ShowCreateTable(String),
566    /// v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES FROM <t>`
567    /// (also `SHOW INDEX`, `SHOW KEYS`).
568    ShowIndexes(String),
569    /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS`.
570    ShowStatus,
571    /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW VARIABLES`.
572    ShowVariables,
573    /// r1067 — MySQL `SHOW VARIABLES LIKE 'pattern'` (sysbench-tpcc
574    /// probes isolation with it at connect).
575    ShowVariablesLike(String),
576    /// v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
577    ShowProcesslist,
578    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
579    /// pgbouncer sends `DISCARD ALL` between pooled client sessions to make
580    /// the connection look brand new to the next client; it used to be
581    /// swallowed as dump noise, so nothing was discarded.
582    Discard(DiscardTarget),
583    /// v7.39 (round 318, V51) — MySQL `KILL [CONNECTION | QUERY] <expr>`.
584    /// The id is an expression because MariaDB accepts one
585    /// (`KILL connection_id()` is the documented way to drop your own
586    /// connection). `query_only` is the `QUERY` form: stop the target's
587    /// running statement but leave it connected.
588    Kill {
589        query_only: bool,
590        id: Box<Expr>,
591    },
592    /// `SHOW COLUMNS FROM <table>` — return one row per column with
593    /// its declared name / type / nullability.
594    ShowColumns(String),
595    /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
596    /// Role is optional; defaults to `readonly` when omitted.
597    CreateUser(CreateUserStatement),
598    /// `DROP USER 'name'` (v4.1). v7.39 (read01 round 58) — `IF EXISTS` is
599    /// carried through: PG skips with a NOTICE rather than erroring.
600    DropUser {
601        name: String,
602        if_exists: bool,
603    },
604    /// v7.39 (RLS) — `SET ROLE { name | NONE | DEFAULT }` / `RESET ROLE`.
605    /// `Some(name)` switches the session's effective role (drives
606    /// `current_user` and RLS enforcement); `None` resets to the login
607    /// identity (the Admin superuser).
608    SetRole(Option<String>),
609    /// v7.39 (read01 round 57) — `GRANT <privs> ON <object> TO <roles>`.
610    Grant(GrantStatement),
611    /// v7.39 (read01 round 57) — `REVOKE [GRANT OPTION FOR] <privs> ON
612    /// <object> FROM <roles>`.
613    Revoke(GrantStatement),
614    /// v7.39 (RLS) — `CREATE POLICY name ON table …`.
615    CreatePolicy(CreatePolicyStatement),
616    /// v7.39 (RLS) — `ALTER POLICY name ON table …`.
617    AlterPolicy(AlterPolicyStatement),
618    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
619    DropPolicy(DropPolicyStatement),
620    /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
621    ShowUsers,
622    /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
623    /// single-column text table describing the rewritten plan tree
624    /// for `inner`. `analyze` triggers an actual exec to attach
625    /// observed row counts and elapsed micros to each node.
626    Explain(ExplainStatement),
627    /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
628    /// Synchronous rebuild of an NSW index. With the optional
629    /// encoding clause, every stored cell at the indexed column is
630    /// also re-encoded through `coerce_value` before the new graph
631    /// builds.
632    AlterIndex(AlterIndexStatement),
633    /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
634    /// The only setting in v6.7.2 is `hot_tier_bytes`, which
635    /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
636    /// for the named table.
637    AlterTable(AlterTableStatement),
638    /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
639    /// The catalog row lives in `spg_publications`. Publisher-side
640    /// WAL filtering arrives in v6.1.5.
641    CreatePublication(CreatePublicationStatement),
642    /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
643    /// no-op when the publication does not exist.
644    DropPublication {
645        name: String,
646        /// v7.39 (round 754, F31-B4) — `IF EXISTS` quietly skips a
647        /// missing publication; the bare form refuses with PG's
648        /// sentence (PG18-measured — the old "silent no-op" note on
649        /// the executor was wrong).
650        if_exists: bool,
651    },
652    /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
653    /// publication ordered by name with `(name, scope_summary,
654    /// table_count)` columns. The scope summary is the human-
655    /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
656    /// TABLES EXCEPT …`; `table_count` is `NULL` for the
657    /// `AllTables` scope and the table-list length otherwise.
658    ShowPublications,
659    /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
660    /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
661    /// in `spg_subscriptions`; when the subscription is
662    /// `enabled = true` (default) the server spawns a
663    /// background worker that connects to `conn` and drains the
664    /// requested publication(s) into the local engine.
665    CreateSubscription(CreateSubscriptionStatement),
666    /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
667    /// PUBLICATION, silent no-op when absent. Stops the
668    /// associated worker thread before removing the row.
669    DropSubscription {
670        name: String,
671        /// v7.39 (round 754, F31-B4) — same contract as
672        /// [`Statement::DropPublication`].
673        if_exists: bool,
674    },
675    /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
676    /// subscription ordered by name with `(name, conn_str,
677    /// publications, enabled, last_received_pos)`.
678    ShowSubscriptions,
679    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
680    /// Blocks until the local server's apply position reaches
681    /// `<pos>` or `<ms>` elapses. Server-layer command: the
682    /// engine refuses it (`EngineError::Unsupported`) since
683    /// `lag_state` lives in `spg-server`'s `ServerState`.
684    WaitForWalPosition {
685        pos: u64,
686        /// `None` → wait forever; `Some(ms)` → return after `ms`
687        /// milliseconds even if the target isn't reached.
688        timeout_ms: Option<u64>,
689    },
690    /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
691    /// table; `ANALYZE <name>` re-stats just one. Populates
692    /// `spg_statistic` with per-column null_frac + n_distinct +
693    /// 100-bucket equi-depth histogram.
694    Analyze(Option<String>),
695    /// v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
696    /// PostgreSQL has only `ALTER TABLE … RENAME TO`, so this spelling
697    /// had nowhere to go; it is what a MySQL migration writes.
698    RenameTables(Vec<(String, String)>),
699    /// v7.39 (round 535) — `REINDEX { INDEX | TABLE | SCHEMA | DATABASE
700    /// | SYSTEM } [CONCURRENTLY] <name>` and `CLUSTER [VERBOSE]
701    /// [<table> [USING <index>]]`.
702    ///
703    /// SPG has neither index bloat nor a clustering order to rebuild, so
704    /// the work is a no-op — but PG VALIDATES the target, and both were
705    /// swallowed at parse time, so `REINDEX TABLE typo` reported success.
706    /// The name is carried now so the engine can say what PG says.
707    Maintain {
708        kind: MaintainKind,
709        /// `REINDEX … CONCURRENTLY`. Carried for the same reason as
710        /// [`CreateIndexStatement::concurrently`]: PG bars the
711        /// CONCURRENTLY form inside a transaction block and allows the
712        /// plain one.
713        concurrently: bool,
714        /// `None` for the whole-database forms, which name nothing.
715        target: Option<String>,
716    },
717    /// v7.37.17 (17.6 sibling) — `TRUNCATE [TABLE] [ONLY] <name>
718    /// [, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE |
719    /// RESTRICT]`. Clears every row from each named table. SPG's
720    /// SEQUENCE identity is per-table; RESTART IDENTITY reinitializes
721    /// the associated sequence to its starting value. CASCADE
722    /// currently walks direct FK-referring tables and truncates
723    /// them too (PG's semantics). The ONLY modifier (skip partitions)
724    /// and RESTRICT (default) are accepted with no effect since
725    /// SPG's declarative partitions are always truncated together.
726    Truncate {
727        tables: Vec<String>,
728        restart_identity: bool,
729        cascade: bool,
730        /// v7.39 (round 647) — `TRUNCATE ONLY t`. Absorbed as a no-op
731        /// since v7.14 on the reasoning that SPG's children are separate
732        /// relations a truncate does not descend into. Same reasoning
733        /// round 621 applied to `FROM ONLY`, and it stopped being true
734        /// for the same reason: measured, `TRUNCATE <inheritance parent>`
735        /// leaves the children's rows where PG empties them, and
736        /// `TRUNCATE ONLY <partitioned parent>` is silently accepted
737        /// where PG refuses it outright.
738        only: bool,
739    },
740    /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
741    /// BTree-cold indices and merges small cold-tier segments
742    /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
743    /// 4 MiB) into a single larger segment per (table, index).
744    /// `WHERE` predicate filtering on which tables to compact is
745    /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
746    /// v6.7.3 only supports the bare form.
747    CompactColdSegments,
748    /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
749    /// parameter on the engine; v7.12.1 honours
750    /// `default_text_search_config` (consumed by `to_tsvector` /
751    /// `plainto_tsquery` family when called without an explicit
752    /// config arg). All other names are accepted as a no-op so PG
753    /// dumps with `SET client_encoding`, `SET search_path` etc.
754    /// load cleanly.
755    SetParameter {
756        name: String,
757        value: SetValue,
758        /// v7.38 (read01 P3.19) — `SET LOCAL` scopes the change to the
759        /// current transaction; the engine saves the prior value and
760        /// restores it at COMMIT / ROLLBACK. Plain `SET` (and `SET
761        /// SESSION`) leave this false and persist for the session.
762        local: bool,
763    },
764    /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
765    /// multi-assignment (mysqldump preamble uses
766    /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
767    /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
768    /// source order. Pairs whose LHS is a MySQL session/user
769    /// variable (`@VAR` / `@@VAR`) are recorded with the raw
770    /// name so the engine can ignore them; pairs whose LHS is
771    /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
772    /// go through the regular `set_session_param` path.
773    SetParameterList(Vec<(String, SetValue)>),
774    /// v7.39 (round 430) — MySQL's USER-defined variables:
775    /// `SET @x = 5, @s := CONCAT('a','b')`. Distinct from
776    /// [`Self::SetParameter`] (a `@@`-style engine/session setting) in
777    /// every way that matters: the value is an arbitrary EXPRESSION, the
778    /// name lives in its own per-session namespace, and reading an unset
779    /// one answers NULL rather than raising. `:=` and `=` are the same
780    /// assignment here.
781    ///
782    /// Before this the parser stripped every `@`, so `@x` and `@@x` were
783    /// the same node: `SET @x = 5` silently landed in the session-parameter
784    /// store where nothing could read it back, and `SELECT @x` failed with
785    /// "Unknown system variable".
786    /// v7.39 (round 554) — `SET @a = …, SETTING = …`.
787    ///
788    /// `settings` is the trailing half a mysqldump preamble writes:
789    /// `SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'`
790    /// saves a value and changes it in one statement. The parser used
791    /// to refuse the mixture outright, so no mysqldump could be
792    /// restored past its preamble.
793    SetUserVars(Vec<(String, Expr)>, Vec<(String, Expr)>),
794    /// v7.38 轴 4 — `SET [SESSION] TRANSACTION ISOLATION LEVEL …`
795    /// (plus optional READ ONLY / READ WRITE / DEFERRABLE clauses
796    /// silently accepted). PG-standard surface for picking an
797    /// isolation level. Engine tracks the value on
798    /// `Engine::current_isolation_level()`; actual MVCC / SSI
799    /// semantics implementation lands separately. PG itself maps
800    /// READ UNCOMMITTED to READ COMMITTED; SPG mirrors that —
801    /// effectively every level reads as READ COMMITTED in v7.37.8.
802    SetTransaction {
803        modes: TransactionModes,
804    },
805    /// v7.38 轴 4 — `SHOW <param>` returns a 1-column 1-row result
806    /// with the parameter's current value as TEXT. Today the only
807    /// recognised param is `transaction_isolation`; further
808    /// surfaces (`search_path`, `application_name`, …) land as the
809    /// session-parameter inventory grows.
810    ShowParameter(String),
811    /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
812    /// to its default. No-op for parameters SPG does not track.
813    ResetParameter(Option<String>),
814    /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
815    /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
816    /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
817    /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
818    /// languages parse but error at exec time with a clear
819    /// unsupported message.
820    CreateFunction(CreateFunctionStatement),
821    /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
822    /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
823    /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
824    /// triggers and column-list / WHEN clauses are out of scope
825    /// for v7.12.4.
826    CreateTrigger(CreateTriggerStatement),
827    /// v7.39 (round 139) — `CREATE RULE name AS ON event TO table [WHERE cond]
828    /// DO [ALSO|INSTEAD] { NOTHING | command }` query-rewrite rule.
829    CreateRule(CreateRuleStatement),
830    /// v7.39 (round 139) — `DROP RULE [IF EXISTS] name ON table`.
831    DropRule {
832        name: String,
833        table: String,
834        if_exists: bool,
835    },
836    /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
837    /// no-op when missing if `IF EXISTS` is set.
838    DropTrigger {
839        name: String,
840        table: String,
841        if_exists: bool,
842    },
843    /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
844    /// DROP TRIGGER but global (no table scope).
845    DropFunction {
846        name: String,
847        /// v7.39 (read01 round 62) — the argument TYPES, when the statement gave
848        /// them: `DROP FUNCTION f(int)` drops that overload only. `None` = no
849        /// argument list, which PG accepts only when the name is unambiguous.
850        args: Option<Vec<String>>,
851        if_exists: bool,
852    },
853    /// v7.17.0 — `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] name
854    /// [AS data_type]
855    /// [INCREMENT [BY] n]
856    /// [MINVALUE n | NO MINVALUE]
857    /// [MAXVALUE n | NO MAXVALUE]
858    /// [START [WITH] n]
859    /// [CACHE n]
860    /// [[NO] CYCLE]
861    /// [OWNED BY {table.col | NONE}]`.
862    /// Closes the round-7+ silent-no-op SEQUENCE story so pg_dump
863    /// emits + nextval/currval/setval downstream all work.
864    CreateSequence(CreateSequenceStatement),
865    /// v7.17.0 — `ALTER SEQUENCE [IF EXISTS] name <options>` with
866    /// the same option grammar as CREATE SEQUENCE, plus
867    /// `RESTART [WITH n]` and `OWNED BY ...` re-attach.
868    AlterSequence(AlterSequenceStatement),
869    /// v7.17.0 — `DROP SEQUENCE [IF EXISTS] name [, name…]
870    /// [CASCADE | RESTRICT]`. CASCADE / RESTRICT trailers parsed
871    /// silently (no FK on sequences).
872    DropSequence {
873        names: Vec<String>,
874        if_exists: bool,
875    },
876    /// v7.17.0 Phase 1.2 — `CREATE [OR REPLACE] [TEMPORARY] VIEW
877    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …>`. Closes the
878    /// silent-no-op VIEW story from the v7.17 customer-readiness
879    /// audit: pre-v7.17 SPG parsed CREATE VIEW as Statement::Empty
880    /// so any downstream `SELECT FROM v` errored with table-not-
881    /// found. The view body is stored verbatim; SELECT FROM <v>
882    /// rewrites at exec-time by prepending the view body as a
883    /// synthetic CTE.
884    CreateView(CreateViewStatement),
885    /// v7.17.0 Phase 1.2 — `DROP VIEW [IF EXISTS] name [, name…]
886    /// [CASCADE | RESTRICT]`. Removes the matching view from the
887    /// catalog; CASCADE/RESTRICT parsed silently.
888    DropView {
889        names: Vec<String>,
890        if_exists: bool,
891    },
892    /// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW [IF NOT
893    /// EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
894    /// Closes the silent-no-op MATERIALIZED VIEW story. Storage
895    /// model: the materialised result lives as a regular table
896    /// with the matching name + a parallel
897    /// `materialized_views` registry mapping name → body source
898    /// (used by REFRESH).
899    CreateMaterializedView(CreateMaterializedViewStatement),
900    /// v7.17.0 Phase 1.3 — `REFRESH MATERIALIZED VIEW name [WITH
901    /// [NO] DATA]`. Re-runs the stored body and replaces the
902    /// cached rows. `WITH NO DATA` truncates without re-running.
903    RefreshMaterializedView {
904        name: String,
905        with_data: bool,
906    },
907    /// v7.17.0 Phase 1.3 — `DROP MATERIALIZED VIEW [IF EXISTS]
908    /// name [, name…] [CASCADE | RESTRICT]`. Drops both the
909    /// backing table and the source registry entry.
910    DropMaterializedView {
911        names: Vec<String>,
912        if_exists: bool,
913    },
914    /// v7.17.0 Phase 1.4 — `CREATE TYPE name AS ENUM ('a', 'b',
915    /// …)`. Closes the silent-no-op CREATE TYPE story so PG
916    /// dumps that declare enum types load with real constraints
917    /// instead of becoming free-form TEXT. Future kinds
918    /// (composite / range / domain) extend the inner `kind`
919    /// enum.
920    CreateType(CreateTypeStatement),
921    /// v7.37 D.55 — `ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
922    /// [{BEFORE | AFTER} 'existing']`. Extends an enum's label list so
923    /// enum evolution stops being a silent no-op. `position` is
924    /// `Some((is_before, anchor))`.
925    /// v7.39 (read01 round 49) — `ALTER TYPE t RENAME VALUE 'old' TO 'new'`.
926    /// Used to be swallowed by the ALTER TYPE no-op tail, so the rename was
927    /// accepted and silently ignored.
928    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
929    /// Used to be swallowed as dump noise, so a comment was accepted and lost
930    /// (and obj_description / col_description always returned NULL).
931    /// `kind` is lowercase ("table" / "column" / "index" / …); for a column
932    /// `name` is the dotted `table.column`. `comment: None` = `IS NULL` = remove.
933    CommentOn {
934        kind: String,
935        name: String,
936        comment: Option<String>,
937    },
938    AlterTypeRenameValue {
939        type_name: String,
940        old: String,
941        new: String,
942    },
943    AlterTypeAddValue {
944        type_name: String,
945        label: String,
946        if_not_exists: bool,
947        position: Option<(bool, String)>,
948    },
949    /// v7.17.0 Phase 1.4 — `DROP TYPE [IF EXISTS] name [, name…]
950    /// [CASCADE | RESTRICT]`. Removes the matching enum/domain
951    /// from the catalog.
952    DropType {
953        names: Vec<String>,
954        if_exists: bool,
955    },
956    /// v7.17.0 Phase 1.5 — `CREATE DOMAIN name AS base_type
957    /// [DEFAULT expr] [NOT NULL | NULL] [CHECK (expr)]*`.
958    /// A DOMAIN is a named CHECK-constrained alias over a built-
959    /// in type. The CHECK + NOT NULL + DEFAULT clauses apply to
960    /// every column declared with the domain. Closes the
961    /// silent-no-op CREATE DOMAIN story so PG dumps that ship
962    /// validated identifier types (email, positive_int, …) keep
963    /// their guarantees.
964    CreateDomain(CreateDomainStatement),
965    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`. Every form was
966    /// previously swallowed by the catch-all DDL arm: the statement
967    /// reported success and did nothing, so a migration that dropped a
968    /// constraint kept rejecting the data it had just been told to
969    /// accept.
970    AlterDomain {
971        name: String,
972        action: AlterDomainAction,
973    },
974    /// v7.17.0 Phase 1.5 — `DROP DOMAIN [IF EXISTS] name
975    /// [, name…] [CASCADE | RESTRICT]`. Removes the matching
976    /// domain from the catalog.
977    DropDomain {
978        names: Vec<String>,
979        if_exists: bool,
980    },
981    /// v7.17.0 Phase 1.6 — `CREATE SCHEMA [IF NOT EXISTS]
982    /// name [AUTHORIZATION user]`. SPG is single-database;
983    /// schemas are tracked as a namespace registry so pg_dump
984    /// multi-schema declarations land cleanly and `SELECT *
985    /// FROM information_schema.schemata` returns real entries.
986    /// Schema-qualified `schema.table` references still strip
987    /// the prefix at lookup time per PG (schemas are not
988    /// isolation boundaries in v7.17 — see project-next-docket
989    /// for the v7.18+ isolation tracking).
990    CreateSchema {
991        name: String,
992        if_not_exists: bool,
993    },
994    /// v7.17.0 Phase 1.6 — `DROP SCHEMA [IF EXISTS] name
995    /// [, name…] [CASCADE | RESTRICT]`. Removes the schema
996    /// from the registry; built-in `public` / `pg_catalog` /
997    /// `information_schema` cannot be dropped.
998    DropSchema {
999        names: Vec<String>,
1000        if_exists: bool,
1001    },
1002}
1003
1004/// v7.39 (round 260) — the `ALTER DOMAIN` actions SPG implements.
1005#[derive(Debug, Clone, PartialEq)]
1006pub enum AlterDomainAction {
1007    AddConstraint { name: Option<String>, check: Expr },
1008    DropConstraint { name: String, if_exists: bool },
1009    SetDefault(Expr),
1010    DropDefault,
1011    SetNotNull,
1012    DropNotNull,
1013    RenameTo(String),
1014}
1015
1016/// v7.17.0 Phase 1.5 — `CREATE DOMAIN` AST.
1017#[derive(Debug, Clone, PartialEq)]
1018pub struct CreateDomainStatement {
1019    pub name: String,
1020    /// Base type for the domain (one of the built-in
1021    /// `ColumnTypeName` variants).
1022    pub base_type: ColumnTypeName,
1023    /// v7.39 (round 259) — `CREATE DOMAIN child AS parent …` where
1024    /// `parent` is itself a DOMAIN. The parser already captured the
1025    /// unknown type name; it just was not carried here, so the parent's
1026    /// CHECK constraints were invisible and a value violating them was
1027    /// silently accepted. `base_type` still holds the ultimate scalar
1028    /// type, which is what the storage tier stores.
1029    pub base_domain: Option<String>,
1030    /// Optional `DEFAULT <expr>`. Resolved at engine-side
1031    /// CREATE TABLE time when a column is bound to this domain.
1032    pub default: Option<Expr>,
1033    /// `NOT NULL` from the domain definition. Engine ORs this
1034    /// with the column-level nullability so the strictest of the
1035    /// two wins (i.e. the column is non-nullable if either side
1036    /// says so).
1037    pub not_null: bool,
1038    /// Zero-or-more `CHECK (expr)` predicates. Each one is
1039    /// enforced as part of the column's CHECK list at INSERT /
1040    /// UPDATE time, with `VALUE` substituted for the column's
1041    /// current cell value.
1042    pub checks: Vec<Expr>,
1043}
1044
1045/// v7.17.0 Phase 1.4 — `CREATE TYPE` AST.
1046#[derive(Debug, Clone, PartialEq, Eq)]
1047pub struct CreateTypeStatement {
1048    pub name: String,
1049    pub kind: TypeKind,
1050}
1051
1052/// v7.17.0 Phase 1.4 — flavour of the new type. Only ENUM is
1053/// implemented; the variant set is open so Phase 1.5 (DOMAIN)
1054/// and later (COMPOSITE, RANGE) can land without an AST shape
1055/// migration.
1056///
1057/// v7.37.x (ζ-B Phase 1 composite accept) — added Composite for
1058/// `CREATE TYPE name AS (field_name field_type, …)`. Phase 1
1059/// stores the field list in the catalog so PG dumps that emit
1060/// `CREATE TYPE … AS (…)` don't error out; using a composite type
1061/// as a column type lands in Phase 2 (Value::Composite encoding +
1062/// ROW() literal + field-access syntax).
1063#[derive(Debug, Clone, PartialEq, Eq)]
1064pub enum TypeKind {
1065    /// `AS ENUM ('a', 'b', …)`. Order is preserved (PG enum
1066    /// labels are ordered).
1067    Enum { labels: Vec<String> },
1068    /// `AS (field_name field_type, …)`. Order matters; PG
1069    /// composite literals are positional.
1070    Composite {
1071        fields: Vec<(String, ColumnTypeName)>,
1072        /// v7.39 (round 264) — parallel to `fields`: the raw type NAME
1073        /// when a field's type is not a builtin (i.e. another composite).
1074        /// The parser already captures it; without carrying it here a
1075        /// nested composite field resolved to the Text placeholder and
1076        /// the inner record never became a record.
1077        field_user_types: Vec<Option<String>>,
1078    },
1079}
1080
1081/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
1082/// a string literal, an identifier (often a config name), an
1083/// integer/float, or the bare `DEFAULT` keyword.
1084#[derive(Debug, Clone, PartialEq)]
1085pub enum SetValue {
1086    String(String),
1087    Ident(String),
1088    Number(String),
1089    Default,
1090    /// v7.40.11 — MySQL only. `SET character_set_results = NULL` is the
1091    /// second statement Connector/J sends on every connection, and it
1092    /// means "send results in the column's own charset, do not
1093    /// transcode". PostgreSQL 18.6 answers `syntax error at or near
1094    /// "NULL"` for the same shape (measured), so the parser produces
1095    /// this variant only for a MySQL-dialect session.
1096    Null,
1097}
1098
1099/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1100/// at parse time and tracks the selected value on the engine. The
1101/// actual semantic differentiation (REPEATABLE READ snapshot,
1102/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1103/// today every level reads as effective READ COMMITTED (which is
1104/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1105/// READ COMMITTED). Default = `ReadCommitted`.
1106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1107pub enum IsolationLevel {
1108    ReadUncommitted,
1109    #[default]
1110    ReadCommitted,
1111    RepeatableRead,
1112    Serializable,
1113}
1114
1115impl IsolationLevel {
1116    /// Canonical PG-style display name, as `SHOW transaction_isolation`
1117    /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1118    /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1119    /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1120    /// `read uncommitted`) and only BEHAVES as read committed; the old
1121    /// fold renamed the label too.
1122    /// v7.39 — the MySQL display name, which is NOT the PG one: MySQL
1123    /// hyphenates and upper-cases. Measured on MySQL 9.7.2 by setting
1124    /// each level and reading `@@transaction_isolation` back:
1125    /// `READ-UNCOMMITTED` / `READ-COMMITTED` / `REPEATABLE-READ` /
1126    /// `SERIALIZABLE` (the last has no hyphen because it is one word).
1127    ///
1128    /// This exists so the two MySQL surfaces cannot drift: both
1129    /// `SHOW VARIABLES` and `@@transaction_isolation` used to carry
1130    /// their own hard-coded literal, and the literals disagreed —
1131    /// one said `REPEATABLE-READ` while the engine ran read committed.
1132    /// v7.39 — parse what `default_transaction_isolation` holds. PG
1133    /// accepts the SQL spellings and stores them lower-cased with a
1134    /// space; anything else is not a level this understands and the
1135    /// caller keeps its own default rather than guessing.
1136    #[must_use]
1137    pub fn from_pg_name(name: &str) -> Option<Self> {
1138        match name.trim().to_ascii_lowercase().as_str() {
1139            "read uncommitted" => Some(Self::ReadUncommitted),
1140            "read committed" => Some(Self::ReadCommitted),
1141            "repeatable read" => Some(Self::RepeatableRead),
1142            "serializable" => Some(Self::Serializable),
1143            _ => None,
1144        }
1145    }
1146
1147    #[must_use]
1148    pub fn as_mysql_str(self) -> &'static str {
1149        match self {
1150            Self::ReadUncommitted => "READ-UNCOMMITTED",
1151            Self::ReadCommitted => "READ-COMMITTED",
1152            Self::RepeatableRead => "REPEATABLE-READ",
1153            Self::Serializable => "SERIALIZABLE",
1154        }
1155    }
1156
1157    pub fn as_pg_str(self) -> &'static str {
1158        match self {
1159            Self::ReadUncommitted => "read uncommitted",
1160            Self::ReadCommitted => "read committed",
1161            Self::RepeatableRead => "repeatable read",
1162            Self::Serializable => "serializable",
1163        }
1164    }
1165}
1166
1167impl core::fmt::Display for IsolationLevel {
1168    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1169        f.write_str(self.as_pg_str())
1170    }
1171}
1172
1173/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1174/// single fixed-shape DDL; the WITH-clause options PG supports
1175/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1176/// scope for v6.1.4 — `enabled` defaults to true and there are
1177/// no other knobs to set in v6.1.x.
1178#[derive(Debug, Clone, PartialEq, Eq)]
1179pub struct CreateSubscriptionStatement {
1180    pub name: String,
1181    /// Connection string in PG keyword=value form (e.g.
1182    /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1183    /// `host` and `port` fields; the rest is reserved for
1184    /// future v6.1.x options.
1185    pub conn_str: String,
1186    /// One or more publications on the remote side. Order is
1187    /// preserved verbatim from the DDL; the worker requests them
1188    /// in this order. v6.1.4 records the list; v6.1.5
1189    /// publisher-side filtering enforces it.
1190    pub publications: Vec<String>,
1191}
1192
1193/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1194#[derive(Debug, Clone, PartialEq, Eq)]
1195pub struct CreateSequenceStatement {
1196    pub name: String,
1197    pub if_not_exists: bool,
1198    pub temporary: bool,
1199    /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1200    pub data_type: Option<SequenceDataType>,
1201    pub options: SequenceOptions,
1202}
1203
1204/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1206pub enum SequenceDataType {
1207    SmallInt,
1208    Int,
1209    BigInt,
1210}
1211
1212/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1213/// All fields are optional. `min_value`/`max_value` carry
1214/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1215#[derive(Debug, Clone, Default, PartialEq, Eq)]
1216pub struct SequenceOptions {
1217    pub increment: Option<i64>,
1218    pub min_value: Option<SeqBound>,
1219    pub max_value: Option<SeqBound>,
1220    pub start: Option<i64>,
1221    /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1222    /// RESTART, `Some(Some(n))` = RESTART WITH n.
1223    pub restart: Option<Option<i64>>,
1224    pub cache: Option<i64>,
1225    pub cycle: Option<bool>,
1226    pub owned_by: Option<SequenceOwnedBy>,
1227}
1228
1229/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1231pub enum SeqBound {
1232    Value(i64),
1233    NoBound,
1234}
1235
1236/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1237#[derive(Debug, Clone, PartialEq, Eq)]
1238pub enum SequenceOwnedBy {
1239    None,
1240    Column { table: String, column: String },
1241}
1242
1243/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1244#[derive(Debug, Clone, PartialEq)]
1245pub struct CreateMaterializedViewStatement {
1246    pub name: String,
1247    pub if_not_exists: bool,
1248    /// Optional `(col, col, …)` rename list. Applies to the
1249    /// backing table at CREATE / REFRESH time.
1250    pub columns: Vec<String>,
1251    /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1252    /// the cached rows.
1253    pub body: SelectStatement,
1254    /// `WITH DATA` (default) = materialise the rows at CREATE
1255    /// time. `WITH NO DATA` = create an empty backing table;
1256    /// callers must REFRESH before SELECT returns rows.
1257    pub with_data: bool,
1258    /// v7.38 (read01 P6.49) — when true this node came from
1259    /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1260    /// executor creates a plain table and does NOT register it in the
1261    /// materialized-view registry (no REFRESH semantics).
1262    pub as_plain_table: bool,
1263    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1264    /// meaningful together with `as_plain_table`; the executor puts the
1265    /// resulting table in the creating session's namespace.
1266    pub temporary: bool,
1267}
1268
1269/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1270/// auto-updatable view. `Cascaded` is PG's default when the bare
1271/// `WITH CHECK OPTION` is written.
1272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1273pub enum ViewCheckOption {
1274    Local,
1275    Cascaded,
1276}
1277
1278/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1279#[derive(Debug, Clone, PartialEq)]
1280pub struct CreateViewStatement {
1281    pub name: String,
1282    pub or_replace: bool,
1283    pub if_not_exists: bool,
1284    pub temporary: bool,
1285    /// Optional `(col, col, …)` rename list. When non-empty,
1286    /// these override the body's projected column names per-
1287    /// position at SELECT-from-view time.
1288    pub columns: Vec<String>,
1289    /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1290    /// time to materialise the view as a synthetic CTE.
1291    pub body: SelectStatement,
1292    /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1293    /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1294    /// 44000). `None` = no check option.
1295    pub check_option: Option<ViewCheckOption>,
1296}
1297
1298/// v7.17.0 — `ALTER SEQUENCE` AST node.
1299#[derive(Debug, Clone, PartialEq, Eq)]
1300pub struct AlterSequenceStatement {
1301    pub name: String,
1302    pub if_exists: bool,
1303    pub options: SequenceOptions,
1304    /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1305    /// instead of `options`; the two forms are mutually exclusive in PG.
1306    pub rename_to: Option<String>,
1307}
1308
1309/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1310/// the [`PublicationScope`] shape. v6.1.2 only accepted
1311/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1312/// variants by flipping the parser gate (no AST migration).
1313#[derive(Debug, Clone, PartialEq, Eq)]
1314pub struct CreatePublicationStatement {
1315    pub name: String,
1316    pub scope: PublicationScope,
1317}
1318
1319/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1320/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1321/// variants — the on-disk shape, snapshot serialisation, and the
1322/// AST round-trip Display path were already in place in v6.1.2
1323/// so this is a parser-only widening.
1324#[derive(Debug, Clone, PartialEq, Eq)]
1325pub enum PublicationScope {
1326    AllTables,
1327    ForTables(Vec<String>),
1328    AllTablesExcept(Vec<String>),
1329    /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1330    /// (PG 15+). AST-only: the executor folds `public` to
1331    /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1332    /// and refuses any other schema with PG's sentence, so the
1333    /// catalog / serializer / replication filter never see it.
1334    TablesInSchema(String),
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Eq)]
1338pub struct AlterIndexStatement {
1339    pub name: String,
1340    pub target: AlterIndexTarget,
1341}
1342
1343#[derive(Debug, Clone, PartialEq, Eq)]
1344#[non_exhaustive]
1345pub enum AlterIndexTarget {
1346    /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1347    /// rebuilds the existing graph in place without touching the
1348    /// column encoding; `Some(enc)` re-encodes every cell first.
1349    Rebuild { encoding: Option<VecEncoding> },
1350    /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1351    /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1352    /// uses it to make the migration idempotent (re-running on a
1353    /// DB where the rename already happened is a no-op rather
1354    /// than an error).
1355    Rename { new: String, if_exists: bool },
1356    /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1357    /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1358    /// does not exist`), so the index is validated and the storage
1359    /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1360    /// SET/RESET arms already record).
1361    StorageParams,
1362}
1363
1364/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1365/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1366/// can add more SET subjects without changing the dispatch shape.
1367#[derive(Debug, Clone, PartialEq)]
1368pub struct AlterTableStatement {
1369    pub name: String,
1370    /// v7.13.2 — mailrs round-6 S1. One or more subactions
1371    /// separated by commas in the source SQL. PG-semantic apply
1372    /// is sequential; engine bails on first error (no
1373    /// transactional rollback of completed subactions in v7.13).
1374    /// Single-subaction shape stays a 1-element vec.
1375    pub targets: Vec<AlterTableTarget>,
1376}
1377/// v7.39.9 — the `FIRST` / `AFTER c` trailer, written back the way it
1378/// was read.
1379fn write_column_position(
1380    f: &mut core::fmt::Formatter<'_>,
1381    pos: Option<&ColumnPosition>,
1382) -> core::fmt::Result {
1383    match pos {
1384        Some(ColumnPosition::First) => f.write_str(" FIRST"),
1385        Some(ColumnPosition::After(c)) => write!(f, " AFTER {}", quote_ident(c)),
1386        None => Ok(()),
1387    }
1388}
1389
1390/// v7.39.9 — where MySQL's `ADD` / `MODIFY` / `CHANGE` puts a column.
1391///
1392/// The row encoding is positional and `SELECT *` reads it in order, so
1393/// this is an answer, not a formatting preference.
1394#[derive(Debug, Clone, PartialEq, Eq)]
1395pub enum ColumnPosition {
1396    First,
1397    After(String),
1398}
1399
1400#[derive(Debug, Clone, PartialEq)]
1401#[allow(clippy::large_enum_variant)]
1402pub enum AlterTableTarget {
1403    /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1404    ///
1405    /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1406    /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1407    /// the reasoning went stale: `NO INHERIT` reported success while the
1408    /// child stayed attached, which is the worst kind of answer — the
1409    /// statement says it worked and the catalog disagrees.
1410    Inherit { parent: String, detach: bool },
1411    /// Per-table hot-tier byte budget override. The freezer
1412    /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1413    SetHotTierBytes(u64),
1414    /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1415    /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1416    /// Engine validates existing rows against the new constraint
1417    /// before installing it.
1418    AddForeignKey(ForeignKeyConstraint),
1419    /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1420    /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1421    /// no-op when no FK with that name exists; otherwise raises.
1422    DropForeignKey { name: String, if_exists: bool },
1423    /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1424    /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1425    /// as the standalone `DROP INDEX` statement.
1426    DropIndex { name: String, if_exists: bool },
1427    /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1428    /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1429    /// (20 migrate-*.sql hits). Engine appends the column to the
1430    /// schema and back-fills every existing row with the DEFAULT
1431    /// (or NULL when no DEFAULT and the column is nullable).
1432    AddColumn {
1433        column: ColumnDef,
1434        if_not_exists: bool,
1435        /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>`, which say where
1436        /// the column goes. `None` is the PostgreSQL form and appends.
1437        position: Option<ColumnPosition>,
1438    },
1439    /// v7.39.9 — MySQL's `MODIFY COLUMN c <definition>` and
1440    /// `CHANGE COLUMN old new <definition>`.
1441    ///
1442    /// Both REPLACE the column's definition rather than amending it,
1443    /// which is the part that cannot be expressed by the PostgreSQL
1444    /// spellings SPG already had. Measured on MySQL 9.7.2: a column
1445    /// declared `INT NOT NULL DEFAULT 5`, after `MODIFY COLUMN b
1446    /// BIGINT`, is `bigint` NULLABLE with NO default — restating them
1447    /// keeps them, omitting them drops them. `CHANGE` is the same and
1448    /// also renames.
1449    ModifyColumn {
1450        /// The column as it is named now.
1451        column: String,
1452        /// `CHANGE`'s new name; `None` for `MODIFY`, which keeps it.
1453        rename_to: Option<String>,
1454        /// The whole new definition, exactly as written.
1455        definition: ColumnDef,
1456        position: Option<ColumnPosition>,
1457    },
1458    /// v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
1459    RenameIndex { old: String, new: String },
1460    /// v7.39.9 — MySQL's `ALTER TABLE t AUTO_INCREMENT = n`, which sets
1461    /// the value the NEXT insert takes. Measured on 9.7.2: after
1462    /// `= 100`, the next row's id is 100.
1463    SetTableAutoIncrement(i64),
1464    /// v7.39.9 — MySQL's `ENGINE = <name>`. SPG has one storage engine
1465    /// and substitutes for every name MySQL knows, exactly as
1466    /// `CREATE TABLE` already does; a name MySQL does not know is
1467    /// refused with its 1286, because a typo in a migration must not
1468    /// quietly become SPG's storage.
1469    SetEngine(String),
1470    /// v7.39.9 — MySQL's `CONVERT TO CHARACTER SET <cs> [COLLATE <c>]`.
1471    /// SPG stores UTF-8 throughout, so a charset it can represent is
1472    /// accepted and one it cannot is refused with MySQL's 1115.
1473    ConvertToCharacterSet {
1474        charset: String,
1475        collate: Option<String>,
1476    },
1477    /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1478    /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1479    /// existing row's column value by evaluating the optional
1480    /// USING expression (default `col::<ty>`) and re-coercing
1481    /// against the new column type.
1482    AlterColumnType {
1483        column: String,
1484        new_type: ColumnTypeName,
1485        using: Option<Expr>,
1486        /// v7.39 (round 713) — `COLLATE <name>` between the type and
1487        /// USING. PG re-collates the column, and an ABSENT clause RESETS
1488        /// the collation to the type default (measured round 713) — so
1489        /// `None` is not "leave it alone". The type parser consumed the
1490        /// clause all along and this surface dropped it on the floor:
1491        /// the statement succeeded and the ordering did not change, the
1492        /// silent-divergence shape. Folded variant + the name as written.
1493        collation: Option<(Collation, String)>,
1494    },
1495    /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1496    /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1497    /// every row's value at that position is removed; any index
1498    /// on the column is dropped. `if_exists` makes the drop a
1499    /// no-op when the column is missing. `cascade` removes
1500    /// dependents (FKs referencing the column, partial indexes
1501    /// whose predicate names the column); without it, the engine
1502    /// rejects when dependents exist.
1503    DropColumn {
1504        column: String,
1505        if_exists: bool,
1506        cascade: bool,
1507    },
1508    /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1509    /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1510    /// CONSTRAINT name CHECK (expr)` — table-level constraints
1511    /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1512    /// separate ALTER TABLE statement, so this surface lets the
1513    /// dump load straight through.
1514    AddTableConstraint(TableConstraint),
1515    /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1516    /// there is nothing to record; what PG does that SPG did not is
1517    /// REFUSE a role that does not exist. The name has to reach the
1518    /// engine for that, because only the engine knows the roles.
1519    OwnerTo { role: String },
1520    /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1521    /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1522    /// the hint is still a no-op; naming an index that does not exist is
1523    /// not.
1524    ClusterOn { index: Option<String> },
1525    /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1526    /// already in the table against a constraint added `NOT VALID` and,
1527    /// if they all pass, mark it validated. It used to be swallowed as a
1528    /// no-op on the theory that SPG validated at ADD time; SPG did not.
1529    ValidateConstraint { name: String },
1530    /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1531    /// Renames the column in the schema and propagates the rename
1532    /// to every stored source string that references it as a
1533    /// (potentially-qualified) column identifier: CHECK predicates,
1534    /// partial-index predicates, runtime DEFAULT expressions, and
1535    /// triggers' `UPDATE OF` column lists. Function bodies and
1536    /// trigger bodies are NOT auto-rewritten — they're loose
1537    /// source text and may contain references SPG can't statically
1538    /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1539    /// the column even if dependents exist; users renaming a
1540    /// column referenced by a function body update the function
1541    /// body separately.
1542    RenameColumn { old: String, new: String },
1543    /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1544    /// Reachable now that the schema stores user-supplied constraint names.
1545    RenameConstraint { old: String, new: String },
1546    /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1547    /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1548    /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1549    /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1550    /// (identity); both lower to this. SPG's auto-increment is
1551    /// max+1-scan based, so the dump's `setval(…)` calls stay
1552    /// no-ops without losing the sequence position.
1553    SetColumnAutoIncrement {
1554        column: String,
1555        /// The implicit sequence pg_dump names for an identity
1556        /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1557        /// nextval target for a serial default. The engine creates
1558        /// it if absent so the dump's later `setval(s, …)` lands.
1559        seq_name: Option<String>,
1560    },
1561    /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1562    /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1563    /// migrate-042 uses it). The engine moves the table entry
1564    /// in the catalog under the new name; child catalog state
1565    /// (FKs pointing at this table, triggers watching this
1566    /// table) tracks the rename through the storage layer.
1567    RenameTable { new: String },
1568    /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1569    /// { ALL | <name> }`. Toggles whether row-level triggers
1570    /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1571    /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1572    /// ENABLE epilogue around every table's data block so the
1573    /// rows already-computed in prod don't get re-rewritten
1574    /// (and so trigger-driven side effects like
1575    /// audit/queueing don't re-fire during a bulk reload).
1576    /// `which == TriggerSelector::All` toggles every trigger
1577    /// on the table; `Named(name)` toggles one trigger. The
1578    /// engine persists the disabled state on `TriggerDef.enabled`
1579    /// (catalog FILE_VERSION 25+) and the row-write paths skip
1580    /// the trigger when `!enabled`.
1581    SetTriggerEnabled {
1582        which: TriggerSelector,
1583        enabled: bool,
1584    },
1585    /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1586    /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1587    /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1588    /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1589    SetRowSecurity {
1590        enabled: Option<bool>,
1591        force: Option<bool>,
1592    },
1593    /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1594    /// <bounds>`. Promotes an existing table `child` to a partition
1595    /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1596    /// Engine validates that `child`'s columns are layout-compatible
1597    /// with `parent` and that every row in `child` satisfies the
1598    /// bound before installing the role.
1599    AttachPartition {
1600        child: String,
1601        bounds: PartitionOfBoundsAst,
1602    },
1603    /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1604    /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1605    /// to a standalone table (clears `partition_role`) and removes
1606    /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1607    /// is parser-accepted; engine performs the same atomic detach
1608    /// (single-engine, no replication lag — the PG semantics that
1609    /// require the two-phase split don't apply).
1610    DetachPartition {
1611        child: String,
1612        concurrently: bool,
1613        finalize: bool,
1614    },
1615    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1616    /// <expr>`. Engine re-parses + freezes the literal at this point,
1617    /// matching CREATE TABLE-side default semantics. Volatile shapes
1618    /// (`now()` / `nextval`) take the runtime-default path.
1619    AlterColumnSetDefault { column: String, default_expr: Expr },
1620    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1621    AlterColumnDropDefault { column: String },
1622    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1623    /// Engine validates that no existing row has NULL in that column
1624    /// before flipping the flag (PG semantics — partial NOT NULL
1625    /// would surface inconsistently).
1626    AlterColumnSetNotNull { column: String },
1627    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1628    AlterColumnDropNotNull { column: String },
1629    /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1630    /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1631    /// column's start value = 1). Engine records a next-value floor over
1632    /// SPG's max+1 identity allocation.
1633    AlterColumnRestart { column: String, with: Option<i64> },
1634    /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1635    /// EXPRESSION` turns a stored generated column into a plain column
1636    /// (its generation expression is removed; existing values are kept).
1637    AlterColumnDropExpression { column: String, if_exists: bool },
1638    /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1639    /// de-generate an identity column into a plain column.
1640    AlterColumnDropIdentity { column: String, if_exists: bool },
1641    /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1642    /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1643    /// expression and recomputes every existing row.
1644    AlterColumnSetExpression { column: String, expr: Expr },
1645    /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1646    /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1647    /// (PG: `type "x" does not exist`).
1648    OfType { type_name: String },
1649    /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1650    /// identity setting no-ops (SPG has no logical replication consumer);
1651    /// the INDEX must exist on this table (PG: `index "i" for table "t"
1652    /// does not exist`).
1653    ReplicaIdentityUsingIndex { index: String },
1654}
1655
1656/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1657/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1658/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1659/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1660/// shouldn't surface from a dump.
1661#[derive(Debug, Clone, PartialEq, Eq)]
1662pub enum TriggerSelector {
1663    /// Every trigger on the table.
1664    All,
1665    /// A specific trigger by name.
1666    Named(String),
1667}
1668
1669/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1670/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1671/// bitflags word or a nested options struct would only relocate the lint
1672/// while making the option each caller sets harder to read.
1673#[allow(clippy::struct_excessive_bools)]
1674#[derive(Debug, Clone, PartialEq)]
1675pub struct ExplainStatement {
1676    pub analyze: bool,
1677    /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1678    /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1679    /// `Insert on / Update on / Delete on` trees for them.
1680    pub inner: Box<Statement>,
1681    /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1682    /// advisor pass: after the regular plan tree, the engine
1683    /// emits one suggestion line per column referenced in the
1684    /// query's WHERE / JOIN that has no covering index on the
1685    /// owning table.
1686    pub suggest: bool,
1687    /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1688    /// `elapsed=…us` annotations from the Total line (and any
1689    /// future cost-bearing lines). PG-standard option used by
1690    /// regression suites and diff-friendly EXPLAIN output. When
1691    /// `true`, takes precedence over the per-session
1692    /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1693    pub costs_off: bool,
1694    /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1695    /// option that surfaces hot/cold/shared block counters. SPG's
1696    /// hot-tier scan path counts examined rows; the BUFFERS option
1697    /// makes that an explicit per-operator annotation.
1698    pub buffers: bool,
1699    /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1700    /// uses this to disable per-operator timing while still
1701    /// emitting actual-row counts (cheaper than ANALYZE). Default
1702    /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1703    /// timing portion of the Total line. Decoupled from `costs_off`:
1704    /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1705    /// measured wall-clock.
1706    pub timing_off: bool,
1707    /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1708    /// modified GUC values to the plan output. SPG emits the
1709    /// session params that diverge from default after the main
1710    /// plan body.
1711    pub settings: bool,
1712    /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1713    /// bytes / records / FPI emitted by the query. SPG's
1714    /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1715    /// ANALYZE) report against the engine WAL counter delta.
1716    pub wal: bool,
1717    /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1718    /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1719    /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1720    /// this is set.
1721    pub summary_off: bool,
1722    /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1723    /// PG's standard format selector. Default is text. JSON / XML
1724    /// / YAML emit a single-row TEXT result whose body wraps the
1725    /// existing line-per-operator text in the chosen container —
1726    /// PG-compatible just enough for dashboards that parse those
1727    /// container shapes (pgAdmin's JSON path picker, etc.).
1728    pub format: ExplainFormat,
1729}
1730
1731#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1732pub enum ExplainFormat {
1733    #[default]
1734    Text,
1735    Json,
1736    Xml,
1737    Yaml,
1738}
1739
1740/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1741#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1742pub enum PolicyCmd {
1743    All,
1744    Select,
1745    Insert,
1746    Update,
1747    Delete,
1748}
1749
1750/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1751/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1752#[derive(Debug, Clone, PartialEq)]
1753pub struct CreatePolicyStatement {
1754    pub name: String,
1755    pub table: String,
1756    /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1757    pub permissive: bool,
1758    pub cmd: PolicyCmd,
1759    /// Empty = PUBLIC.
1760    pub roles: Vec<String>,
1761    pub using: Option<Expr>,
1762    pub with_check: Option<Expr>,
1763}
1764
1765/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1766/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1767/// or the command (matches PG).
1768#[derive(Debug, Clone, PartialEq)]
1769pub struct AlterPolicyStatement {
1770    pub name: String,
1771    pub table: String,
1772    pub rename_to: Option<String>,
1773    pub roles: Option<Vec<String>>,
1774    pub using: Option<Expr>,
1775    pub with_check: Option<Expr>,
1776}
1777
1778/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1779#[derive(Debug, Clone, PartialEq, Eq)]
1780pub struct DropPolicyStatement {
1781    pub name: String,
1782    pub table: String,
1783    pub if_exists: bool,
1784}
1785
1786#[derive(Debug, Clone, PartialEq, Eq)]
1787pub struct CreateUserStatement {
1788    pub name: String,
1789    /// Empty when the statement carried no PASSWORD — legal for a bare
1790    /// `CREATE ROLE`, which cannot log in anyway.
1791    pub password: String,
1792    /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1793    /// the parser; the engine validates against `Role::parse` so a
1794    /// typo lands as a runtime error with a clear message rather than
1795    /// a parse failure.
1796    pub role: String,
1797    /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1798    /// statement did not say, so the default for its spelling applies:
1799    /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1800    /// both default to INHERIT and NOSUPERUSER.
1801    pub login: Option<bool>,
1802    pub inherit: Option<bool>,
1803    pub superuser: Option<bool>,
1804    /// `true` when spelled `CREATE USER` (LOGIN by default).
1805    pub is_user: bool,
1806}
1807
1808/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1809/// it tells the planner how far a call may be moved or folded. SPG records
1810/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1811/// yet exploit it for constant folding.
1812#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1813pub enum FunctionVolatility {
1814    Immutable,
1815    Stable,
1816    #[default]
1817    Volatile,
1818}
1819
1820impl FunctionVolatility {
1821    /// PG's one-character `pg_proc.provolatile` code.
1822    #[must_use]
1823    pub const fn as_pg_char(self) -> &'static str {
1824        match self {
1825            Self::Immutable => "i",
1826            Self::Stable => "s",
1827            Self::Volatile => "v",
1828        }
1829    }
1830
1831    #[must_use]
1832    pub const fn as_sql(self) -> &'static str {
1833        match self {
1834            Self::Immutable => "IMMUTABLE",
1835            Self::Stable => "STABLE",
1836            Self::Volatile => "VOLATILE",
1837        }
1838    }
1839}
1840
1841/// v7.39 (round 322, V46) — PG's parallel-safety class.
1842#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1843pub enum FunctionParallel {
1844    #[default]
1845    Unsafe,
1846    Restricted,
1847    Safe,
1848}
1849
1850impl FunctionParallel {
1851    /// PG's one-character `pg_proc.proparallel` code.
1852    #[must_use]
1853    pub const fn as_pg_char(self) -> &'static str {
1854        match self {
1855            Self::Unsafe => "u",
1856            Self::Restricted => "r",
1857            Self::Safe => "s",
1858        }
1859    }
1860
1861    #[must_use]
1862    pub const fn as_sql(self) -> &'static str {
1863        match self {
1864            Self::Unsafe => "PARALLEL UNSAFE",
1865            Self::Restricted => "PARALLEL RESTRICTED",
1866            Self::Safe => "PARALLEL SAFE",
1867        }
1868    }
1869}
1870
1871/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1872/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1873/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1874/// language's default cost / rows.
1875#[derive(Debug, Clone, Copy, PartialEq, Default)]
1876pub struct FunctionAttrs {
1877    pub volatility: FunctionVolatility,
1878    /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1879    /// argument returns NULL without running the body.
1880    pub strict: bool,
1881    pub security_definer: bool,
1882    pub leakproof: bool,
1883    pub parallel: FunctionParallel,
1884    /// `COST n` — `None` leaves PG's per-language default.
1885    pub cost: Option<f64>,
1886    /// `ROWS n` — set-returning functions only; `None` = default.
1887    pub rows: Option<f64>,
1888}
1889
1890impl FunctionAttrs {
1891    /// The attribute words `pg_get_functiondef` puts on their own line,
1892    /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1893    /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1894    /// at its default — PG then emits no such line at all.
1895    #[must_use]
1896    pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1897        let mut out = alloc::vec::Vec::new();
1898        if self.volatility != FunctionVolatility::Volatile {
1899            out.push(alloc::string::String::from(self.volatility.as_sql()));
1900        }
1901        if self.parallel != FunctionParallel::Unsafe {
1902            out.push(alloc::string::String::from(self.parallel.as_sql()));
1903        }
1904        if self.strict {
1905            out.push(alloc::string::String::from("STRICT"));
1906        }
1907        if self.security_definer {
1908            out.push(alloc::string::String::from("SECURITY DEFINER"));
1909        }
1910        if self.leakproof {
1911            out.push(alloc::string::String::from("LEAKPROOF"));
1912        }
1913        if let Some(c) = self.cost {
1914            out.push(alloc::format!("COST {}", render_attr_number(c)));
1915        }
1916        if let Some(r) = self.rows {
1917            out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1918        }
1919        out
1920    }
1921}
1922
1923/// PG prints a whole-numbered cost / rows without a decimal point.
1924fn render_attr_number(v: f64) -> alloc::string::String {
1925    // no_std: `f64::fract` lives in std, so compare against the truncation.
1926    let whole = v as i64;
1927    if v.abs() < 1e15 && (whole as f64) == v {
1928        alloc::format!("{whole}")
1929    } else {
1930        alloc::format!("{v}")
1931    }
1932}
1933
1934/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1935/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1936/// (the row-level trigger body the CREATE TRIGGER below references).
1937/// Non-trigger user-defined functions parse but error at execution
1938/// time with a clear unsupported message; that surface lands in
1939/// v7.12.5+.
1940#[derive(Debug, Clone, PartialEq)]
1941pub struct CreateFunctionStatement {
1942    pub name: String,
1943    /// `OR REPLACE` was present; an existing function with the
1944    /// same name is overwritten instead of erroring.
1945    pub or_replace: bool,
1946    /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1947    /// list `()` (sufficient for trigger functions). Other shapes
1948    /// parse and store the args but the executor refuses to call
1949    /// them.
1950    pub args: Vec<FunctionArg>,
1951    /// `RETURNS <type>` — `trigger` is the supported shape for
1952    /// v7.12.4; arbitrary return types parse to
1953    /// [`FunctionReturn::Other`].
1954    pub returns: FunctionReturn,
1955    /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1956    /// side of `AS $$...$$`; the parser canonicalises to one slot.
1957    /// `plpgsql` and `sql` are the two interesting values.
1958    pub language: String,
1959    /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1960    /// a structured AST; non-trigger / non-plpgsql bodies stay as
1961    /// the raw source text so the v7.12.5+ executor can pick them
1962    /// up without a parser rev.
1963    pub body: FunctionBody,
1964    /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1965    /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1966    /// on either side of the body; before this they were a parse error, so
1967    /// PG's own `pg_dump` output would not restore.
1968    pub attrs: FunctionAttrs,
1969}
1970
1971/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1972#[derive(Debug, Clone, PartialEq)]
1973pub struct FunctionArg {
1974    /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1975    /// (the default); `OUT` / `INOUT` parse but the executor
1976    /// refuses them.
1977    pub mode: FunctionArgMode,
1978    /// Optional arg name. Trigger functions traditionally don't
1979    /// name their args (they read NEW/OLD instead), so `None` is
1980    /// the common case.
1981    pub name: Option<String>,
1982    /// Declared type, normalised to the SPG `DataType` mapping
1983    /// where one exists. Unknown / extension types parse as a
1984    /// raw string under [`FunctionArgType::Raw`].
1985    pub ty: FunctionArgType,
1986}
1987
1988#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1989pub enum FunctionArgMode {
1990    In,
1991    Out,
1992    InOut,
1993}
1994
1995#[derive(Debug, Clone, PartialEq)]
1996pub enum FunctionArgType {
1997    Typed(ColumnTypeName),
1998    /// Unknown / extension types — kept as the parser-side raw
1999    /// identifier so error messages can name them precisely.
2000    Raw(String),
2001}
2002
2003#[derive(Debug, Clone, PartialEq)]
2004pub enum FunctionReturn {
2005    /// `RETURNS TRIGGER` — the row-level trigger function shape.
2006    /// v7.12.4 ships exactly this for execution.
2007    Trigger,
2008    /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
2009    /// the function is unused (since v7.12.4 doesn't ship scalar
2010    /// function invocation).
2011    Void,
2012    /// `RETURNS <type>` for any concrete data type. Reserved for
2013    /// v7.12.5+'s scalar UDF surface.
2014    Type(ColumnTypeName),
2015    /// `RETURNS <ident>` for types SPG doesn't know — extension
2016    /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
2017    Other(String),
2018}
2019
2020#[derive(Debug, Clone, PartialEq)]
2021pub enum FunctionBody {
2022    /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
2023    /// trigger-function executor walks this directly without
2024    /// re-parsing.
2025    PlPgSql(PlPgSqlBlock),
2026    /// Raw source text — parser couldn't (or didn't try to)
2027    /// structure-parse the body. Used for `LANGUAGE sql`
2028    /// functions and any PL/pgSQL body that contains v7.12.5+
2029    /// features the v7.12.4 parser doesn't yet recognise. The
2030    /// executor returns an unsupported error when invoked.
2031    Raw(String),
2032}
2033
2034/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
2035/// from assignment + return to a real-PL/pgSQL surface:
2036/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
2037/// control flow, `RAISE` diagnostics, and embedded SQL
2038/// statements that execute through the regular engine path.
2039/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
2040/// which mailrs's trigger doesn't need but other PG customers
2041/// may; deferred to a future minor release.
2042#[derive(Debug, Clone, PartialEq)]
2043pub struct PlPgSqlBlock {
2044    /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
2045    /// preceding `BEGIN`. Empty when the body opens directly with
2046    /// `BEGIN`. Declarations execute in order; each may reference
2047    /// earlier-declared locals in its init expression.
2048    pub declarations: Vec<PlPgSqlDeclare>,
2049    pub statements: Vec<PlPgSqlStmt>,
2050    /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
2051    /// <body>` handlers appended to the block. Empty when no
2052    /// EXCEPTION clause is present. When a body statement raises
2053    /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
2054    /// handlers are tried in order; the first matching condition
2055    /// runs its body and the block terminates cleanly. `OTHERS`
2056    /// matches any exception. Unhandled exceptions propagate.
2057    pub exception_handlers: Vec<ExceptionHandler>,
2058}
2059
2060/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
2061/// arm inside an EXCEPTION block.
2062#[derive(Debug, Clone, PartialEq)]
2063pub struct ExceptionHandler {
2064    /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
2065    /// conditions joined by `OR` share one handler body.
2066    pub conditions: Vec<String>,
2067    /// Statements to run when a matching exception is caught.
2068    pub body: Vec<PlPgSqlStmt>,
2069}
2070
2071/// v7.12.6 — single `DECLARE` entry: variable name + declared
2072/// type + optional initialiser. Variables default to SQL NULL
2073/// when no init is given (matches PG).
2074#[derive(Debug, Clone, PartialEq)]
2075pub struct PlPgSqlDeclare {
2076    pub name: String,
2077    /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
2078    /// knows it; raw text otherwise).
2079    pub ty: FunctionArgType,
2080    pub default: Option<Expr>,
2081}
2082
2083#[derive(Debug, Clone, PartialEq)]
2084pub enum PlPgSqlStmt {
2085    /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
2086    /// for clarity in error reporting (PG also forbids it) — the
2087    /// executor errors with a clear "OLD is read-only" message.
2088    Assign { target: AssignTarget, value: Expr },
2089    /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
2090    /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
2091    /// the SELECT statement with the INTO clause stripped; the
2092    /// engine runs it via `Engine::execute`, takes the first
2093    /// row's first column, and assigns to the local variable
2094    /// in the DECLARE scope. Single-column / single-row
2095    /// queries only at v7.16.2; multi-target (`INTO a, b`) is
2096    /// a v7.16.x follow-up.
2097    SelectInto {
2098        var: String,
2099        body: Box<SelectStatement>,
2100    },
2101    /// `RETURN <target>;` — trigger functions canonically return
2102    /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
2103    /// expression for forward compatibility with scalar UDFs.
2104    Return(ReturnTarget),
2105    /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
2106    /// set a SETOF function is building, and KEEP GOING. Not a return.
2107    ReturnNext(Expr),
2108    /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
2109    /// query yields, and keep going. It used to desugar to a side-effect
2110    /// statement whose result was DISCARDED — in a SETOF function that is the
2111    /// whole answer thrown away.
2112    ReturnQuery(Box<SelectStatement>),
2113    /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
2114    /// twin. Its rows go to the set too; it used to run and discard them.
2115    ReturnQueryExecute { sql: Expr },
2116    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
2117    /// [ELSE body] END IF;`. Branches are tried in order; first
2118    /// truthy condition wins; the optional ELSE runs when no
2119    /// condition matched.
2120    If {
2121        branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
2122        else_branch: Vec<PlPgSqlStmt>,
2123    },
2124    /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
2125    /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
2126    /// (logging — observable side effect only) or `EXCEPTION`
2127    /// (aborts the trigger and propagates as an error). v7.12.6
2128    /// supports the basic format-string substitution PG uses
2129    /// (`%` placeholders consumed positionally).
2130    Raise {
2131        level: RaiseLevel,
2132        message: String,
2133        args: Vec<Expr>,
2134    },
2135    /// v7.12.6 — embedded SQL statement inside the trigger body
2136    /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
2137    /// NEW.col / OLD.col references inside the embedded
2138    /// statement's expression tree are substituted with the
2139    /// current trigger context before the engine re-executes the
2140    /// statement. Recursion depth into nested triggers is
2141    /// bounded by the engine's existing trigger-fire guard.
2142    EmbeddedSql(Box<Statement>),
2143    /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
2144    /// the condition evaluates falsy the trigger / DO block aborts
2145    /// with the message (defaulting to a generic shape when none
2146    /// is provided). Same propagation shape as `RAISE EXCEPTION`
2147    /// — the error reaches the caller's query path. PG's behaviour
2148    /// is identical except for a `plpgsql.check_asserts` GUC that
2149    /// can disable the check globally; SPG always evaluates.
2150    Assert {
2151        condition: Expr,
2152        message: Option<Expr>,
2153    },
2154    /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2155    /// Iterate the body while condition evaluates truthy. Iteration
2156    /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2157    /// loops; the executor errors out when reached. EXIT / CONTINUE
2158    /// inside the body queue with 20.2.
2159    While {
2160        condition: Expr,
2161        body: Vec<PlPgSqlStmt>,
2162    },
2163    /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2164    /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2165    /// bounds inclusive on both sides. REVERSE walks backward.
2166    /// Iteration budget guards runaway.
2167    ForRange {
2168        var: String,
2169        start: Expr,
2170        end: Expr,
2171        reverse: bool,
2172        body: Vec<PlPgSqlStmt>,
2173    },
2174    /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2175    /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2176    /// budget guards runaway.
2177    Loop { body: Vec<PlPgSqlStmt> },
2178    /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2179    /// Unconditional (no WHEN) or conditional (only breaks when
2180    /// condition is truthy). Bubbles up as BodyOutcome::Break which
2181    /// the enclosing loop catches. Outside a loop it's a no-op.
2182    Exit { when: Option<Expr> },
2183    /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2184    /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2185    /// which the enclosing loop catches, skipping the remainder of
2186    /// the body and jumping to the next iteration.
2187    Continue { when: Option<Expr> },
2188    /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2189    /// computed SQL statement. The expression is evaluated to a
2190    /// text value, the resulting string is parsed and dispatched
2191    /// through the engine like an EmbeddedSql. USING <param_list>
2192    /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2193    ExecuteDynamic { sql: Expr },
2194    /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2195    /// END LOOP;`. Runs the SELECT once, iterates the resulting
2196    /// rows, binds the first column of each row to `var` as a
2197    /// scalar Value, then runs the body per iteration. EXIT /
2198    /// CONTINUE / ASSERT / RAISE etc. propagate through the
2199    /// enclosing loop's BodyOutcome discipline the same way
2200    /// FOR range and WHILE do. Full record-binding (var as
2201    /// composite carrying all columns) queues with v7.40 record
2202    /// type infrastructure.
2203    ForQuery {
2204        var: String,
2205        query: Box<SelectStatement>,
2206        body: Vec<PlPgSqlStmt>,
2207    },
2208    /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2209    /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2210    /// computed at runtime from a text expression, parsed on the
2211    /// fly, then iterated. Enables dynamic queries where the
2212    /// projection / FROM / WHERE clauses depend on runtime values.
2213    ForExecute {
2214        var: String,
2215        sql_expr: Expr,
2216        body: Vec<PlPgSqlStmt>,
2217    },
2218}
2219
2220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2221#[non_exhaustive]
2222pub enum RaiseLevel {
2223    /// `RAISE NOTICE` — diagnostic message, observable in the
2224    /// server log. Does not affect the trigger's outcome.
2225    Notice,
2226    /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2227    Warning,
2228    /// `RAISE INFO` — like NOTICE, slightly quieter.
2229    Info,
2230    /// `RAISE LOG` — like NOTICE, lower priority.
2231    Log,
2232    /// `RAISE DEBUG` — like NOTICE, lowest priority.
2233    Debug,
2234    /// `RAISE EXCEPTION` — aborts the trigger function with the
2235    /// given message, propagating up to the caller as a query-
2236    /// level error.
2237    Exception,
2238}
2239
2240#[derive(Debug, Clone, PartialEq)]
2241pub enum AssignTarget {
2242    NewColumn(String),
2243    OldColumn(String),
2244    /// Reserved for v7.12.5 DECLARE'd local variables.
2245    Local(String),
2246}
2247
2248#[derive(Debug, Clone, PartialEq)]
2249pub enum ReturnTarget {
2250    /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2251    /// actually gets written (possibly with NEW.col mutations
2252    /// applied). For AFTER triggers, the return value is ignored.
2253    New,
2254    /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2255    /// the delete proceed; for BEFORE UPDATE / INSERT it's
2256    /// equivalent to dropping the write.
2257    Old,
2258    /// `RETURN NULL;` — for BEFORE triggers, skips the write
2259    /// entirely. For AFTER, the return value is ignored.
2260    Null,
2261    /// `RETURN <expr>;` — non-row return shape; reserved for the
2262    /// scalar UDF surface in v7.12.5+. Executor errors when used
2263    /// inside a trigger function.
2264    Expr(Expr),
2265}
2266
2267/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2268/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2269/// but the executor refuses them. `WHEN (cond)` clauses are out
2270/// of scope; the trigger function can short-circuit on a leading
2271/// IF inside its body once v7.12.5 lands IF.
2272#[derive(Debug, Clone, PartialEq)]
2273pub struct CreateTriggerStatement {
2274    pub name: String,
2275    pub or_replace: bool,
2276    pub timing: TriggerTiming,
2277    /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2278    /// three entries in order.
2279    pub events: Vec<TriggerEvent>,
2280    pub table: String,
2281    /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2282    /// only `Row`; `Statement` parses but the executor refuses.
2283    pub for_each: TriggerForEach,
2284    /// Name of the function to invoke. The function must exist at
2285    /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2286    /// forward reference (`function no_such_fn() does not exist`), so
2287    /// requiring it IS the PG behaviour (the old note claimed the
2288    /// opposite).
2289    pub function: String,
2290    /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2291    /// (mailrs round-5 G7). Non-empty only when the events list
2292    /// contains UPDATE and the user wrote the column-list filter.
2293    /// PG fires the trigger only when at least one of these
2294    /// columns appears in the SET clause; SPG conservatively
2295    /// fires on any UPDATE matching the listed columns or
2296    /// rewriting them at the row level. Empty vec = no filter
2297    /// (fire on every UPDATE).
2298    pub update_columns: Vec<String>,
2299    /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2300    /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2301    /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2302    pub when_condition: Option<Expr>,
2303}
2304
2305/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2306#[derive(Debug, Clone, PartialEq)]
2307pub struct CreateRuleStatement {
2308    pub name: String,
2309    pub or_replace: bool,
2310    /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2311    pub event: String,
2312    pub table: String,
2313    /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2314    /// (run alongside; PG's default when neither keyword is written).
2315    pub instead: bool,
2316    /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2317    pub when_condition: Option<Expr>,
2318    /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2319    pub commands: Vec<Statement>,
2320}
2321
2322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2323pub enum TriggerTiming {
2324    /// Fires before the row is written; the trigger function's
2325    /// return value (NEW or NULL) decides the row content and
2326    /// whether the write proceeds at all.
2327    Before,
2328    /// Fires after the row is written; the return value is
2329    /// ignored.
2330    After,
2331    /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2332    /// v7.12.4 (SPG has no updatable-view surface).
2333    InsteadOf,
2334}
2335
2336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2337pub enum TriggerEvent {
2338    Insert,
2339    Update,
2340    Delete,
2341    /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2342    /// so the trigger never fires.
2343    Truncate,
2344}
2345
2346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2347pub enum TriggerForEach {
2348    Row,
2349    Statement,
2350}
2351
2352/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2353///
2354/// SPG's index does not scan in a direction, but `indexdef` reproduces
2355/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2356/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2357/// which case PG's default applies — LAST for ascending, FIRST for
2358/// descending, and neither is rendered.
2359#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2360pub struct IndexColumnOrder {
2361    pub descending: bool,
2362    pub nulls_first: Option<bool>,
2363}
2364
2365#[derive(Debug, Clone, PartialEq)]
2366pub struct CreateIndexStatement {
2367    pub name: String,
2368    /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2369    /// either way, so this changes nothing about how the index is made
2370    /// — it is carried because PG refuses the CONCURRENTLY form inside
2371    /// a transaction block and accepts the plain one, and the engine
2372    /// cannot tell them apart without it.
2373    pub concurrently: bool,
2374    /// v7.39 (round 537) — the leading key column's ordering clause,
2375    /// which is the column SPG indexes.
2376    pub key_order: IndexColumnOrder,
2377    /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2378    /// written. SPG orders text by bytes, so honouring it changes
2379    /// nothing; PG prints it, because an explicitly named collation and
2380    /// the one a column inherits are different objects.
2381    pub key_collation: Option<String>,
2382    pub table: String,
2383    pub column: String,
2384    /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2385    /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2386    /// any NULL in the key exempts the row from the uniqueness check.
2387    pub nulls_not_distinct: bool,
2388    /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2389    /// graph for vector kNN); unspecified is the default B-tree index.
2390    pub method: IndexMethod,
2391    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2392    /// index name already exists, instead of raising `DuplicateIndex`.
2393    pub if_not_exists: bool,
2394    /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2395    /// non-key columns the planner should treat as "covered" by
2396    /// this index when checking whether a query can run as an
2397    /// index-only scan. Empty when no `INCLUDE` clause was given.
2398    pub included_columns: Vec<String>,
2399    /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2400    /// for which `<expr>` evaluates truthy enter the index;
2401    /// queries whose `WHERE` clause's canonical Display form
2402    /// matches this expression's Display form can be served by the
2403    /// partial index. Stored as a parsed `Expr` so the engine
2404    /// re-uses the existing evaluation path; storage persists the
2405    /// Display form on the catalog snapshot.
2406    pub partial_predicate: Option<Expr>,
2407    /// v6.8.2 — expression-based index. When `Some(expr)`, the
2408    /// index key is the result of `expr` evaluated on each row
2409    /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2410    /// field still names the *primary* column the expression
2411    /// touches so existing planner shortcuts that resolve a
2412    /// column position stay valid. `None` = plain
2413    /// column-reference index (the legacy shape).
2414    pub expression: Option<Expr>,
2415    /// v7.9.14 — extra column names after the leading column in a
2416    /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2417    /// planner today still only uses the leading column for index
2418    /// seeks; the extras are tracked verbatim so the same DDL
2419    /// round-trips through WAL replay + catalog snapshot, and so
2420    /// the engine can emit a clear warning at INDEX CREATE time
2421    /// that only the leading column is currently honoured.
2422    /// Composite BTree index keys land in v7.10.
2423    pub extra_columns: Vec<String>,
2424    /// v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
2425    /// / `NULLS LAST`, positionally aligned with `extra_columns`. The
2426    /// parser used to discard these, so a composite index's direction
2427    /// survived only on the leading column and `pg_get_indexdef`
2428    /// rendered `(a, b DESC)` back as `(a, b)`.
2429    pub extra_orders: Vec<IndexColumnOrder>,
2430    /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2431    /// enforces uniqueness on the indexed key (combined with the
2432    /// `partial_predicate` filter — only rows where the predicate
2433    /// evaluates truthy enter the uniqueness check). Standard SQL
2434    /// and PG's canonical way to express conditional uniqueness.
2435    /// mailrs K1.
2436    pub is_unique: bool,
2437    /// v7.15.0 — operator class on the leading column, when the
2438    /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2439    /// Lower-cased. Most opclasses are still informational; the
2440    /// engine routes on `gin_trgm_ops` specifically to build a
2441    /// trigram-shingle GIN over a TEXT column, and otherwise
2442    /// keeps the current "accepted and discarded" behaviour for
2443    /// pg_dump compatibility.
2444    pub opclass: Option<String>,
2445    /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2446    /// there was no `USING` clause.
2447    ///
2448    /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2449    /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2450    /// implementation for still load. That degradation is deliberate, but
2451    /// it loses the name — and the operator-class check needs it, both to
2452    /// look the class up under the AM the user actually named and to say
2453    /// which AM it was missing from, the way PG's message does.
2454    pub method_name: Option<String>,
2455}
2456
2457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2458pub enum IndexMethod {
2459    /// Default — B-tree over `IndexKey`. Used for equality / range
2460    /// lookups on scalar columns.
2461    BTree,
2462    /// `USING hnsw` — NSW graph for kNN over a vector column.
2463    Hnsw,
2464    /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2465    /// metadata that records (min_key, max_key) for each page in a
2466    /// cold-tier segment, on the indexed column. The optimizer
2467    /// can use these summaries to skip pages whose range does NOT
2468    /// overlap a query's WHERE predicate. BRIN indexes carry no
2469    /// in-memory data — the summaries live in the segment v2
2470    /// envelope's sidecar. Created via the standard
2471    /// `CREATE INDEX … USING brin (col)` syntax.
2472    Brin,
2473    /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2474    /// column. Posting lists map `lexeme word` → row locators; the
2475    /// planner uses them to narrow `WHERE col @@ tsquery` to the
2476    /// candidate rows whose vectors contain a matching term, then
2477    /// re-evaluates the full `@@` semantics on each candidate.
2478    /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2479    /// silently degraded to a full scan at query time.
2480    Gin,
2481}
2482
2483/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2484/// inside a CREATE TABLE column list.
2485///
2486/// The source table's shape can only be read from the catalog, so the
2487/// parser records the clause and the engine expands it. `at` is how many
2488/// explicit columns preceded it: PG keeps the written order, so
2489/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2490#[derive(Debug, Clone, PartialEq)]
2491pub struct LikeSpec {
2492    pub source: String,
2493    pub at: usize,
2494    pub options: LikeOptions,
2495    /// v7.40.0 — MySQL's `CREATE TABLE b LIKE a` keeps the source's
2496    /// index names (`PRIMARY`, `ks`); PostgreSQL's
2497    /// `CREATE TABLE b (LIKE a INCLUDING ALL)` renames the copies after
2498    /// the new table (`lb_pkey`, `lb_s_idx`). Both measured. The
2499    /// spelling says which engine's rule applies.
2500    pub keep_index_names: bool,
2501}
2502
2503/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2504/// types and NOT NULL and nothing else — measured on PG18, where a
2505/// copied generated column becomes a plain one and a copied identity
2506/// column loses its identity.
2507#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2508pub struct LikeOptions {
2509    pub defaults: bool,
2510    pub constraints: bool,
2511    pub identity: bool,
2512    pub generated: bool,
2513    pub indexes: bool,
2514    pub comments: bool,
2515}
2516
2517#[derive(Debug, Clone, PartialEq)]
2518pub struct CreateTableStatement {
2519    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2520    /// creating session's own namespace: it shadows a permanent table of the
2521    /// same name, other sessions never see it, and it is dropped when the
2522    /// session ends. A `bool` here lands in the struct's existing padding.
2523    pub temporary: bool,
2524    pub name: String,
2525    /// v7.39 — the `ENGINE=` a MySQL dump names. Consumed and discarded
2526    /// before, so `ENGINE=NONSUCH` built a table where MySQL 9.7.2
2527    /// answers `ERROR 1286`, and `sql_mode` claimed
2528    /// `NO_ENGINE_SUBSTITUTION` while doing it.
2529    pub engine: Option<String>,
2530    /// v7.40.0 — the `AUTO_INCREMENT=N` table option: the next value
2531    /// the table hands out. It was consumed and dropped, so the first
2532    /// row of a table declared `AUTO_INCREMENT=100` got 1 where MySQL
2533    /// 9.7.2 gives it 100 — and `SHOW CREATE TABLE`, which reproduces
2534    /// the option from the counter, round-tripped a different number.
2535    pub auto_increment: Option<i64>,
2536    pub columns: Vec<ColumnDef>,
2537    /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2538    /// the order written. Empty for a table that has none.
2539    pub like_specs: Vec<LikeSpec>,
2540    /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2541    /// Empty for a table that inherits from nothing. Order matters:
2542    /// the child takes each parent's columns in this order before its
2543    /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2544    pub inherits: Vec<String>,
2545    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2546    /// table name already exists, instead of raising `DuplicateTable`.
2547    pub if_not_exists: bool,
2548    /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2549    /// constraints. Column-level `REFERENCES` (single-column inline
2550    /// form) is normalised into this vec at parse time so the engine
2551    /// sees one uniform list.
2552    pub foreign_keys: Vec<ForeignKeyConstraint>,
2553    /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2554    /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2555    /// Engine resolves each into a BTree index named after the
2556    /// constraint's leading column at CREATE TABLE time; INSERT
2557    /// path enforces composite uniqueness via row scan on the
2558    /// leading column index.
2559    pub table_constraints: Vec<TableConstraint>,
2560    /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2561    /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2562    /// the engine creates a parent table whose own rows stay
2563    /// empty and routes INSERT/SELECT through children. Mutually
2564    /// exclusive with `partition_of` (parser enforces).
2565    pub partition_by: Option<PartitionBySpec>,
2566    /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2567    /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2568    /// the table inherits its column list from `parent` (the
2569    /// parser rejects an explicit column list when this is set);
2570    /// engine routes child rows back to the parent at INSERT.
2571    pub partition_of: Option<PartitionOfSpec>,
2572}
2573
2574/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2575/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2576/// future LIST / HASH without breaking the public AST shape.
2577#[derive(Debug, Clone, PartialEq)]
2578pub struct PartitionBySpec {
2579    pub kind: PartitionKindAst,
2580    /// One or more ident references into the parent's column list.
2581    /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2582    /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2583    /// shape PG-compatible.
2584    pub key_columns: Vec<String>,
2585}
2586
2587#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2588pub enum PartitionKindAst {
2589    Range,
2590    /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2591    /// `FOR VALUES IN (lit, lit, …)`.
2592    List,
2593    /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2594    /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2595    Hash,
2596}
2597
2598/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2599/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2600/// or the catch-all `DEFAULT` partition.
2601#[derive(Debug, Clone, PartialEq)]
2602pub struct PartitionOfSpec {
2603    pub parent_name: String,
2604    pub bounds: PartitionOfBoundsAst,
2605}
2606
2607#[derive(Debug, Clone, PartialEq)]
2608pub enum PartitionOfBoundsAst {
2609    /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2610    /// (lits include vector bodies), so we box both bounds to keep
2611    /// the variant size in line with `Default` for clippy and to
2612    /// minimise per-statement footprint when the partition shape
2613    /// isn't in use.
2614    Range {
2615        lower: Box<Expr>,
2616        upper: Box<Expr>,
2617    },
2618    /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2619    /// expr resolves to a typed literal at child-create time.
2620    List {
2621        values: Vec<Expr>,
2622    },
2623    /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2624    /// PG enforces `0 ≤ r < m`; m must be positive.
2625    Hash {
2626        modulus: u32,
2627        remainder: u32,
2628    },
2629    Default,
2630}
2631
2632/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2633/// column list. Either a composite PRIMARY KEY or a UNIQUE
2634/// (single- or multi-column).
2635#[derive(Debug, Clone, PartialEq)]
2636pub enum TableConstraint {
2637    /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2638    /// referenced column. Engine builds a BTree index named
2639    /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2640    PrimaryKey {
2641        name: Option<String>,
2642        columns: Vec<String>,
2643        /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2644        /// Round 621 consumed the clauses; these carry them.
2645        deferrable: bool,
2646        initially_deferred: bool,
2647    },
2648    /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2649    /// named `<table>_<leading_col>_key` (single-column) or
2650    /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2651    /// uniqueness on INSERT.
2652    Unique {
2653        name: Option<String>,
2654        columns: Vec<String>,
2655        /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2656        /// G10). PG 15+ flips the NULL handling so any number of
2657        /// NULL rows collide on the constraint. Default is
2658        /// `false` (NULLS DISTINCT, standard SQL behaviour).
2659        nulls_not_distinct: bool,
2660        /// v7.39 (round 711) — see PrimaryKey.
2661        deferrable: bool,
2662        initially_deferred: bool,
2663        /// v7.40.0 — MySQL's per-column index prefix on a
2664        /// `UNIQUE KEY k (b(4))`, aligned with `columns`. Unlike a
2665        /// plain KEY's, this one CHANGES what the constraint accepts:
2666        /// MySQL rejects two rows sharing the first four characters.
2667        /// Empty for every PostgreSQL spelling.
2668        prefix_lengths: Vec<Option<u32>>,
2669    },
2670    /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2671    /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2672    /// this same variant at parse time. Engine evaluates the
2673    /// predicate against each INSERT/UPDATE candidate row; a
2674    /// false / NULL result rejects the mutation.
2675    /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2676    /// PG adds such a constraint without scanning the existing rows: new
2677    /// rows are checked, the ones already there are grandfathered in, and
2678    /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2679    /// scans and flips it. pg_dump emits the suffix for exactly those, so
2680    /// validating them on restore would refuse a dump PG itself produced.
2681    Check {
2682        name: Option<String>,
2683        expr: Expr,
2684        not_valid: bool,
2685    },
2686    /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2687    /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2688    /// every element (the booking/scheduling non-overlap constraint,
2689    /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2690    /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2691    /// enforcement doesn't build the index yet). Each element pairs a
2692    /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2693    Exclude {
2694        name: Option<String>,
2695        method: Option<String>,
2696        elements: Vec<(String, String)>,
2697    },
2698    /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2699    /// non-unique secondary-index declaration inline in CREATE
2700    /// TABLE. Engine builds a BTree index on the leading column
2701    /// (composite columns parse but only the leading column is
2702    /// honoured at v7.15 — matches the existing
2703    /// `CreateIndexStatement::extra_columns` semantics). Useful
2704    /// for `mysql/blog`-style schemas that lean on routine
2705    /// secondary indexes for ORM lookups.
2706    Index {
2707        name: Option<String>,
2708        columns: Vec<String>,
2709        /// v7.40.0 — MySQL's per-column index prefix, `KEY k (b(4))`,
2710        /// positionally aligned with `columns`. `None` for a column
2711        /// written without one, which is every PostgreSQL index key.
2712        ///
2713        /// It was skipped by the parser and dropped, so the declaration
2714        /// was accepted and the index built over the whole column with
2715        /// nothing recording that a prefix had been asked for —
2716        /// `SHOW INDEX` then reported `Sub_part` NULL and
2717        /// `SHOW CREATE TABLE` printed `(b)` where MySQL 9.7.2 prints
2718        /// `(b(4))`.
2719        prefix_lengths: Vec<Option<u32>>,
2720    },
2721    /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2722    /// (cols)` inline declaration. Pre-v7.17 the parser
2723    /// silently dropped these so MyISAM-imported FULLTEXT
2724    /// indexes vanished; v7.17 routes them through the
2725    /// existing tsvector-GIN engine path so MATCH AGAINST
2726    /// queries get a real inverted index instead of falling
2727    /// back to a full scan. Multi-column FULLTEXT KEYs build
2728    /// one GIN per column at v7.17 (per-column posting lists);
2729    /// the leading column drives query planning.
2730    FulltextIndex {
2731        name: Option<String>,
2732        columns: Vec<String>,
2733    },
2734}
2735
2736#[derive(Debug, Clone, PartialEq)]
2737#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2738pub struct ColumnDef {
2739    pub name: String,
2740    pub ty: ColumnTypeName,
2741    pub nullable: bool,
2742    /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2743    /// evaluates this once (with an empty row) and caches the resulting
2744    /// `Value` on the column schema.
2745    pub default: Option<Expr>,
2746    /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2747    /// per such column and fills the slot when INSERT leaves it
2748    /// unbound (omitted from a column-list INSERT or explicitly NULL).
2749    pub auto_increment: bool,
2750    /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2751    /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2752    /// an implicit BTree index named `<table>_pkey` over this
2753    /// column at CREATE TABLE time, satisfying the parent-side
2754    /// index requirement for any FOREIGN KEY pointing at it.
2755    pub is_primary_key: bool,
2756    /// v7.13.0 — inline `UNIQUE` column constraint
2757    /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2758    /// into a single-column `TableConstraint::Unique` so the
2759    /// engine path stays uniform with table-level UNIQUE.
2760    pub is_unique: bool,
2761    /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2762    /// inline column constraint: treat NULL keys as equal so only one NULL
2763    /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2764    /// `TableConstraint::Unique { nulls_not_distinct }`.
2765    pub unique_nulls_not_distinct: bool,
2766    /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2767    /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2768    /// since this round so the fold into the table-level constraint keeps it.
2769    pub constraint_deferrable: bool,
2770    pub constraint_initially_deferred: bool,
2771    /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2772    /// (mailrs round-5 G3). Stored alongside the column so the
2773    /// CREATE TABLE handler can fold these into table-level
2774    /// CHECK constraints. Multiple inline CHECKs on the same
2775    /// column are concatenated with AND at the table level.
2776    pub check: Option<Expr>,
2777    /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2778    /// parser sees an unknown column-type ident (anything not in
2779    /// the built-in `parse_column_type_name` table), it sets
2780    /// `ty = ColumnTypeName::Text` and records the original name
2781    /// here. The engine resolves at CREATE TABLE time: if a
2782    /// catalog enum/domain with this name exists, the column is
2783    /// bound to it (label-checked on INSERT for enums; CHECK-
2784    /// constrained for domains); otherwise the CREATE TABLE
2785    /// errors with "unknown type".
2786    pub user_type_ref: Option<String>,
2787    /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2788    /// CURRENT_TIMESTAMP` column attribute. When set, an
2789    /// UPDATE that does NOT explicitly bind this column
2790    /// overrides the new value with `now()` (engine clock).
2791    /// Pre-v7.17 SPG silently accepted the syntax and never
2792    /// fired the override — `updated_at` columns from mysqldump
2793    /// stayed pinned at their initial DEFAULT forever, an
2794    /// audit Tier-S silent-failure. Generalised as a stored
2795    /// expression source so future shapes (`ON UPDATE
2796    /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2797    /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2798    pub on_update_runtime: Option<Expr>,
2799    /// v7.17.0 Phase 2.5 — text collation derived from the
2800    /// post-fix `COLLATE <name>` clause (and / or the table-level
2801    /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2802    /// per column). Pre-2.5 SPG accepted the clause and
2803    /// discarded the name, leaving every column byte-compared
2804    /// — a Tier-S silent failure when the customer expected
2805    /// `_ci` / `case_insensitive` semantics. Parser normalises
2806    /// the raw collation name into the variants in `Collation`.
2807    /// Default `Binary` preserves the legacy compare path.
2808    pub collation: Collation,
2809    /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2810    /// explicit `COLLATE <name>` clause rather than the default. Under the
2811    /// MySQL dialect a text column with NO explicit clause takes the
2812    /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2813    /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2814    /// flag is the only thing that tells them apart.
2815    pub collation_explicit: bool,
2816    /// v7.39 (round 676) — the collation name AS WRITTEN, because
2817    /// `collation` above cannot carry it: `Collation` is a two-variant
2818    /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2819    /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2820    /// tell them apart.
2821    pub collation_name: Option<String>,
2822    /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2823    /// 4.4 SPG accepted and discarded the keyword, leaving
2824    /// negative values silently accepted on a column the
2825    /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2826    /// rejects negative INSERT / UPDATE values on UNSIGNED int
2827    /// columns. SPG widening to `u64`-shaped storage is out of
2828    /// v7.17 scope; the upper bound remains the signed-type max
2829    /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2830    /// exceeds what every mailrs / Rails app actually uses.
2831    pub is_unsigned: bool,
2832    /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2833    /// value list captured at parse time. When `Some`, the parser
2834    /// recognised `ENUM(...)` in the type slot; the engine
2835    /// validates INSERT cells against this list at
2836    /// column_def_to_schema time and persists the variants on
2837    /// `ColumnSchema.inline_enum_variants`. None for all
2838    /// non-ENUM columns.
2839    pub inline_enum_variants: Option<Vec<String>>,
2840    /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2841    /// value list. Distinct from ENUM (subset semantics rather
2842    /// than pick-one). None for all non-SET columns.
2843    pub inline_set_variants: Option<Vec<String>>,
2844    /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2845    /// STORED` computed-column source. When `Some`, the engine
2846    /// stores the Display-form of the parsed expression on
2847    /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2848    /// and re-evaluates the expression against every INSERT /
2849    /// UPDATE candidate row, overwriting whatever the caller
2850    /// supplied for this column. Boxed to keep `ColumnDef` from
2851    /// blowing past the `large_enum_variant` clippy ceiling
2852    /// (`Expr` widens with vector literals).
2853    pub generated_stored_expr: Option<Box<Expr>>,
2854    /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2855    /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2856    /// `auto_increment`; this additionally marks the ALWAYS one, whose
2857    /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2858    /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2859    /// VALUE`. Only meaningful when the column is also an identity column.
2860    pub identity_always: bool,
2861    /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2862    /// integer width (TINYINT / MEDIUMINT), captured before the type
2863    /// collapses to SmallInt / Int. The engine copies it to
2864    /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2865    /// path can enforce the real range. None for every other column and
2866    /// under the PG dialect.
2867    pub mysql_int_width: Option<MysqlIntWidth>,
2868    /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2869    /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2870    /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2871    /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2872    /// CREATE TABLE time so the write path can truncate and the render path
2873    /// can pad. None under the PG dialect, where temporal columns keep full
2874    /// microseconds.
2875    pub mysql_fsp: Option<u8>,
2876    /// v7.39.2 — the column was written `TIMESTAMP` rather than
2877    /// `DATETIME` in a MySQL session. The engine copies it to
2878    /// `ColumnSchema.mysql_declared_timestamp` at CREATE TABLE.
2879    pub mysql_declared_timestamp: bool,
2880    /// v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair, copied to
2881    /// `ColumnSchema.mysql_float_md` at CREATE TABLE.
2882    pub mysql_float_md: Option<(u8, u8)>,
2883}
2884
2885/// v7.17.0 Phase 2.5 — text collation classification surfaced
2886/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2887/// engine bridges between the two at CREATE TABLE time.
2888///
2889/// Recognised collation-name patterns (case-insensitive):
2890///   * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase`         → CaseInsensitive
2891///   * Everything else (`C`, `POSIX`, `default`,
2892///     `pg_catalog.default`, `*_cs`, `*_bin`, unknown names)   → Binary
2893#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2894pub enum Collation {
2895    Binary,
2896    CaseInsensitive,
2897}
2898
2899/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2900/// integer width for a column whose `ColumnTypeName` is too wide to carry
2901/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2902/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2903/// TABLE time. Only recorded under the MySQL dialect.
2904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2905pub enum MysqlIntWidth {
2906    Tiny,
2907    Small,
2908    Medium,
2909    Int,
2910    /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2911    Big,
2912}
2913
2914#[allow(clippy::derivable_impls)]
2915impl Default for Collation {
2916    fn default() -> Self {
2917        Self::Binary
2918    }
2919}
2920
2921impl Collation {
2922    /// Classify a `COLLATE <name>` ident into one of the supported
2923    /// variants. Empty / unknown names fall back to `Binary` —
2924    /// matches the pre-2.5 silent-accept behaviour for snapshots
2925    /// that load through but don't actually depend on the
2926    /// collation semantics.
2927    #[must_use]
2928    pub fn from_collation_name(name: &str) -> Self {
2929        let lc = name.trim().to_ascii_lowercase();
2930        // Strip any quotes / schema-qualifier the parser left on
2931        // (e.g. `pg_catalog.default`).
2932        let bare = lc
2933            .trim_matches(|c: char| c == '"' || c == '\'')
2934            .rsplit('.')
2935            .next()
2936            .unwrap_or("");
2937        if bare.is_empty() {
2938            return Self::Binary;
2939        }
2940        if bare == "case_insensitive" || bare == "nocase" {
2941            return Self::CaseInsensitive;
2942        }
2943        // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2944        // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2945        if bare.ends_with("_ci") {
2946            return Self::CaseInsensitive;
2947        }
2948        Self::Binary
2949    }
2950}
2951
2952/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2953/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2954/// parse into this shape — the column-level form has a single-entry
2955/// `columns` / `parent_columns`.
2956#[derive(Debug, Clone, PartialEq)]
2957pub struct ForeignKeyConstraint {
2958    /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2959    /// today but parses + stores it so a future ALTER TABLE DROP
2960    /// CONSTRAINT can target by name (v7.6.8).
2961    pub name: Option<String>,
2962    /// Local columns participating in the FK (≥ 1).
2963    pub columns: Vec<String>,
2964    /// Referenced parent table.
2965    pub parent_table: String,
2966    /// Referenced parent columns. Must have the same arity as
2967    /// `columns`; engine validates parent has a PK / UNIQUE index
2968    /// on exactly this column set (v7.6.1).
2969    pub parent_columns: Vec<String>,
2970    /// `ON DELETE` action. Defaults to `Restrict` if absent.
2971    pub on_delete: FkAction,
2972    /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2973    pub on_update: FkAction,
2974    /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2975    pub match_type: MatchType,
2976    /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2977    /// dropped on the floor, so a constraint declared DEFERRABLE was
2978    /// enforced immediately and a circular-FK migration could not load.
2979    pub deferrable: bool,
2980    /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2981    /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2982    pub initially_deferred: bool,
2983}
2984
2985/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2986/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2987/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2988#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2989pub enum MatchType {
2990    #[default]
2991    Simple,
2992    Full,
2993}
2994
2995/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2996#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2997pub enum FkAction {
2998    /// Reject the parent mutation if any child row references it.
2999    /// SQL spec default; SPG default when no clause is given.
3000    Restrict,
3001    /// Recursively propagate the parent's delete / update to the
3002    /// child rows. Same TX.
3003    Cascade,
3004    /// Set the child FK column(s) to NULL. Requires the FK columns
3005    /// to be NULL-able.
3006    SetNull,
3007    /// Set the child FK column(s) to their declared DEFAULT.
3008    /// Requires the child column(s) to have DEFAULT.
3009    SetDefault,
3010    /// SQL spec `NO ACTION` (deferred check). SPG treats this as
3011    /// `Restrict` because the single-writer model has no deferred
3012    /// constraint window; the keyword is accepted for compatibility.
3013    NoAction,
3014}
3015
3016/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
3017/// optional `USING <encoding>` clause; omitting it keeps the
3018/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
3019/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
3020/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
3021/// binary16 (2× compression, ~3 decimal digits of precision).
3022#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3023pub enum VecEncoding {
3024    /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
3025    /// uncompressed `vector` type wire / storage layout.
3026    #[default]
3027    F32,
3028    /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
3029    /// `spg_storage::quantize::Sq8Vector` for the math + recall
3030    /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
3031    /// dim ≥ 32).
3032    Sq8,
3033    /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
3034    /// per-element. DDL keyword `HALF` (pgvector convention).
3035    /// Bit-exact dequantise to f32 at the storage layer; no
3036    /// rerank pass needed for kNN search.
3037    F16,
3038}
3039
3040impl fmt::Display for VecEncoding {
3041    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3042        match self {
3043            Self::F32 => f.write_str("F32"),
3044            Self::Sq8 => f.write_str("SQ8"),
3045            // pgvector convention: DDL keyword is `HALF`, not `F16`.
3046            Self::F16 => f.write_str("HALF"),
3047        }
3048    }
3049}
3050
3051/// SQL-level type names. The mapping to the storage runtime's `DataType`
3052/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
3053#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3054pub enum ColumnTypeName {
3055    /// v7.39 (round 291) — PG's `name`, the identifier type its
3056    /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
3057    /// answered `type "name" does not exist` to.
3058    Name,
3059    /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
3060    /// 32-bit wrapping counter the row header carries; `xid8` is the
3061    /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
3062    /// SPG answered `type "xid" does not exist` to.
3063    Xid,
3064    Xid8,
3065    /// v7.39 (round 667) — `OID`. `XID` was already a column type here
3066    /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
3067    /// `type "oid" does not exist` while `t(x XID)` built fine.
3068    Oid,
3069    SmallInt,
3070    Int,
3071    BigInt,
3072    Float,
3073    /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
3074    /// IEEE. It used to map to [`Self::Float`] on the theory that a
3075    /// wider float is harmless, but the width is observable: a `real`
3076    /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
3077    /// answered false where PG answers true.
3078    Real,
3079    Text,
3080    /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
3081    Varchar(u32),
3082    /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
3083    Char(u32),
3084    Bool,
3085    /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
3086    /// `USING <encoding>` clause; omitting it surfaces as
3087    /// `encoding = VecEncoding::F32` (the pre-v6 default).
3088    Vector {
3089        dim: u32,
3090        encoding: VecEncoding,
3091    },
3092    /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
3093    /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
3094    /// v7.39 (round 271) — scale widened to u16 alongside the value's.
3095    /// v7.39 (round 272) — precision too: PG's runs to 1000.
3096    /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
3097    /// a negative one rounds to tens / hundreds. A VALUE's display scale
3098    /// stays unsigned.
3099    Numeric(u16, i16),
3100    /// `DATE` — calendar day, no time-of-day component.
3101    Date,
3102    /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
3103    /// precision.
3104    Timestamp,
3105    /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
3106    /// stores all timestamps as UTC microseconds-since-epoch and
3107    /// does not carry per-row offset (PG's internal representation
3108    /// is the same — TZ is a display convention). The distinction
3109    /// from `TIMESTAMP` exists for the PG-wire layer to advertise
3110    /// OID 1184 so sqlx-style clients decode into
3111    /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
3112    Timestamptz,
3113    /// v4.9 `JSON` — text-backed JSON document. No parse-time
3114    /// validation; the engine round-trips the literal verbatim.
3115    /// PG OID 114 on the wire.
3116    Json,
3117    /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
3118    /// PG OID 3802 on the wire so sqlx-style binary-typed clients
3119    /// decode without a custom type registration.
3120    Jsonb,
3121    /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
3122    /// Literal forms (decoded by the engine at coercion time):
3123    ///   - PG hex form: `'\xDEADBEEF'`
3124    ///   - Escape form: `'foo\\000bar'` (backslash octal triples)
3125    Bytes,
3126    /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
3127    /// OID 1009. Literal forms accepted by the parser:
3128    ///   - `ARRAY['a', 'b', NULL]`
3129    ///   - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
3130    ///     form at coerce time)
3131    TextArray,
3132    /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
3133    /// 1007. Same literal forms as TEXT[] (substituting integer
3134    /// elements).
3135    IntArray,
3136    /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
3137    /// OID 1016.
3138    BigIntArray,
3139    /// v7.40.0 `OID[]` — single-dimension oid array. PG wire OID
3140    /// 1028.
3141    ///
3142    /// `DataType::OidArray` and its value, codec tag, wire encoding
3143    /// and every naming surface have existed since v7.39 (round 694);
3144    /// what was missing was only the DDL spelling, so
3145    /// `CREATE TABLE t (c oid[])` answered `Oid[] not yet supported`
3146    /// while PostgreSQL 18.6 accepts it. Capability present, routing
3147    /// absent — the same shape as this repository's other hand-kept
3148    /// lists.
3149    OidArray,
3150    /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
3151    /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
3152    /// external form). G-CRIT-3.
3153    TsVector,
3154    /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
3155    /// wire OID 3615.
3156    TsQuery,
3157    /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
3158    /// Literal input accepts canonical hyphenated, unhyphenated,
3159    /// uppercase, and `{...}`-braced forms; display normalises to
3160    /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
3161    /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
3162    /// gen_random_uuid()`.
3163    Uuid,
3164    /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
3165    /// microseconds since 00:00:00. PG wire OID 1083. Literal
3166    /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
3167    /// (6-digit microsecond precision). Display normalises to
3168    /// the canonical `HH:MM:SS[.ffffff]`.
3169    Time,
3170    /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
3171    /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
3172    /// PG OID; advertised as INT4 on the wire. Display always
3173    /// 4 digits zero-padded.
3174    Year,
3175    /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
3176    /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
3177    /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
3178    /// Offset range: ±14 hours.
3179    TimeTz,
3180    /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
3181    /// (locale-independent storage). Wire OID 790. Literal input
3182    /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
3183    /// major units), optional leading `-`. Display: en_US locale.
3184    Money,
3185    /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
3186    /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
3187    /// — the engine bridges to `DataType::Range(RangeKind)`.
3188    Range(RangeKindAst),
3189    /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
3190    /// `text => text` map with NULL value support.
3191    Hstore,
3192    /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
3193    IntArray2D,
3194    BigIntArray2D,
3195    TextArray2D,
3196    /// v7.39 (read01 round 75) — `bool[][]`.
3197    BoolArray2D,
3198    /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
3199    /// three-field {months, days, micros} struct (PG-byte-equal),
3200    /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
3201    /// β-P2 `INTERVAL` was runtime-only — literal in expression
3202    /// position but rejected at CREATE TABLE.
3203    Interval,
3204    /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
3205    /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
3206    /// PG external form quotes each non-NULL element because
3207    /// interval text contains spaces / colons
3208    /// (`{"1 day","24:00:00",NULL}`).
3209    IntervalArray,
3210    /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3211    /// mirrors a scalar `ColumnTypeName` that already existed.
3212    BoolArray,
3213    SmallIntArray,
3214    FloatArray,
3215    NumericArray,
3216    DateArray,
3217    TimestampArray,
3218    TimestamptzArray,
3219    UuidArray,
3220    JsonArray,
3221    JsonbArray,
3222    BytesArray,
3223    VarcharArray,
3224    CharArray,
3225    /// v7.40.0 — five array spellings PG 18.6 accepts at
3226    /// `CREATE TABLE` and SPG refused at the type name. The
3227    /// element types were all present; only the `[]` step was.
3228    RealArray,
3229    TimeArray,
3230    TimeTzArray,
3231    InetArray,
3232    XmlArray,
3233    /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3234    /// as `Range(RangeKindAst)` — one column type variant covers
3235    /// all six builtin multiranges, kind pins the element type.
3236    /// Wire OIDs in pgwire.
3237    Multirange(RangeKindAst),
3238    /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3239    /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3240    /// Wire OIDs in pgwire.
3241    Point,
3242    Lseg,
3243    Path,
3244    PgBox,
3245    Polygon,
3246    Line,
3247    Circle,
3248    /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3249    Inet,
3250    Cidr,
3251    Macaddr,
3252    Macaddr8,
3253    /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3254    Bit(u32),
3255    /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3256    BitVarying(u32),
3257    Xml,
3258    Char1,
3259    MoneyArray,
3260}
3261
3262/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3263/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3264/// crate doesn't depend on storage. Bridged at engine boundary.
3265#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3266pub enum RangeKindAst {
3267    Int4,
3268    Int8,
3269    Num,
3270    Ts,
3271    TsTz,
3272    Date,
3273}
3274
3275impl fmt::Display for ColumnTypeName {
3276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3277        match self {
3278            Self::SmallInt => f.write_str("SMALLINT"),
3279            Self::Int => f.write_str("INT"),
3280            Self::BigInt => f.write_str("BIGINT"),
3281            Self::Float => f.write_str("FLOAT"),
3282            Self::Real => f.write_str("REAL"),
3283            Self::Text => f.write_str("TEXT"),
3284            Self::Name => f.write_str("name"),
3285            Self::Xid => f.write_str("xid"),
3286            Self::Xid8 => f.write_str("xid8"),
3287            Self::Oid => f.write_str("oid"),
3288            Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3289            Self::Char(n) => write!(f, "CHAR({n})"),
3290            Self::Bool => f.write_str("BOOL"),
3291            Self::Vector { dim, encoding } => match encoding {
3292                VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3293                VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3294                VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3295            },
3296            Self::Json => f.write_str("JSON"),
3297            Self::Jsonb => f.write_str("JSONB"),
3298            Self::Bytes => f.write_str("BYTEA"),
3299            Self::TextArray => f.write_str("TEXT[]"),
3300            Self::IntArray => f.write_str("INT[]"),
3301            Self::BigIntArray => f.write_str("BIGINT[]"),
3302            Self::OidArray => f.write_str("oid[]"),
3303            Self::TsVector => f.write_str("TSVECTOR"),
3304            Self::TsQuery => f.write_str("TSQUERY"),
3305            Self::Uuid => f.write_str("UUID"),
3306            Self::Numeric(p, s) => {
3307                if *s == 0 {
3308                    write!(f, "NUMERIC({p})")
3309                } else {
3310                    write!(f, "NUMERIC({p}, {s})")
3311                }
3312            }
3313            Self::Date => f.write_str("DATE"),
3314            Self::Timestamp => f.write_str("TIMESTAMP"),
3315            Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3316            Self::Time => f.write_str("TIME"),
3317            Self::Year => f.write_str("YEAR"),
3318            Self::TimeTz => f.write_str("TIMETZ"),
3319            Self::Money => f.write_str("MONEY"),
3320            Self::Range(k) => f.write_str(match k {
3321                RangeKindAst::Int4 => "INT4RANGE",
3322                RangeKindAst::Int8 => "INT8RANGE",
3323                RangeKindAst::Num => "NUMRANGE",
3324                RangeKindAst::Ts => "TSRANGE",
3325                RangeKindAst::TsTz => "TSTZRANGE",
3326                RangeKindAst::Date => "DATERANGE",
3327            }),
3328            Self::Hstore => f.write_str("HSTORE"),
3329            Self::Interval => f.write_str("INTERVAL"),
3330            Self::IntervalArray => f.write_str("INTERVAL[]"),
3331            Self::BoolArray => f.write_str("BOOL[]"),
3332            Self::SmallIntArray => f.write_str("SMALLINT[]"),
3333            Self::FloatArray => f.write_str("FLOAT[]"),
3334            Self::NumericArray => f.write_str("NUMERIC[]"),
3335            Self::DateArray => f.write_str("DATE[]"),
3336            Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3337            Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3338            Self::UuidArray => f.write_str("UUID[]"),
3339            Self::JsonArray => f.write_str("JSON[]"),
3340            Self::JsonbArray => f.write_str("JSONB[]"),
3341            Self::BytesArray => f.write_str("BYTEA[]"),
3342            Self::VarcharArray => f.write_str("VARCHAR[]"),
3343            Self::CharArray => f.write_str("CHAR[]"),
3344            Self::RealArray => f.write_str("REAL[]"),
3345            Self::TimeArray => f.write_str("TIME[]"),
3346            Self::TimeTzArray => f.write_str("TIMETZ[]"),
3347            Self::InetArray => f.write_str("INET[]"),
3348            Self::XmlArray => f.write_str("XML[]"),
3349            Self::Multirange(k) => f.write_str(match k {
3350                RangeKindAst::Int4 => "INT4MULTIRANGE",
3351                RangeKindAst::Int8 => "INT8MULTIRANGE",
3352                RangeKindAst::Num => "NUMMULTIRANGE",
3353                RangeKindAst::Ts => "TSMULTIRANGE",
3354                RangeKindAst::TsTz => "TSTZMULTIRANGE",
3355                RangeKindAst::Date => "DATEMULTIRANGE",
3356            }),
3357            Self::Point => f.write_str("POINT"),
3358            Self::Lseg => f.write_str("LSEG"),
3359            Self::Path => f.write_str("PATH"),
3360            Self::PgBox => f.write_str("BOX"),
3361            Self::Polygon => f.write_str("POLYGON"),
3362            Self::Line => f.write_str("LINE"),
3363            Self::Circle => f.write_str("CIRCLE"),
3364            Self::Inet => f.write_str("INET"),
3365            Self::Cidr => f.write_str("CIDR"),
3366            Self::Macaddr => f.write_str("MACADDR"),
3367            Self::Macaddr8 => f.write_str("MACADDR8"),
3368            Self::Bit(0) => f.write_str("BIT"),
3369            Self::Bit(n) => write!(f, "BIT({n})"),
3370            Self::BitVarying(0) => f.write_str("VARBIT"),
3371            Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3372            Self::Xml => f.write_str("XML"),
3373            Self::Char1 => f.write_str("\"char\""),
3374            Self::MoneyArray => f.write_str("MONEY[]"),
3375            Self::IntArray2D => f.write_str("INT[][]"),
3376            Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3377            Self::TextArray2D => f.write_str("TEXT[][]"),
3378            Self::BoolArray2D => f.write_str("BOOL[][]"),
3379        }
3380    }
3381}
3382
3383/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3384/// engine evaluates `expr` per matched row in the table's row order
3385/// and rewrites cells in place. Indexed columns are dropped + re-
3386/// inserted into the affected B-tree on each row change.
3387/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3388/// tail on a DML statement. Boxed off the statement struct so the PG-only
3389/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3390/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3391/// the identical meaning, so both share this one payload rather than each
3392/// growing its own.
3393#[derive(Debug, Clone, PartialEq)]
3394pub struct DmlOrderLimit {
3395    pub order_by: Vec<OrderBy>,
3396    pub limit: Option<u32>,
3397}
3398
3399/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3400/// FROM, kept so the engine can finish the job.
3401///
3402/// The parser rewrites the statement onto correlated subqueries, and it
3403/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3404/// name belongs to the target or to a source needs their column lists,
3405/// which parse time does not have. Carrying the clause lets the engine
3406/// — which has the catalog — resolve the rest.
3407#[derive(Debug, Clone, PartialEq)]
3408pub struct UpdateFromSources {
3409    pub from: FromClause,
3410    pub sub_where: Option<Expr>,
3411}
3412
3413#[derive(Debug, Clone, PartialEq)]
3414pub struct UpdateStatement {
3415    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3416    /// level UPDATE. Empty for a plain UPDATE.
3417    pub ctes: Vec<Cte>,
3418    pub table: String,
3419    /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3420    /// to `t`'s own rows and not to anything that descends from it.
3421    ///
3422    /// Round 644 taught the FROM clause the keyword and left DML behind
3423    /// because it needed a field here, and this struct carries a warning
3424    /// that round 413 measured widening it in place overflowing the
3425    /// parser's nesting stack. That warning was about `from_sources`, a
3426    /// struct wide enough to need boxing; a `bool` lands in the padding
3427    /// already present — same as `CreateTableStatement::temporary`.
3428    ///
3429    /// It also earns its keep beyond the spelling: the inheritance
3430    /// fan-out needs a way to say "the parent's own rows" as a
3431    /// statement, or running one on the parent recurses forever.
3432    pub only: bool,
3433    /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3434    /// statement's expressions refer to the target row by. PG allows the
3435    /// bare spelling here (unlike INSERT, which requires AS).
3436    pub alias: Option<String>,
3437    pub assignments: Vec<(String, Expr)>,
3438    /// v7.39 (round 533) — boxed: round 413 measured that widening this
3439    /// struct in place overflows the parser's nesting stack.
3440    pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3441    pub where_: Option<Expr>,
3442    /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3443    /// mutate the first `limit` rows in the given order. PG has no such
3444    /// clause; the parser accepts it only under the MySQL dialect. Boxed
3445    /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3446    /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3447    /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3448    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3449    /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3450    /// clause (legacy CommandComplete path). Some = engine
3451    /// evaluates the projection over each mutated row and
3452    /// streams the result as a Rows QueryResult.
3453    pub returning: Option<Vec<SelectItem>>,
3454}
3455
3456/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3457/// from the active catalog and prunes them from every index.
3458#[derive(Debug, Clone, PartialEq)]
3459pub struct DeleteStatement {
3460    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3461    /// level DELETE. Empty for a plain DELETE.
3462    pub ctes: Vec<Cte>,
3463    pub table: String,
3464    /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3465    /// to `t`'s own rows and not to anything that descends from it.
3466    ///
3467    /// Round 644 taught the FROM clause the keyword and left DML behind
3468    /// because it needed a field here, and this struct carries a warning
3469    /// that round 413 measured widening it in place overflowing the
3470    /// parser's nesting stack. That warning was about `from_sources`, a
3471    /// struct wide enough to need boxing; a `bool` lands in the padding
3472    /// already present — same as `CreateTableStatement::temporary`.
3473    ///
3474    /// It also earns its keep beyond the spelling: the inheritance
3475    /// fan-out needs a way to say "the parent's own rows" as a
3476    /// statement, or running one on the parent recurses forever.
3477    pub only: bool,
3478    /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3479    /// the WHERE / RETURNING expressions refer to the target row by.
3480    pub alias: Option<String>,
3481    pub where_: Option<Expr>,
3482    /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3483    /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3484    /// form (round 413), so it shares that payload — and it is boxed for
3485    /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3486    /// statement tipped the parser's 512 KiB nesting stack.
3487    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3488    /// v7.9.4 — `RETURNING <projection>`.
3489    pub returning: Option<Vec<SelectItem>>,
3490}
3491
3492/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3493/// One WHEN clause fires per source row depending on whether the
3494/// `on` condition matched any target row(s); the executor walks
3495/// `clauses` in declaration order and fires the first whose
3496/// `matched` kind and optional `condition` are both satisfied.
3497#[derive(Debug, Clone, PartialEq)]
3498pub struct MergeStatement {
3499    /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3500    /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3501    /// in PG). Each CTE materialises before the merge runs and its alias
3502    /// resolves as a source relation.
3503    pub ctes: Vec<Cte>,
3504    pub target: String,
3505    pub target_alias: Option<String>,
3506    pub source: String,
3507    pub source_alias: Option<String>,
3508    /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3509    /// the engine materialises this SELECT for the source rows and `source`
3510    /// is empty; the alias (required by PG for a subquery source) is in
3511    /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3512    pub source_select: Option<Box<SelectStatement>>,
3513    /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3514    /// positional column-alias list after the source alias. Empty when
3515    /// the statement carries none; the engine renames the materialised
3516    /// source columns positionally (PG's rule).
3517    pub source_column_aliases: Vec<String>,
3518    pub on: Expr,
3519    pub clauses: Vec<MergeWhenClause>,
3520    /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3521    /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3522    /// target/source aliases. `None` = no RETURNING (the common form).
3523    pub returning: Option<Vec<SelectItem>>,
3524}
3525
3526#[derive(Debug, Clone, PartialEq)]
3527pub struct MergeWhenClause {
3528    pub matched: MergeMatched,
3529    /// Optional `AND <expr>` filter — when present, the clause
3530    /// only fires for the source rows whose match-pair satisfies
3531    /// the predicate.
3532    pub condition: Option<Expr>,
3533    pub action: MergeAction,
3534}
3535
3536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3537pub enum MergeMatched {
3538    Matched,
3539    /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3540    /// target row (the classic insert branch).
3541    NotMatched,
3542    /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3543    /// row no source row matches. Actions are UPDATE / DELETE / DO
3544    /// NOTHING only (INSERT is a syntax error, as in PG).
3545    NotMatchedBySource,
3546}
3547
3548#[derive(Debug, Clone, PartialEq)]
3549pub enum MergeAction {
3550    /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3551    /// explicit column list (the bare `INSERT VALUES (vals)`
3552    /// shape lands later).
3553    Insert {
3554        columns: Vec<String>,
3555        values: Vec<Expr>,
3556    },
3557    /// `UPDATE SET col = expr [, …]` — applied to every matched
3558    /// target row for the firing source row.
3559    Update { assignments: Vec<(String, Expr)> },
3560    /// `DELETE` — drop every matched target row.
3561    Delete,
3562    /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3563    /// the clause and SPG mirrors so a customer-side MERGE that
3564    /// uses it for branch-control doesn't error).
3565    DoNothing,
3566}
3567
3568#[derive(Debug, Clone, PartialEq)]
3569pub struct InsertStatement {
3570    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3571    /// level INSERT (writable CTE outer body). Empty for a plain
3572    /// INSERT. PG semantics: each CTE materialises before the
3573    /// outer INSERT runs, sharing the same transaction.
3574    pub ctes: Vec<Cte>,
3575    pub table: String,
3576    /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3577    /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3578    /// row by. PG requires the AS keyword in this position.
3579    pub alias: Option<String>,
3580    /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3581    /// `None`, every tuple is positional and must match the table arity.
3582    /// When `Some`, the engine maps each tuple slot to the named column and
3583    /// fills the rest with NULL (must be nullable).
3584    pub columns: Option<Vec<String>>,
3585    /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3586    /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3587    /// `select_source` is `Some` (the engine builds rows from the
3588    /// inner SELECT result set instead).
3589    pub rows: Vec<Vec<Expr>>,
3590    /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3591    /// round-5 G4). When present, `rows` is empty and the engine
3592    /// materialises the SELECT result, coerces each output tuple to
3593    /// the target column types, and inserts as a single batch.
3594    pub select_source: Option<Box<SelectStatement>>,
3595    /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3596    /// upsert clause. None = legacy INSERT (conflict raises a
3597    /// DuplicateKey error). mailrs migration blocker #2.
3598    pub on_conflict: Option<OnConflictClause>,
3599    /// v7.9.4 — `RETURNING <projection>`.
3600    pub returning: Option<Vec<SelectItem>>,
3601    /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3602    /// between the column list and VALUES. Governs how explicitly-supplied
3603    /// values interact with `GENERATED … AS IDENTITY` columns:
3604    ///   * `None` — default. A `GENERATED ALWAYS` identity column rejects
3605    ///     an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3606    ///   * `System` — override the ALWAYS restriction: the explicit value
3607    ///     is used verbatim, as for a `BY DEFAULT` column.
3608    ///   * `User` — ignore any explicit value on a `BY DEFAULT` identity
3609    ///     column and generate from the sequence instead (no effect on
3610    ///     non-identity columns).
3611    pub overriding: Overriding,
3612    /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3613    /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3614    /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3615    /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3616    /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3617    /// into a NOT NULL column becomes the type's default), and the engine
3618    /// cannot recover that intent from the conflict clause alone. A plain
3619    /// `bool` lands in this struct's existing padding, so the AST does not
3620    /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3621    pub mysql_ignore: bool,
3622}
3623
3624/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3625#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3626#[non_exhaustive]
3627pub enum Overriding {
3628    /// No `OVERRIDING` clause.
3629    #[default]
3630    None,
3631    /// `OVERRIDING SYSTEM VALUE`.
3632    System,
3633    /// `OVERRIDING USER VALUE`.
3634    User,
3635}
3636
3637/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3638#[derive(Debug, Clone, PartialEq)]
3639pub struct OnConflictClause {
3640    /// Local columns that identify the conflict (must match a
3641    /// UNIQUE / PRIMARY KEY index on the target table). Empty
3642    /// list means the user wrote `ON CONFLICT DO …` without a
3643    /// target — the engine arbitrates on every unique constraint
3644    /// (round 240).
3645    pub target_columns: Vec<String>,
3646    /// v7.39 (round 240) — the index predicate after the target list
3647    /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3648    /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3649    /// which satisfy any predicate, so it is parsed and carried but not
3650    /// consulted (recorded residual: partial-unique-index arbiters).
3651    pub index_where: Option<Expr>,
3652    /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3653    /// <name>`: the pg_dump conflict-target form. The engine
3654    /// resolves the name to the constraint's columns.
3655    pub constraint_name: Option<String>,
3656    /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3657    /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3658    /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3659    /// `ON CONFLICT DO UPDATE` is refused (42601).
3660    pub mysql_lowered: bool,
3661    /// The action on conflict.
3662    pub action: OnConflictAction,
3663}
3664
3665/// v7.9.7 — action on conflict.
3666#[derive(Debug, Clone, PartialEq)]
3667pub enum OnConflictAction {
3668    /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3669    /// silently skips conflicting ones.
3670    Nothing,
3671    /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3672    /// may reference `EXCLUDED.col` to read the incoming row's
3673    /// value (engine wires `EXCLUDED` as a virtual table).
3674    Update {
3675        assignments: Vec<(String, Expr)>,
3676        where_: Option<Expr>,
3677    },
3678}
3679
3680/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3681///
3682/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3683/// policies are spelled again here and mapped at the engine boundary.
3684/// v7.39 — the modes `BEGIN` / `START TRANSACTION` / `SET TRANSACTION`
3685/// / `SET SESSION CHARACTERISTICS` accept. `read_only` used to be parsed
3686/// and dropped on the floor, so `BEGIN READ ONLY` opened an ordinary
3687/// read-write transaction and every write in it was accepted.
3688///
3689/// `None` on either field means the statement did not name that mode, so
3690/// the session default applies — which is not the same as naming the
3691/// default explicitly.
3692#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3693pub struct TransactionModes {
3694    pub isolation: Option<IsolationLevel>,
3695    /// `Some(true)` = READ ONLY, `Some(false)` = READ WRITE.
3696    pub read_only: Option<bool>,
3697    /// v7.40.12 — `Some(true)` = DEFERRABLE, `Some(false)` = NOT
3698    /// DEFERRABLE. PG reports it through `transaction_deferrable`, and
3699    /// consults it only for a SERIALIZABLE READ ONLY transaction, where
3700    /// it may make the transaction WAIT for a snapshot it can run
3701    /// against without serialization failures. SPG reports the property
3702    /// and does not defer; the difference a client can observe is a
3703    /// wait, never a different answer.
3704    pub deferrable: Option<bool>,
3705}
3706
3707/// v7.39 — what a read-only transaction refuses, and what PG calls it.
3708///
3709/// SPG did not enforce read-only transactions at all: `BEGIN READ ONLY;
3710/// INSERT …` answered `INSERT 0 1` and committed, and
3711/// `default_transaction_read_only = on` changed nothing. Both GUCs were
3712/// in the inventory, so a session could set one, read it back, and be
3713/// told it held a guarantee nothing was enforcing. Applications open
3714/// read-only transactions as a SAFETY measure — a reporting connection,
3715/// a read-only leg in a pool, a "this path must not write" discipline —
3716/// so accepting the writes is the worst possible answer.
3717///
3718/// `Some(tag)` means refuse with PG's message, `cannot execute {tag} in
3719/// a read-only transaction` (SQLSTATE 25006). Every tag below was read
3720/// back from PostgreSQL 18.6 by running the statement inside
3721/// `BEGIN READ ONLY`, one statement per transaction so no error could be
3722/// attributed to the wrong line.
3723///
3724/// Several answers were not what one would guess, which is why they were
3725/// measured rather than reasoned:
3726///
3727///   * `CREATE TEMP TABLE` is REFUSED, tagged `CREATE TABLE`.
3728///   * `NOTIFY`, `LISTEN` and `REINDEX` are ALLOWED.
3729///   * `PREPARE` of an INSERT is ALLOWED — only the EXECUTE writes.
3730///   * `UPDATE … WHERE false`, which changes nothing, is still REFUSED:
3731///     the verb decides, not the row count.
3732///   * `GRANT`, `COMMENT ON` and `SELECT … FOR SHARE` are all REFUSED.
3733///
3734/// The match is exhaustive on purpose. A new statement cannot be added
3735/// without deciding here whether it writes, which is the failure this
3736/// repository keeps meeting: one member of a family gets handled and its
3737/// siblings quietly do not.
3738impl Statement {
3739    #[must_use]
3740    pub fn read_only_violation_tag(&self) -> Option<&'static str> {
3741        match self {
3742            // v7.39.9 — MySQL's RENAME TABLE is DDL, refused read-only
3743            // for the same reason ALTER TABLE … RENAME TO is.
3744            Self::RenameTables(_) => Some("RENAME TABLE"),
3745            // ---- writes rows -------------------------------------------
3746            Self::Insert { .. } => Some("INSERT"),
3747            Self::Update { .. } => Some("UPDATE"),
3748            Self::Delete { .. } => Some("DELETE"),
3749            Self::Merge { .. } => Some("MERGE"),
3750            Self::Truncate { .. } => Some("TRUNCATE TABLE"),
3751            Self::CopyFromFile { .. } => Some("COPY FROM"),
3752
3753            // A SELECT that takes row locks writes lock state, and PG
3754            // names the strength it was asked for.
3755            Self::Select(sel) => sel.locking.as_ref().map(|l| match l.strength {
3756                LockStrength::Update => "SELECT FOR UPDATE",
3757                LockStrength::NoKeyUpdate => "SELECT FOR NO KEY UPDATE",
3758                LockStrength::Share => "SELECT FOR SHARE",
3759                LockStrength::KeyShare => "SELECT FOR KEY SHARE",
3760            }),
3761
3762            // ---- changes the catalog -----------------------------------
3763            Self::CreateTable { .. } => Some("CREATE TABLE"),
3764            Self::DropTable { .. } => Some("DROP TABLE"),
3765            Self::AlterTable { .. } => Some("ALTER TABLE"),
3766            Self::CreateIndex { .. } => Some("CREATE INDEX"),
3767            Self::DropIndex { .. } => Some("DROP INDEX"),
3768            Self::AlterIndex { .. } => Some("ALTER INDEX"),
3769            Self::CreateView { .. } => Some("CREATE VIEW"),
3770            Self::DropView { .. } => Some("DROP VIEW"),
3771            Self::CreateMaterializedView { .. } => Some("CREATE MATERIALIZED VIEW"),
3772            Self::RefreshMaterializedView { .. } => Some("REFRESH MATERIALIZED VIEW"),
3773            Self::DropMaterializedView { .. } => Some("DROP MATERIALIZED VIEW"),
3774            Self::CreateSequence { .. } => Some("CREATE SEQUENCE"),
3775            Self::AlterSequence { .. } => Some("ALTER SEQUENCE"),
3776            Self::DropSequence { .. } => Some("DROP SEQUENCE"),
3777            Self::CreateType { .. } => Some("CREATE TYPE"),
3778            Self::DropType { .. } => Some("DROP TYPE"),
3779            Self::AlterTypeAddValue { .. } | Self::AlterTypeRenameValue { .. } => {
3780                Some("ALTER TYPE")
3781            }
3782            Self::CreateDomain { .. } => Some("CREATE DOMAIN"),
3783            Self::AlterDomain { .. } => Some("ALTER DOMAIN"),
3784            Self::DropDomain { .. } => Some("DROP DOMAIN"),
3785            Self::CreateSchema { .. } => Some("CREATE SCHEMA"),
3786            Self::DropSchema { .. } => Some("DROP SCHEMA"),
3787            Self::CreateFunction { .. } => Some("CREATE FUNCTION"),
3788            Self::DropFunction { .. } => Some("DROP FUNCTION"),
3789            Self::CreateTrigger { .. } => Some("CREATE TRIGGER"),
3790            Self::DropTrigger { .. } => Some("DROP TRIGGER"),
3791            Self::CreateRule { .. } => Some("CREATE RULE"),
3792            Self::DropRule { .. } => Some("DROP RULE"),
3793            Self::CreateExtension { .. } => Some("CREATE EXTENSION"),
3794            Self::CreateStatistics { .. } => Some("CREATE STATISTICS"),
3795            Self::DropStatistics { .. } => Some("DROP STATISTICS"),
3796            Self::DropAggregate { .. } => Some("DROP AGGREGATE"),
3797            Self::CommentOn { .. } => Some("COMMENT"),
3798            Self::DropDatabase { .. } => Some("DROP DATABASE"),
3799            Self::CreatePublication { .. } => Some("CREATE PUBLICATION"),
3800            Self::DropPublication { .. } => Some("DROP PUBLICATION"),
3801            Self::CreateSubscription { .. } => Some("CREATE SUBSCRIPTION"),
3802            Self::DropSubscription { .. } => Some("DROP SUBSCRIPTION"),
3803
3804            // ---- changes roles / permissions ---------------------------
3805            Self::CreateUser { .. } => Some("CREATE ROLE"),
3806            Self::DropUser { .. } => Some("DROP ROLE"),
3807            Self::AlterRolePassword { .. } | Self::SetDbRoleSetting { .. } => Some("ALTER ROLE"),
3808            Self::Grant { .. } => Some("GRANT"),
3809            Self::Revoke { .. } => Some("REVOKE"),
3810            Self::CreatePolicy { .. } => Some("CREATE POLICY"),
3811            Self::AlterPolicy { .. } => Some("ALTER POLICY"),
3812            Self::DropPolicy { .. } => Some("DROP POLICY"),
3813            Self::AlterSystem { .. } => Some("ALTER SYSTEM"),
3814
3815            // ---- SPG's own writers -------------------------------------
3816            // Rewrites cold-tier segments on disk. PG has no equivalent to
3817            // ask, so the test is what it does, not what it is called.
3818            Self::CompactColdSegments => Some("COMPACT COLD SEGMENTS"),
3819
3820            // ---- allowed -----------------------------------------------
3821            // Reads, transaction control, session state, cursors, and the
3822            // maintenance statements PG itself permits. `REINDEX` really is
3823            // allowed in a read-only transaction (measured), which is why
3824            // `Maintain` is here.
3825            //
3826            // `Prepare`, `Execute`, `Call` and `DoBlock` are allowed at
3827            // this level for the reason PG allows them: the write inside
3828            // is refused when it runs, by this same check. Measured:
3829            // `PREPARE p AS INSERT …` succeeds; `DO $$ … INSERT … $$`
3830            // fails with `cannot execute INSERT`.
3831            Self::Explain { .. }
3832            | Self::CopyTo { .. }
3833            | Self::CopyToFile { .. }
3834            | Self::Analyze { .. }
3835            | Self::Maintain { .. }
3836            | Self::Vacuum { .. }
3837            | Self::Begin { .. }
3838            | Self::Commit
3839            | Self::Rollback
3840            | Self::Savepoint { .. }
3841            | Self::RollbackToSavepoint { .. }
3842            | Self::ReleaseSavepoint { .. }
3843            | Self::PrepareTransaction { .. }
3844            | Self::SetTransaction { .. }
3845            | Self::SetConstraints { .. }
3846            | Self::SetParameter { .. }
3847            | Self::SetParameterList { .. }
3848            | Self::SetUserVars { .. }
3849            | Self::SetRole { .. }
3850            | Self::ResetParameter { .. }
3851            | Self::ShowParameter { .. }
3852            | Self::Discard { .. }
3853            | Self::Prepare { .. }
3854            | Self::Execute { .. }
3855            | Self::Deallocate { .. }
3856            | Self::Call { .. }
3857            | Self::DoBlock { .. }
3858            | Self::DeclareCursor { .. }
3859            | Self::FetchCursor { .. }
3860            | Self::MoveCursor { .. }
3861            | Self::CloseCursor { .. }
3862            | Self::Listen { .. }
3863            | Self::Notify { .. }
3864            | Self::Unlisten { .. }
3865            | Self::Kill { .. }
3866            | Self::WaitForWalPosition { .. }
3867            | Self::ValidateOnly { .. }
3868            | Self::NoOpPreventedInTransaction { .. }
3869            | Self::Empty
3870            | Self::ShowTables
3871            | Self::ShowDatabases
3872            | Self::UseDatabase(_)
3873            | Self::ShowCreateTable { .. }
3874            | Self::ShowIndexes { .. }
3875            | Self::ShowStatus
3876            | Self::ShowVariables
3877            | Self::ShowVariablesLike { .. }
3878            | Self::ShowProcesslist
3879            | Self::ShowColumns { .. }
3880            | Self::ShowUsers
3881            | Self::ShowPublications
3882            | Self::ShowSubscriptions => None,
3883        }
3884    }
3885}
3886
3887#[derive(Debug, Clone, PartialEq, Eq)]
3888pub struct LockingClause {
3889    pub strength: LockStrength,
3890    /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3891    pub of_tables: Vec<String>,
3892    pub policy: LockWait,
3893}
3894
3895/// PG's four tuple-lock strengths, weakest first.
3896#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3897pub enum LockStrength {
3898    KeyShare,
3899    Share,
3900    NoKeyUpdate,
3901    Update,
3902}
3903
3904/// What to do when the row is already locked.
3905#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3906pub enum LockWait {
3907    /// Block until it is free — PG's default.
3908    #[default]
3909    Wait,
3910    /// `NOWAIT` — fail the statement with 55P03.
3911    NoWait,
3912    /// `SKIP LOCKED` — leave the row out of the result.
3913    SkipLocked,
3914}
3915
3916#[derive(Debug, Clone, PartialEq, Default)]
3917pub struct SelectStatement {
3918    /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3919    /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3920    /// whole syntax and locked nothing: two workers running the classic
3921    /// `SKIP LOCKED` queue take both took the same row.
3922    /// v7.39 (round 305) — boxed. A locking clause appears on a
3923    /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3924    /// cost every `SelectStatement` 32 bytes, and this struct sits in
3925    /// recursive evaluation frames where the engine already runs close to
3926    /// its stack budget (a 512 KB depth guard is the canary).
3927    pub locking: Option<alloc::boxed::Box<LockingClause>>,
3928    /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3929    /// expressions, materialised once at query start before the
3930    /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3931    /// only — no `WITH RECURSIVE` for v4.x.
3932    pub ctes: Vec<Cte>,
3933    pub distinct: bool,
3934    /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3935    /// keep the first row (per ORDER BY) of each group the
3936    /// expressions define. Empty = no DISTINCT ON.
3937    pub distinct_on: Vec<Expr>,
3938    pub items: Vec<SelectItem>,
3939    pub from: Option<FromClause>,
3940    pub where_: Option<Expr>,
3941    pub group_by: Option<Vec<Expr>>,
3942    /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3943    /// expands `group_by` to every non-aggregate SELECT-list item
3944    /// before the executor runs. Mutually exclusive with an
3945    /// explicit `group_by` list (the parser sets exactly one).
3946    pub group_by_all: bool,
3947    /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3948    /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3949    /// aggregate executor resolves them through the same synthetic
3950    /// schema used for the SELECT items.
3951    pub having: Option<Expr>,
3952    /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3953    /// itself a `SelectStatement` with `order_by = None` and `limit =
3954    /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3955    /// top of the chain).
3956    pub unions: Vec<(UnionKind, SelectStatement)>,
3957    /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3958    /// Keys are matched left-to-right: first key decides, ties break
3959    /// to the second, etc.
3960    pub order_by: Vec<OrderBy>,
3961    /// `LIMIT <n>` — bound on row output. `n` is an integer
3962    /// literal **or** (v7.9.24) a placeholder `$N` resolved
3963    /// against the prepared-statement Bind values. mailrs
3964    /// migration follow-up H2.
3965    pub limit: Option<LimitExpr>,
3966    /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3967    /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3968    pub offset: Option<LimitExpr>,
3969    /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3970    /// (SQL:2008). When true and an ORDER BY is present, the
3971    /// executor extends past the LIMIT-truncated tail to include
3972    /// every row whose ORDER BY key equals the last-kept row's
3973    /// key. Requires an ORDER BY; the executor errors otherwise
3974    /// (matching PG's `WITH TIES` rule). The parser was already
3975    /// accepting `WITH TIES` since Phase 5.1; this field captures
3976    /// the choice so the executor can act on it.
3977    pub limit_with_ties: bool,
3978    /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3979    /// that NOTHING referenced. PG analyses every definition whether
3980    /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3981    /// and silently succeeded here — the referenced ones get their columns
3982    /// resolved through the WindowFunction nodes they were inlined into,
3983    /// and the unreferenced ones used to be dropped at parse, unexamined.
3984    /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3985    ///
3986    /// Not part of `Display`: an unreferenced definition has no effect on
3987    /// the result, so a deparsed body (a stored view) omits it.
3988    pub window_check_exprs: Vec<Expr>,
3989}
3990
3991impl Expr {
3992    /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3993    /// directly inside this expression to `f`. `f` receives each nested
3994    /// statement once; descending further (into that statement's own
3995    /// clauses) is the caller's job, which keeps this walk finite and
3996    /// lets the caller order the recursion.
3997    ///
3998    /// The match is deliberately **wildcard-free**: a new `Expr` variant
3999    /// does not compile until it says whether it can carry a subquery.
4000    /// The row-count resolution pass is built on this, and a shape it
4001    /// silently failed to visit would leave a `LimitExpr::Expr` behind —
4002    /// which every row-count reader would take as "no limit", i.e. the
4003    /// whole table. Compile-time exhaustiveness is what rules that out.
4004    /// Iterative on purpose. Expression trees here get deep (long
4005    /// boolean chains, big IN lists), and this walk is on the path of
4006    /// every statement; recursing would add a frame per node to a stack
4007    /// budget the engine already runs close to — a depth guard that runs
4008    /// on a deliberately small stack caught exactly that. Depth costs
4009    /// heap here instead.
4010    pub fn for_each_subquery_mut<E>(
4011        &mut self,
4012        f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
4013    ) -> Result<(), E> {
4014        let mut stack: Vec<&mut Self> = alloc::vec![self];
4015        while let Some(e) = stack.pop() {
4016            match e {
4017                Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
4018                Self::NamedArg { expr, .. }
4019                | Self::Collate { expr, .. }
4020                | Self::Variadic(expr)
4021                | Self::Unary { expr, .. }
4022                | Self::Cast { expr, .. }
4023                | Self::FieldAccess { base: expr, .. }
4024                | Self::IsNull { expr, .. }
4025                | Self::BoolTest { expr, .. }
4026                | Self::Extract { source: expr, .. } => stack.push(expr),
4027                Self::Binary { lhs, rhs, .. } => {
4028                    stack.push(lhs);
4029                    stack.push(rhs);
4030                }
4031                Self::Like { expr, pattern, .. } => {
4032                    stack.push(expr);
4033                    stack.push(pattern);
4034                }
4035                Self::ArraySubscript { target, index } => {
4036                    stack.push(target);
4037                    stack.push(index);
4038                }
4039                Self::ArraySlice { target, lo, hi } => {
4040                    stack.push(target);
4041                    stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
4042                }
4043                Self::AnyAll { expr, array, .. } => {
4044                    stack.push(expr);
4045                    stack.push(array);
4046                }
4047                Self::FunctionCall { args, .. } | Self::Array(args) => {
4048                    stack.extend(args.iter_mut());
4049                }
4050                Self::AggregateOrdered {
4051                    call,
4052                    order_by,
4053                    filter,
4054                    ..
4055                } => {
4056                    stack.push(call);
4057                    stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
4058                    stack.extend(filter.iter_mut().map(|b| &mut **b));
4059                }
4060                Self::WindowFunction {
4061                    args,
4062                    partition_by,
4063                    order_by,
4064                    filter,
4065                    ..
4066                } => {
4067                    // `frame` bounds hold folded numbers / interval
4068                    // parts, never expressions — nothing to visit there.
4069                    stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
4070                    stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
4071                    stack.extend(filter.iter_mut().map(|b| &mut **b));
4072                }
4073                Self::InList { expr, list, .. } => {
4074                    stack.push(expr);
4075                    stack.extend(list.iter_mut());
4076                }
4077                Self::Case {
4078                    operand,
4079                    branches,
4080                    else_branch,
4081                } => {
4082                    stack.extend(
4083                        operand
4084                            .iter_mut()
4085                            .chain(else_branch.iter_mut())
4086                            .map(|b| &mut **b),
4087                    );
4088                    for (when, then) in branches.iter_mut() {
4089                        stack.push(when);
4090                        stack.push(then);
4091                    }
4092                }
4093                Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
4094                Self::InSubquery { expr, subquery, .. } => {
4095                    stack.push(expr);
4096                    f(subquery)?;
4097                }
4098                Self::RowInSubquery { row, subquery, .. }
4099                | Self::RowCmpSubquery { row, subquery, .. } => {
4100                    stack.extend(row.iter_mut());
4101                    f(subquery)?;
4102                }
4103            }
4104        }
4105        Ok(())
4106    }
4107
4108    /// The shared twin of [`Self::for_each_subquery_mut`], for analysis
4109    /// that reads a statement rather than rewriting it.
4110    ///
4111    /// Same wildcard-free match, same iterative walk. Rust cannot write
4112    /// one body generic over `&`/`&mut`, so the two must be edited
4113    /// together; exhaustiveness is what makes that a compile error
4114    /// rather than a silent gap in one of them.
4115    ///
4116    /// # Errors
4117    /// Whatever `f` returns.
4118    pub fn for_each_subquery<'a, E>(
4119        &'a self,
4120        f: &mut impl FnMut(&'a SelectStatement) -> Result<(), E>,
4121    ) -> Result<(), E> {
4122        let mut stack: Vec<&'a Self> = alloc::vec![self];
4123        while let Some(e) = stack.pop() {
4124            match e {
4125                Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
4126                Self::NamedArg { expr, .. }
4127                | Self::Collate { expr, .. }
4128                | Self::Variadic(expr)
4129                | Self::Unary { expr, .. }
4130                | Self::Cast { expr, .. }
4131                | Self::FieldAccess { base: expr, .. }
4132                | Self::IsNull { expr, .. }
4133                | Self::BoolTest { expr, .. }
4134                | Self::Extract { source: expr, .. } => stack.push(expr),
4135                Self::Binary { lhs, rhs, .. } => {
4136                    stack.push(lhs);
4137                    stack.push(rhs);
4138                }
4139                Self::Like { expr, pattern, .. } => {
4140                    stack.push(expr);
4141                    stack.push(pattern);
4142                }
4143                Self::ArraySubscript { target, index } => {
4144                    stack.push(target);
4145                    stack.push(index);
4146                }
4147                Self::ArraySlice { target, lo, hi } => {
4148                    stack.push(target);
4149                    stack.extend(lo.iter().chain(hi.iter()).map(|b| &**b));
4150                }
4151                Self::AnyAll { expr, array, .. } => {
4152                    stack.push(expr);
4153                    stack.push(array);
4154                }
4155                Self::FunctionCall { args, .. } | Self::Array(args) => {
4156                    stack.extend(args.iter());
4157                }
4158                Self::AggregateOrdered {
4159                    call,
4160                    order_by,
4161                    filter,
4162                    ..
4163                } => {
4164                    stack.push(call);
4165                    stack.extend(order_by.iter().map(|o| &o.expr));
4166                    stack.extend(filter.iter().map(|b| &**b));
4167                }
4168                Self::WindowFunction {
4169                    args,
4170                    partition_by,
4171                    order_by,
4172                    filter,
4173                    ..
4174                } => {
4175                    stack.extend(args.iter().chain(partition_by.iter()));
4176                    stack.extend(order_by.iter().map(|(e, _, _)| e));
4177                    stack.extend(filter.iter().map(|b| &**b));
4178                }
4179                Self::InList { expr, list, .. } => {
4180                    stack.push(expr);
4181                    stack.extend(list.iter());
4182                }
4183                Self::Case {
4184                    operand,
4185                    branches,
4186                    else_branch,
4187                } => {
4188                    stack.extend(operand.iter().chain(else_branch.iter()).map(|b| &**b));
4189                    for (when, then) in branches {
4190                        stack.push(when);
4191                        stack.push(then);
4192                    }
4193                }
4194                Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
4195                Self::InSubquery { expr, subquery, .. } => {
4196                    stack.push(expr);
4197                    f(subquery)?;
4198                }
4199                Self::RowInSubquery { row, subquery, .. }
4200                | Self::RowCmpSubquery { row, subquery, .. } => {
4201                    stack.extend(row.iter());
4202                    f(subquery)?;
4203                }
4204            }
4205        }
4206        Ok(())
4207    }
4208}
4209
4210/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
4211/// time or a placeholder `$N` resolved during extended-query
4212/// Bind. mailrs migration follow-up H2.
4213///
4214/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
4215/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
4216/// made the compiler point at every site that used to duplicate a
4217/// row-count out of the AST, which is exactly the set that must not
4218/// bypass the resolution pre-pass.
4219#[derive(Debug, Clone, PartialEq)]
4220pub enum LimitExpr {
4221    /// `LIMIT 10` — value known at parse time.
4222    Literal(u32),
4223    /// `LIMIT $N` — the 1-based parameter index, resolved against
4224    /// the bind values when the prepared statement executes.
4225    Placeholder(u16),
4226    /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
4227    /// greatest(2,3)`: a row-count expression that isn't constant, so
4228    /// it can't be folded at parse time. Evaluated once, before
4229    /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
4230    /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
4231    /// "no limit"). **No execution path may see this variant** —
4232    /// `as_literal` would report `None`, which every row-count reader
4233    /// takes to mean "unlimited", i.e. the whole table.
4234    Expr(alloc::boxed::Box<Expr>),
4235}
4236
4237impl fmt::Display for LimitExpr {
4238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4239        match self {
4240            Self::Literal(n) => write!(f, "{n}"),
4241            Self::Placeholder(n) => write!(f, "${n}"),
4242            // Parenthesised so the round-trip text re-parses as one
4243            // row-count expression (`LIMIT (SELECT 4)`), which is also
4244            // the only spelling `FETCH FIRST` accepts.
4245            Self::Expr(e) => write!(f, "({e})"),
4246        }
4247    }
4248}
4249
4250impl LimitExpr {
4251    /// Convenience for the simple-query path where no placeholders
4252    /// can possibly exist. Returns the literal value or `None` if
4253    /// this is a placeholder (caller must surface as Unsupported).
4254    ///
4255    /// v7.39 (round 305) — `None` is read by every row-count consumer as
4256    /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
4257    /// therefore silently return the whole table, so the engine's
4258    /// `resolve_limit_exprs` pre-pass rewrites the variant away before
4259    /// dispatch. The assertion makes a missed nesting site fail loudly
4260    /// in every test build rather than quietly widening a result set.
4261    #[must_use]
4262    pub fn as_literal(&self) -> Option<u32> {
4263        match self {
4264            Self::Literal(n) => Some(*n),
4265            Self::Placeholder(_) => None,
4266            Self::Expr(_) => {
4267                debug_assert!(
4268                    false,
4269                    "LimitExpr::Expr reached execution — resolve_limit_exprs \
4270                     missed a nesting site; treating it as `no limit` would \
4271                     return every row"
4272                );
4273                None
4274            }
4275        }
4276    }
4277}
4278
4279/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
4280/// the engine's `substitute_placeholders` pass these are
4281/// always Literal; in the simple-query path a Placeholder
4282/// shape returns None (executor surfaces as
4283/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
4284impl SelectStatement {
4285    #[must_use]
4286    pub fn limit_literal(&self) -> Option<u32> {
4287        self.limit.as_ref().and_then(LimitExpr::as_literal)
4288    }
4289    #[must_use]
4290    pub fn offset_literal(&self) -> Option<u32> {
4291        self.offset.as_ref().and_then(LimitExpr::as_literal)
4292    }
4293}
4294
4295#[derive(Debug, Clone, PartialEq)]
4296pub struct Cte {
4297    pub name: String,
4298    /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
4299    /// classical case) or a data-modifying statement
4300    /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
4301    /// CTE semantics. The modifying body's RETURNING projection
4302    /// becomes the materialised CTE table the outer query can
4303    /// reference; the modifying statement runs once before the
4304    /// outer query, within the same transaction.
4305    pub body: CteBody,
4306    /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
4307    /// RECURSIVE keyword. Applies to every CTE in the clause per
4308    /// PG semantics. A non-recursive body in a RECURSIVE WITH is
4309    /// allowed; the engine just runs it once.
4310    pub recursive: bool,
4311    /// v4.22: optional `WITH name(a, b, c)` column-name list. When
4312    /// non-empty, these override the body's output column names
4313    /// position-by-position; the engine errors out if the count
4314    /// doesn't match the body's projection width.
4315    pub column_overrides: Vec<String>,
4316    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
4317    /// SET seqcol` on a recursive CTE. Desugared at parse time into an
4318    /// extra ordering column on the body (see `rewrite_search_and_cycle`).
4319    pub search: Option<SearchClause>,
4320    /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
4321    /// USING pathcol` cycle detection, desugared at parse time.
4322    pub cycle: Option<CycleClause>,
4323}
4324
4325/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
4326#[derive(Debug, Clone, PartialEq)]
4327pub struct SearchClause {
4328    /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
4329    pub depth_first: bool,
4330    /// The CTE output columns the search orders by.
4331    pub by_columns: Vec<String>,
4332    /// The new column holding the ordering key (a row-array for depth,
4333    /// a `(depth, keys…)` row for breadth).
4334    pub set_column: String,
4335}
4336
4337/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
4338#[derive(Debug, Clone, PartialEq)]
4339pub struct CycleClause {
4340    /// Columns whose repetition along a path marks a cycle.
4341    pub columns: Vec<String>,
4342    /// The new boolean-ish column set to `mark_value` on a cycle.
4343    pub mark_column: String,
4344    /// Value written to `mark_column` when a cycle is detected (default
4345    /// `true`); `default_value` otherwise. PG allows any type; SPG carries
4346    /// them as literals.
4347    pub mark_value: Option<Literal>,
4348    pub default_value: Option<Literal>,
4349    /// The new column accumulating the visited-row path array.
4350    pub path_column: String,
4351}
4352
4353/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
4354/// (Insert / Update / Delete with optional RETURNING). The
4355/// data-modifying variants must carry a RETURNING projection for the
4356/// outer query to reference the CTE alias by; an empty RETURNING is
4357/// only valid if no outer reference materialises (rare — typically
4358/// caught at planning).
4359#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
4360#[derive(Debug, Clone, PartialEq)]
4361pub enum CteBody {
4362    Select(SelectStatement),
4363    Insert(Box<InsertStatement>),
4364    Update(Box<UpdateStatement>),
4365    Delete(Box<DeleteStatement>),
4366    /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
4367    /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
4368    Merge(Box<MergeStatement>),
4369}
4370
4371impl CteBody {
4372    /// Convenience accessor used by classical (read-only) CTE
4373    /// callsites that still expect a SELECT body. Returns None for
4374    /// data-modifying CTEs; callers must explicitly route those
4375    /// through `exec_with_ctes`'s modifying branch.
4376    #[must_use]
4377    pub fn as_select(&self) -> Option<&SelectStatement> {
4378        match self {
4379            Self::Select(s) => Some(s),
4380            _ => None,
4381        }
4382    }
4383
4384    #[must_use]
4385    pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
4386        match self {
4387            Self::Select(s) => Some(s),
4388            _ => None,
4389        }
4390    }
4391
4392    #[must_use]
4393    pub fn is_modifying(&self) -> bool {
4394        !matches!(self, Self::Select(_))
4395    }
4396}
4397
4398#[derive(Debug, Clone, PartialEq)]
4399pub struct OrderBy {
4400    pub expr: Expr,
4401    /// `false` = ASC (default), `true` = DESC.
4402    pub desc: bool,
4403    /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
4404    /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
4405    /// NULLS FIRST for DESC); the engine resolves the effective
4406    /// value via `nulls_first.unwrap_or(desc)`.
4407    pub nulls_first: Option<bool>,
4408    /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
4409    /// It lives here rather than in the expression for the same reason
4410    /// `desc` does: at an ORDER BY key a collation is ordering
4411    /// information, and nothing downstream of the sort needs it. A new
4412    /// `Expr` variant would instead put a new arm on `eval_expr`, which
4413    /// this repo has measured to overflow the debug stack.
4414    ///
4415    /// `None` means none was written, and the key falls back to whatever
4416    /// its COLUMN declares — which is every key that existed before this.
4417    pub collation: Option<String>,
4418}
4419
4420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4421pub enum UnionKind {
4422    /// `UNION` — dedupes the combined set.
4423    Distinct,
4424    /// `UNION ALL` — concatenates without dedup.
4425    All,
4426    /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
4427    /// present on both sides.
4428    Intersect,
4429    /// `INTERSECT ALL` — multiset intersection (min per-row count).
4430    IntersectAll,
4431    /// `EXCEPT` — distinct left rows absent from the right.
4432    Except,
4433    /// `EXCEPT ALL` — multiset subtraction.
4434    ExceptAll,
4435}
4436
4437#[derive(Debug, Clone, PartialEq)]
4438pub enum SelectItem {
4439    Wildcard,
4440    /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
4441    /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
4442    /// `NEW` pseudo-relation).
4443    QualifiedWildcard(String),
4444    Expr {
4445        expr: Expr,
4446        alias: Option<String>,
4447    },
4448}
4449
4450#[derive(Debug, Clone, PartialEq)]
4451pub struct TableRef {
4452    pub name: String,
4453    pub alias: Option<String>,
4454    /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
4455    /// children.
4456    ///
4457    /// The keyword used to be absorbed at parse time, on the reasoning
4458    /// that SPG's inheritance children are separate relations a plain
4459    /// scan does not descend into — so ONLY already described what the
4460    /// scan did. That stopped being true when a partition parent
4461    /// started unioning its children: measured, `SELECT count(*) FROM
4462    /// ONLY <partitioned parent>` answered 2 where PG answers 0.
4463    pub only: bool,
4464    /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
4465    /// When `Some(id)`, the scan restricts to rows that live in
4466    /// segment `<id>` only — useful for forensic inspection of a
4467    /// specific freezer-emitted segment without exposing the hot
4468    /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
4469    /// is STABILITY carve-out for v6.10 — needs the freezer to
4470    /// stamp each segment with a wall-clock at creation time.
4471    pub as_of_segment: Option<u32>,
4472    /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
4473    /// source. When `Some`, `name` is the alias (defaulting to
4474    /// `"unnest"` when no `AS` is given) and the engine builds a
4475    /// synthetic single-column table by evaluating the expression
4476    /// once at SELECT entry. Each TEXT[] element becomes one row;
4477    /// NULL elements become NULL cells. v7.11 supported
4478    /// uncorrelated UNNEST only as the FROM primary; v7.13.2
4479    /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
4480    /// position (cross-join with regular tables).
4481    pub unnest_expr: Option<Box<Expr>>,
4482    /// v7.13.2 — mailrs round-6 S5. PG-standard
4483    /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
4484    /// when non-empty, the first entry overrides the projected
4485    /// column name for the unnested column. Empty = fall back to
4486    /// the table alias (pre-v7.13.2 behaviour).
4487    pub unnest_column_aliases: Vec<String>,
4488    /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
4489    /// row-stream gains a trailing BIGINT column counting rows
4490    /// from 1 in element order. PG names it `ordinality`; a second
4491    /// entry in the column-alias list renames it.
4492    pub with_ordinality: bool,
4493    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4494    /// [, step])` set-returning source. When `Some`, the engine
4495    /// materialises a single-column virtual table by stepping
4496    /// `start` to `stop` inclusive. Args are the literal arg list
4497    /// (2 for default-step, 3 for explicit-step). Supports:
4498    ///   * SmallInt / Int / BigInt with integer step (default = 1)
4499    ///   * Timestamp with INTERVAL step (PG date-range pattern)
4500    /// Mutually exclusive with `unnest_expr` — both populate the
4501    /// same downstream dispatch slot. `name` defaults to
4502    /// `"generate_series"` when no alias is provided.
4503    pub generate_series_args: Option<Vec<Expr>>,
4504    /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
4505    /// table. When `Some`, the TableRef is a parenthesised SELECT
4506    /// that may reference columns from the preceding FROM items
4507    /// (correlated derived table). The executor materialises the
4508    /// subquery per left-row, substituting outer-column references
4509    /// against the current join row's values before running the
4510    /// inner SELECT, then cross-joins the result back.
4511    /// Mutually exclusive with `name` / `unnest_expr` /
4512    /// `generate_series_args`.
4513    pub lateral_subquery: Option<Box<SelectStatement>>,
4514    /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
4515    /// function as a FROM item. PG semantics: for each key/value
4516    /// pair in the JSONB object argument, emit one (key TEXT,
4517    /// value TEXT) row. When prefixed by `LATERAL` and joined via
4518    /// `CROSS JOIN LATERAL`, the argument may reference columns
4519    /// from a preceding FROM item, in which case the executor
4520    /// evaluates `<expr>` per outer row.
4521    /// Mutually exclusive with `unnest_expr` / `generate_series_args`
4522    /// / `lateral_subquery`. The optional `LATERAL` keyword does not
4523    /// require a separate flag — the executor evaluates per-row
4524    /// whenever the join sits in a JoinKind context.
4525    ///
4526    /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4527    /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4528    /// `json_each` / `json_each_text`) so the executor picks the
4529    /// value-column rendering (JSON text vs unwrapped text).
4530    pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4531    /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4532    /// function channel: `(lowercase fn name, args)`. Carries
4533    /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4534    /// dispatches by name.
4535    pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4536    /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4537    /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4538    /// reference to it yields the value, not a one-field composite
4539    /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4540    /// desugared shape is indistinguishable from a hand-written
4541    /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4542    /// only the parser knows which one it built, so it says so here.
4543    pub scalar_fn_item: bool,
4544    /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4545    /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4546    /// target-list SRFs follow — see round 67). The array-returning family keeps
4547    /// its own lowering; this channel carries the ones that have no array form
4548    /// (`generate_series`, a user `RETURNS SETOF` function).
4549    pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4550    /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4551    /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4552    /// tables (implicit LATERAL, like every SRF channel). Executed by
4553    /// walking the row path over the parsed doc, then each column's
4554    /// path per row-item; NESTED expands as a per-parent outer join.
4555    pub json_table: Option<Box<JsonTable>>,
4556}
4557
4558/// What a FROM item IS.
4559///
4560/// v7.40.10 — a boolean cannot make a consumer handle a new kind; an
4561/// enum can. Every consumer that must know the difference writes a
4562/// `match` with no wildcard arm, so a variant added here is a compile
4563/// error at each of them rather than a defect at whichever one the new
4564/// shape reaches first.
4565///
4566/// The engine asked "is this item synthesised?" in fifty-six places and
4567/// exactly one of them listed every field. The rest were missing
4568/// between one and five, and the gaps were reachable:
4569/// `SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)` answered
4570/// `relation "jsonb_each_text" does not exist` over the extended
4571/// protocol because one such list named four of seven.
4572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4573pub enum FromItemKind {
4574    /// A table, view or CTE named in the catalog.
4575    Relation,
4576    /// `unnest(...)` and the set-returning functions the parser lowers
4577    /// onto the same slot (`string_to_table`, `jsonb_object_keys`, …).
4578    Unnest,
4579    /// `generate_series(...)`.
4580    GenerateSeries,
4581    /// A derived table or LATERAL subquery.
4582    Subquery,
4583    /// `jsonb_each(...)` / `jsonb_each_text(...)`.
4584    JsonbEach,
4585    /// A table function call the parser kept as a name plus arguments.
4586    TableFn,
4587    /// `ROWS FROM (...)`.
4588    RowsFrom,
4589    /// `JSON_TABLE(...)`.
4590    JsonTable,
4591    /// A scalar function in FROM position, which yields one row.
4592    ScalarFn,
4593}
4594
4595/// One walkable slot of a FROM item: an expression, or a nested SELECT.
4596///
4597/// v7.40.10 — see [`TableRef::try_for_each_slot_mut`].
4598#[derive(Debug)]
4599pub enum FromSlot<'a> {
4600    Expr(&'a mut Expr),
4601    Select(&'a mut SelectStatement),
4602}
4603
4604/// The shared half of [`FromSlot`].
4605///
4606/// v7.40.11 — analysis reads a statement it must not modify, and Rust
4607/// has no way to write one walk that is generic over `&`/`&mut`. The two
4608/// bodies are the same destructure and must move together; the
4609/// destructures are total, so a new slot fails to compile in both.
4610#[derive(Debug)]
4611pub enum FromSlotRef<'a> {
4612    Expr(&'a Expr),
4613    Select(&'a SelectStatement),
4614}
4615
4616impl TableRef {
4617    /// Whether this FROM item NAMES A RELATION — a table, view or CTE —
4618    /// rather than producing its own rows.
4619    ///
4620    /// **A total destructure, no `..`.** A field added to `TableRef` is a
4621    /// compile error here.
4622    ///
4623    /// v7.40.10, on evidence. This question is asked in 56 places across
4624    /// the engine and every one of them wrote its own list of fields.
4625    /// Exactly one was complete. The others were missing between one and
4626    /// five slots each, and each gap is a defect waiting for the shape
4627    /// that reaches it:
4628    ///
4629    /// ```text
4630    ///   try_stream_single_table's guard named four of seven, so
4631    ///   SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)
4632    ///     ERROR:  relation "jsonb_each_text" does not exist
4633    ///   over the extended protocol, while count(*) over the same item
4634    ///   answered and the simple query protocol answered.
4635    /// ```
4636    ///
4637    /// `scalar_fn_item` counts as not-a-relation for the same reason the
4638    /// rest do: the row comes from the item, not from the catalog.
4639    #[must_use]
4640    pub fn names_a_relation(&self) -> bool {
4641        self.kind() == FromItemKind::Relation
4642    }
4643
4644    /// What this FROM item is.
4645    ///
4646    /// **A total destructure, no `..`.** A field added to `TableRef` is
4647    /// a compile error here — which is the point, because every
4648    /// consumer matches exhaustively on the result.
4649    ///
4650    /// The slots are mutually exclusive by construction: the parser
4651    /// fills exactly one of them, or none for a plain relation.
4652    #[must_use]
4653    pub fn kind(&self) -> FromItemKind {
4654        let Self {
4655            name: _,
4656            alias: _,
4657            only: _,
4658            as_of_segment: _,
4659            unnest_column_aliases: _,
4660            with_ordinality: _,
4661            scalar_fn_item,
4662            unnest_expr,
4663            generate_series_args,
4664            lateral_subquery,
4665            jsonb_each_text_arg,
4666            table_fn_call,
4667            rows_from,
4668            json_table,
4669        } = self;
4670        if unnest_expr.is_some() {
4671            FromItemKind::Unnest
4672        } else if generate_series_args.is_some() {
4673            FromItemKind::GenerateSeries
4674        } else if lateral_subquery.is_some() {
4675            FromItemKind::Subquery
4676        } else if jsonb_each_text_arg.is_some() {
4677            FromItemKind::JsonbEach
4678        } else if table_fn_call.is_some() {
4679            FromItemKind::TableFn
4680        } else if rows_from.is_some() {
4681            FromItemKind::RowsFrom
4682        } else if json_table.is_some() {
4683            FromItemKind::JsonTable
4684        } else if *scalar_fn_item {
4685            FromItemKind::ScalarFn
4686        } else {
4687            FromItemKind::Relation
4688        }
4689    }
4690
4691    /// Every expression this FROM item carries, and the SELECT nested in
4692    /// it — in one place, for every pass that needs them.
4693    ///
4694    /// **Written as a TOTAL destructure, with no `..`.** A field added
4695    /// to `TableRef` is a compile error here, rather than a defect in
4696    /// each pass that enumerated the slots for itself. That is the whole
4697    /// point of the function existing.
4698    ///
4699    /// v7.40.10, on evidence. `TableRef` carries seven expression slots
4700    /// and three separate passes each knew a different subset of them.
4701    /// In one day: the parameter-substitution walk knew only
4702    /// `lateral_subquery`, so `unnest($1)` reached execution still
4703    /// holding a placeholder (a customer's live 500); `describe` knew
4704    /// only `unnest_expr`, so `generate_series(…)` described no columns
4705    /// and a driver got a protocol error; and the LIMIT/OFFSET
4706    /// resolution knew CTEs and UNION peers but not a FROM subquery, so
4707    /// `LIMIT $n` inside a derived table returned every row.
4708    ///
4709    /// Fixing those three one at a time left four slots unvisited.
4710    /// Measured after the third fix shipped, all on the same message:
4711    ///
4712    /// ```text
4713    ///   jsonb_each_text($1)  parameter $1 referenced but only 0 bound
4714    ///   ROWS FROM (…$1…)     parameter $1 referenced but only 0 bound
4715    ///   json_table($1, …)    parameter $1 referenced but only 0 bound
4716    /// ```
4717    ///
4718    /// Those were the next three reports. This is what stops the fourth.
4719    ///
4720    /// # Errors
4721    /// Whatever the callbacks return; the walk stops at the first.
4722    pub fn try_for_each_slot_mut<E>(
4723        &mut self,
4724        visit: &mut dyn FnMut(FromSlot<'_>) -> Result<(), E>,
4725    ) -> Result<(), E> {
4726        // One callback rather than two, so a caller that needs the same
4727        // state for both — every caller so far — does not have to lend
4728        // it twice.
4729        let Self {
4730            // Not expressions — named so the destructure stays total.
4731            name: _,
4732            alias: _,
4733            only: _,
4734            as_of_segment: _,
4735            unnest_column_aliases: _,
4736            with_ordinality: _,
4737            scalar_fn_item: _,
4738            // The seven that carry something to walk.
4739            unnest_expr,
4740            generate_series_args,
4741            lateral_subquery,
4742            jsonb_each_text_arg,
4743            table_fn_call,
4744            rows_from,
4745            json_table,
4746        } = self;
4747        if let Some(e) = unnest_expr {
4748            visit(FromSlot::Expr(e))?;
4749        }
4750        if let Some(args) = generate_series_args {
4751            for a in args.iter_mut() {
4752                visit(FromSlot::Expr(a))?;
4753            }
4754        }
4755        if let Some(sub) = lateral_subquery {
4756            visit(FromSlot::Select(sub))?;
4757        }
4758        if let Some((_, e)) = jsonb_each_text_arg {
4759            visit(FromSlot::Expr(e))?;
4760        }
4761        if let Some(call) = table_fn_call {
4762            for a in call.1.iter_mut() {
4763                visit(FromSlot::Expr(a))?;
4764            }
4765        }
4766        if let Some(items) = rows_from {
4767            for (_, args) in items.iter_mut() {
4768                for a in args.iter_mut() {
4769                    visit(FromSlot::Expr(a))?;
4770                }
4771            }
4772        }
4773        if let Some(jt) = json_table {
4774            let JsonTable {
4775                doc,
4776                row_path: _,
4777                columns: _,
4778                passing,
4779            } = jt.as_mut();
4780            visit(FromSlot::Expr(doc))?;
4781            for (_, e) in passing.iter_mut() {
4782                visit(FromSlot::Expr(e))?;
4783            }
4784        }
4785        Ok(())
4786    }
4787
4788    /// The shared twin of [`Self::try_for_each_slot_mut`], for analysis
4789    /// that reads a statement rather than rewriting it. Same total
4790    /// destructure — a new slot is a compile error in both.
4791    ///
4792    /// # Errors
4793    /// Whatever `visit` returns.
4794    pub fn try_for_each_slot<'a, E>(
4795        &'a self,
4796        visit: &mut dyn FnMut(FromSlotRef<'a>) -> Result<(), E>,
4797    ) -> Result<(), E> {
4798        let Self {
4799            name: _,
4800            alias: _,
4801            only: _,
4802            as_of_segment: _,
4803            unnest_column_aliases: _,
4804            with_ordinality: _,
4805            scalar_fn_item: _,
4806            unnest_expr,
4807            generate_series_args,
4808            lateral_subquery,
4809            jsonb_each_text_arg,
4810            table_fn_call,
4811            rows_from,
4812            json_table,
4813        } = self;
4814        if let Some(e) = unnest_expr {
4815            visit(FromSlotRef::Expr(e))?;
4816        }
4817        if let Some(args) = generate_series_args {
4818            for a in args {
4819                visit(FromSlotRef::Expr(a))?;
4820            }
4821        }
4822        if let Some(sub) = lateral_subquery {
4823            visit(FromSlotRef::Select(sub))?;
4824        }
4825        if let Some((_, e)) = jsonb_each_text_arg {
4826            visit(FromSlotRef::Expr(e))?;
4827        }
4828        if let Some(call) = table_fn_call {
4829            for a in &call.1 {
4830                visit(FromSlotRef::Expr(a))?;
4831            }
4832        }
4833        if let Some(items) = rows_from {
4834            for (_, args) in items {
4835                for a in args {
4836                    visit(FromSlotRef::Expr(a))?;
4837                }
4838            }
4839        }
4840        if let Some(jt) = json_table {
4841            let JsonTable {
4842                doc,
4843                row_path: _,
4844                columns: _,
4845                passing,
4846            } = jt.as_ref();
4847            visit(FromSlotRef::Expr(doc))?;
4848            for (_, e) in passing {
4849                visit(FromSlotRef::Expr(e))?;
4850            }
4851        }
4852        Ok(())
4853    }
4854}
4855
4856/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4857#[derive(Debug, Clone, PartialEq)]
4858pub struct JsonTable {
4859    /// The document expression (jsonb/json/text). May reference outer
4860    /// columns → implicit LATERAL.
4861    pub doc: Box<Expr>,
4862    /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4863    /// match is one row's context item.
4864    pub row_path: String,
4865    /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4866    pub columns: Vec<JsonTableColumn>,
4867    /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4868    pub passing: Vec<(String, Expr)>,
4869}
4870
4871/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4872#[derive(Debug, Clone, PartialEq)]
4873pub enum JsonTableColumn {
4874    /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4875    Ordinality { name: String },
4876    /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4877    /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4878    /// `<name> <type> EXISTS [PATH '<p>']`.
4879    Regular {
4880        name: String,
4881        ty: ColumnTypeName,
4882        /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4883        path: String,
4884        /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4885        exists: bool,
4886        /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4887        format_json: bool,
4888        /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4889        wrapper: bool,
4890        /// Behaviour when the path matches nothing (default NULL).
4891        on_empty: JsonTableOnBehavior,
4892        /// Behaviour when coercion fails (default NULL).
4893        on_error: JsonTableOnBehavior,
4894    },
4895    /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4896    /// row like a LEFT JOIN (a parent with no nested match still emits one
4897    /// row, nested cols NULL).
4898    Nested {
4899        path: String,
4900        columns: Vec<JsonTableColumn>,
4901    },
4902}
4903
4904/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4905#[derive(Debug, Clone, PartialEq)]
4906pub enum JsonTableOnBehavior {
4907    /// Default: the column value is NULL.
4908    Null,
4909    /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4910    Error,
4911    /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4912    Default(Box<Expr>),
4913}
4914
4915/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4916/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4917/// joins evaluate left-associatively in nested-loop order.
4918#[derive(Debug, Clone, PartialEq)]
4919pub struct FromClause {
4920    pub primary: TableRef,
4921    pub joins: Vec<FromJoin>,
4922}
4923
4924#[derive(Debug, Clone, PartialEq)]
4925pub struct FromJoin {
4926    pub kind: JoinKind,
4927    pub table: TableRef,
4928    /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4929    pub on: Option<Expr>,
4930    /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4931    /// USING column list so the executor can perform PG's column-merge
4932    /// (the join columns collapse to a single unqualified output column,
4933    /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4934    /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4935    /// USING into an equivalent `on` predicate so the join filter/count
4936    /// path works unchanged; `using_cols` drives only the output-shape
4937    /// rewrite. Empty/`None` for `ON` and CROSS joins.
4938    pub using_cols: Option<Vec<String>>,
4939    /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4940    /// column names are not known until the table schemas are available
4941    /// (parse time is schema-less), so the parser only sets this flag and
4942    /// leaves `on`/`using_cols` empty; the engine resolves the common
4943    /// columns at execution time, synthesises the `on` predicate + the
4944    /// USING column-merge, and clears the flag. If there are no common
4945    /// columns PG treats it as a CROSS join.
4946    pub natural: bool,
4947}
4948
4949#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4950pub enum JoinKind {
4951    Inner,
4952    Left,
4953    Cross,
4954    /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4955    /// NULL-filling the left (drive) columns on unmatched right rows.
4956    /// The executor runs the LEFT algorithm's mirror: it tracks which
4957    /// peer rows matched and emits the unmatched ones with a NULL-left
4958    /// tuple after the probe loop. Output column order is unchanged
4959    /// (left-table cols then right-table cols).
4960    Right,
4961    /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4962    /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4963    FullOuter,
4964    /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4965    /// once, paired with the first peer row that satisfies the ON. Not
4966    /// reachable from SQL — the EXISTS pull-up emits it, which is what
4967    /// frees positive EXISTS from the round-721 uniqueness gate (an
4968    /// INNER join would multiply the outer rows; a semi join cannot).
4969    Semi,
4970}
4971
4972#[derive(Debug, Clone, PartialEq)]
4973pub enum Expr {
4974    Literal(Literal),
4975    /// v7.39.2 — `<expr> COLLATE <name>`: the collation this expression
4976    /// compares under, whatever the column or the database says.
4977    ///
4978    /// The parser used to refuse the locale names in this position and
4979    /// SILENTLY ABSORB the byte-order ones, so `'a' COLLATE "C" < 'B'`
4980    /// answered `t` where PostgreSQL 18.6 answers `f` — the one family
4981    /// it let through is the one where dropping it changes the answer.
4982    ///
4983    /// Whether dropping is safe depends on the DATABASE's own collation,
4984    /// which the parser cannot see: under `SPG_LC_COLLATE=C` absorbing
4985    /// `COLLATE "C"` is exactly right. So the name rides along and the
4986    /// engine, which knows, decides.
4987    Collate {
4988        expr: Box<Expr>,
4989        collation: String,
4990    },
4991    Column(ColumnName),
4992    /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4993    /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4994    /// callee's declared parameter names, and a user function's live in the
4995    /// catalog — which the parser cannot see. So the name rides along in the
4996    /// tree and the evaluator, which has the catalog, does the reordering.
4997    /// Appears only inside a `FunctionCall`'s argument list.
4998    NamedArg {
4999        name: String,
5000        expr: Box<Expr>,
5001    },
5002    /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
5003    /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
5004    /// expression evaluates to an array whose elements the evaluator splices
5005    /// into the call as individual trailing arguments. Appears only inside a
5006    /// `FunctionCall`'s argument list.
5007    Variadic(Box<Expr>),
5008    /// v6.1.1 — `$N` parameter placeholder for the extended query
5009    /// protocol. The number is 1-based per PostgreSQL convention.
5010    /// Evaluation looks up `params[N-1]` from the prepared-statement
5011    /// bind buffer; out-of-range indices raise a runtime error
5012    /// (same shape as a column-not-found miss).
5013    Placeholder(u16),
5014    Binary {
5015        lhs: Box<Expr>,
5016        op: BinOp,
5017        rhs: Box<Expr>,
5018    },
5019    Unary {
5020        op: UnOp,
5021        expr: Box<Expr>,
5022    },
5023    /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
5024    /// TEXT, BOOL targets; engine coerces at evaluation time.
5025    Cast {
5026        expr: Box<Expr>,
5027        target: CastTarget,
5028    },
5029    /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
5030    /// evaluates to a composite/record value (an explicit `ROW(...)`, a
5031    /// whole-row reference, or a composite-returning function); `field` names
5032    /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
5033    /// column names for a whole-row). Only the parenthesised form reaches
5034    /// here — a bare `a.b` is parsed as a qualified column reference.
5035    FieldAccess {
5036        base: Box<Expr>,
5037        field: String,
5038    },
5039    /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
5040    IsNull {
5041        expr: Box<Expr>,
5042        negated: bool,
5043    },
5044    /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
5045    /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
5046    /// `Some(false)` for FALSE and `None` for UNKNOWN.
5047    ///
5048    /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
5049    /// The semantics were right, but the AST then had no way to say what
5050    /// the user wrote, so every renderer printed the lowering:
5051    /// `CHECK ((a > 1) IS TRUE)` came back as
5052    /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
5053    /// dumped view lost the form too.
5054    BoolTest {
5055        expr: Box<Expr>,
5056        value: Option<bool>,
5057        negated: bool,
5058    },
5059    /// Function call `name(args...)`. v1.4 supports a small built-in set
5060    /// (length, upper, lower, abs, coalesce); unknown names error at eval
5061    /// time so the parser stays open for v1.5 aggregates.
5062    FunctionCall {
5063        name: String,
5064        args: Vec<Expr>,
5065    },
5066    /// v7.24 (mailrs round-16 A) — an aggregate call with an
5067    /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
5068    /// Wraps the plain [`Expr::FunctionCall`] so every existing
5069    /// FunctionCall consumer stays untouched; only the aggregate
5070    /// executor (and the expression walkers) know the wrapper.
5071    /// Non-aggregate evaluation contexts reject it at eval time.
5072    AggregateOrdered {
5073        call: Box<Expr>,
5074        order_by: Vec<OrderBy>,
5075        /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
5076        /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
5077        /// aggregate modifier so plain FunctionCall stays untouched.
5078        distinct: bool,
5079        /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
5080        /// Only the rows where `cond` is true contribute to this
5081        /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
5082        /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
5083        /// END)`, which is faithful for NULL-ignoring aggregates but
5084        /// WRONG for `array_agg` (it would collect a NULL per excluded
5085        /// row). The executor instead skips excluded rows before
5086        /// accumulation, which is correct for every aggregate.
5087        filter: Option<Box<Expr>>,
5088    },
5089    /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
5090    /// wildcards are `%` (any run) and `_` (one char), backslash escapes
5091    /// the next char (so `\%` matches a literal `%`).
5092    Like {
5093        expr: Box<Expr>,
5094        pattern: Box<Expr>,
5095        negated: bool,
5096        /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
5097        /// match. PG folds both operands.
5098        case_insensitive: bool,
5099    },
5100    /// v4.12 window function call: `name(args) OVER (PARTITION BY
5101    /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
5102    /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
5103    /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
5104    /// unordered windows and "from start of partition through
5105    /// current row" for ordered windows — no explicit ROWS /
5106    /// RANGE clause in v4.12 MVP.
5107    WindowFunction {
5108        name: String,
5109        args: Vec<Expr>,
5110        partition_by: Vec<Expr>,
5111        /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
5112        /// (None = PG default, same contract as [`OrderBy`]).
5113        order_by: Vec<(
5114            Expr,
5115            bool,         /* desc */
5116            Option<bool>, /* nulls_first */
5117        )>,
5118        /// v4.20 explicit frame. `None` means "use the default":
5119        /// whole-partition when unordered, running aggregate from
5120        /// partition start through current row when ordered.
5121        frame: Option<WindowFrame>,
5122        /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
5123        /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
5124        /// `Respect` (PG / ANSI default — NULLs participate). Other
5125        /// window functions ignore this flag.
5126        null_treatment: NullTreatment,
5127        /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
5128        /// = no FILTER. Only aggregate window functions honor it; the
5129        /// predicate restricts which peer rows contribute within the frame.
5130        filter: Option<Box<Expr>>,
5131    },
5132    /// v4.10 scalar subquery — `(SELECT ...)` used in expression
5133    /// position. Must return exactly one row × one column at eval
5134    /// time; the engine errors out otherwise. Uncorrelated only —
5135    /// the inner SELECT cannot reference outer columns.
5136    ScalarSubquery(Box<SelectStatement>),
5137    /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
5138    /// projection is ignored; only row-count matters.
5139    Exists {
5140        subquery: Box<SelectStatement>,
5141        negated: bool,
5142    },
5143    /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
5144    /// project exactly one column; membership is tested by Eq
5145    /// against each row's value (NULL handling follows ANSI:
5146    /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
5147    InSubquery {
5148        expr: Box<Expr>,
5149        subquery: Box<SelectStatement>,
5150        negated: bool,
5151    },
5152    /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
5153    /// against a multi-column subquery. Row comparisons against a *list*
5154    /// decompose to OR-of-AND at parse time, but the subquery form can't
5155    /// (its rows are only known at runtime), so this survives as its own
5156    /// node evaluated with PG's row-comparison three-valued logic.
5157    RowInSubquery {
5158        row: Vec<Expr>,
5159        subquery: Box<SelectStatement>,
5160        negated: bool,
5161    },
5162    /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
5163    /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
5164    /// RowInSubquery, the literal-RHS form decomposes at parse time but the
5165    /// subquery form can't, so it survives as its own node. The subquery
5166    /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
5167    RowCmpSubquery {
5168        row: Vec<Expr>,
5169        op: BinOp,
5170        subquery: Box<SelectStatement>,
5171    },
5172    /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
5173    /// list. Both the parser's literal-list path and the engine's
5174    /// IN-subquery materialisation used to desugar into a left-deep
5175    /// OR-Eq chain, so expression depth scaled with the element count
5176    /// — a 24k-row subquery result overflowed the 2 MiB worker stack
5177    /// (recursive eval AND recursive Box drop) and aborted embedding
5178    /// host processes. The flat node keeps depth constant: eval is an
5179    /// iterative scan with PG three-valued logic, drop is a Vec drop.
5180    InList {
5181        expr: Box<Expr>,
5182        list: Vec<Expr>,
5183        negated: bool,
5184    },
5185    /// `EXTRACT(<field> FROM <source>)` — pull an integer component
5186    /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
5187    /// because the `FROM` keyword is what separates the two halves,
5188    /// not a comma.
5189    Extract {
5190        field: ExtractField,
5191        source: Box<Expr>,
5192    },
5193    /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
5194    /// element is evaluated independently; NULLs are allowed.
5195    /// v7.10 supports only single-dimension TEXT[] semantically;
5196    /// non-text elements coerce at engine evaluation time when
5197    /// the surrounding context (column type / cast) makes the
5198    /// target clear.
5199    Array(Vec<Expr>),
5200    /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
5201    /// engine returns NULL for out-of-range indices.
5202    ArraySubscript {
5203        target: Box<Expr>,
5204        index: Box<Expr>,
5205    },
5206    /// Array slice `arr[lo:hi]` — PG 1-based, both ends
5207    /// inclusive; a missing bound extends to that end of the
5208    /// array and out-of-range bounds clamp. Returns an array of
5209    /// the same element type.
5210    ArraySlice {
5211        target: Box<Expr>,
5212        lo: Option<Box<Expr>>,
5213        hi: Option<Box<Expr>>,
5214    },
5215    /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
5216    /// operator is the comparison binary op (Eq / Ne / Lt / …);
5217    /// the engine desugars: `ANY` returns true if any element
5218    /// satisfies; `ALL` returns true only if every element does.
5219    /// NULL handling follows PG's three-valued logic.
5220    AnyAll {
5221        expr: Box<Expr>,
5222        op: BinOp,
5223        array: Box<Expr>,
5224        /// `true` = ANY, `false` = ALL.
5225        is_any: bool,
5226    },
5227    /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
5228    /// (searched form, `operand` is None) and
5229    /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
5230    /// `operand` is the lead expression compared against each
5231    /// branch's match). Each `(when_expr, then_expr)` branch
5232    /// stays as written; engine short-circuits on the first match.
5233    /// `else_branch` is `None` when no ELSE; evaluates to NULL.
5234    /// mailrs round-5 G9.
5235    Case {
5236        operand: Option<Box<Expr>>,
5237        branches: Vec<(Expr, Expr)>,
5238        else_branch: Option<Box<Expr>>,
5239    },
5240}
5241
5242/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
5243/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
5244/// in the offset walk. `Ignore` causes the function to skip NULL
5245/// values in the argument expression, returning the next non-NULL.
5246#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5247#[non_exhaustive]
5248pub enum NullTreatment {
5249    #[default]
5250    Respect,
5251    Ignore,
5252}
5253
5254/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
5255/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
5256/// where end implicitly = CURRENT ROW.
5257#[derive(Debug, Clone, PartialEq, Eq)]
5258pub struct WindowFrame {
5259    pub kind: FrameKind,
5260    pub start: FrameBound,
5261    pub end: Option<FrameBound>,
5262    /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
5263    /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
5264    /// no-op; CURRENT ROW drops the current row from the frame.
5265    pub exclude: FrameExclusion,
5266}
5267
5268#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5269pub enum FrameExclusion {
5270    /// Default — exclude nothing.
5271    #[default]
5272    NoOthers,
5273    /// Drop the current row from the frame.
5274    CurrentRow,
5275    /// Drop the current row's whole peer group.
5276    Group,
5277    /// Drop the current row's peers but keep the current row.
5278    Ties,
5279}
5280
5281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5282pub enum FrameKind {
5283    Rows,
5284    Range,
5285    /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
5286    /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
5287    /// bounds (no explicit integer offsets) GROUPS behaves identically
5288    /// to RANGE — both consult the peer-group of the current row.
5289    /// Integer offsets are not yet supported; the executor rejects
5290    /// them at run time.
5291    Groups,
5292}
5293
5294#[derive(Debug, Clone, PartialEq, Eq)]
5295pub enum FrameBound {
5296    UnboundedPreceding,
5297    OffsetPreceding(u64),
5298    CurrentRow,
5299    OffsetFollowing(u64),
5300    UnboundedFollowing,
5301    /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
5302    /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
5303    /// interval is folded to its (months, days, micros) components at
5304    /// parse time.
5305    IntervalPreceding {
5306        months: i32,
5307        days: i32,
5308        micros: i64,
5309    },
5310    IntervalFollowing {
5311        months: i32,
5312        days: i32,
5313        micros: i64,
5314    },
5315}
5316
5317impl fmt::Display for FrameBound {
5318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5319        match self {
5320            Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
5321            Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
5322            Self::CurrentRow => f.write_str("CURRENT ROW"),
5323            Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
5324            Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
5325            Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
5326            Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
5327        }
5328    }
5329}
5330
5331#[derive(Debug, Clone, PartialEq, Eq)]
5332pub enum ExtractField {
5333    Year,
5334    Month,
5335    Day,
5336    Hour,
5337    Minute,
5338    Second,
5339    Microsecond,
5340    /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
5341    /// SPG keeps the integer convention — truncated seconds).
5342    Epoch,
5343    /// Day of week, 0 = Sunday … 6 = Saturday.
5344    Dow,
5345    /// ISO day of week, 1 = Monday … 7 = Sunday.
5346    Isodow,
5347    /// Day of year, 1-366.
5348    Doy,
5349    /// ISO 8601 week number, 1-53.
5350    Week,
5351    /// ISO 8601 week-numbering year (pairs with `Week`).
5352    Isoyear,
5353    /// Quarter, 1-4.
5354    Quarter,
5355    /// Year divided by 10 (floor).
5356    Decade,
5357    /// Century — 2001-2100 is century 21.
5358    Century,
5359    /// Millennium — 2001-3000 is millennium 3.
5360    Millennium,
5361    /// Julian day number (truncated for timestamps).
5362    Julian,
5363    /// Seconds and fraction in milliseconds (ss·1000 + frac).
5364    Millisecond,
5365    /// UTC offset in seconds — SPG sessions run UTC, so 0.
5366    Timezone,
5367    /// Hour component of the UTC offset — 0.
5368    TimezoneHour,
5369    /// Minute component of the UTC offset — 0.
5370    TimezoneMinute,
5371    /// v7.39 (round 253) — a field name the parser does not know. PG
5372    /// resolves EXTRACT fields at RUNTIME and reports them with the
5373    /// source type (`unit "nosuch" not recognized for type timestamp
5374    /// without time zone`, 22023), so the parser carries the raw name
5375    /// instead of rejecting.
5376    Other(String),
5377}
5378
5379impl fmt::Display for ExtractField {
5380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5381        f.write_str(match self {
5382            Self::Year => "YEAR",
5383            Self::Month => "MONTH",
5384            Self::Day => "DAY",
5385            Self::Hour => "HOUR",
5386            Self::Minute => "MINUTE",
5387            Self::Second => "SECOND",
5388            Self::Microsecond => "MICROSECOND",
5389            Self::Epoch => "EPOCH",
5390            Self::Dow => "DOW",
5391            Self::Isodow => "ISODOW",
5392            Self::Doy => "DOY",
5393            Self::Week => "WEEK",
5394            Self::Isoyear => "ISOYEAR",
5395            Self::Quarter => "QUARTER",
5396            Self::Decade => "DECADE",
5397            Self::Century => "CENTURY",
5398            Self::Millennium => "MILLENNIUM",
5399            Self::Julian => "JULIAN",
5400            Self::Millisecond => "MILLISECOND",
5401            Self::Timezone => "TIMEZONE",
5402            Self::TimezoneHour => "TIMEZONE_HOUR",
5403            Self::TimezoneMinute => "TIMEZONE_MINUTE",
5404            Self::Other(name) => return f.write_str(name),
5405        })
5406    }
5407}
5408
5409#[derive(Debug, Clone, PartialEq, Eq)]
5410pub enum CastTarget {
5411    Int,
5412    BigInt,
5413    Float,
5414    Text,
5415    Bool,
5416    Vector,
5417    Date,
5418    Timestamp,
5419    /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
5420    /// H3a. Engine reuses the existing runtime-interval / timestamp
5421    /// paths (parse the text input, return the matching Value).
5422    Interval,
5423    Timestamptz,
5424    /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
5425    /// types (v7.9.0); the cast just routes Text→Json with the
5426    /// requested OID for the wire layer.
5427    Json,
5428    Jsonb,
5429    /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
5430    /// compatibility; engine surfaces as Unsupported with a
5431    /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
5432    RegType,
5433    RegClass,
5434    /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
5435    /// the PG external array form `{a,b,NULL}`.
5436    TextArray,
5437    /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
5438    /// `{1,2,3}` or widens a `TextArray` whose elements are
5439    /// integer-shaped.
5440    IntArray,
5441    BigIntArray,
5442    /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
5443    /// external form text representation. Used by pg_dump output
5444    /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
5445    TsVector,
5446    TsQuery,
5447    /// v7.17.0 — `::uuid`. Decodes the LHS Text via
5448    /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
5449    /// unhyphenated, uppercase, and brace-wrapped forms); malformed
5450    /// input is a SQL error.
5451    Uuid,
5452    /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
5453    /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
5454    /// inputs pass through unchanged. Closes the mailrs D-pre #3
5455    /// reverse-acceptance gap — anywhere a PG schema writes
5456    /// `expr::bytea`, SPG now matches.
5457    Bytea,
5458    /// v7.37.5 ship triage — generic cast target for the long tail
5459    /// of PG type names the parser meets in `expr::TYPE` shapes that
5460    /// don't deserve their own enum variant. The engine routes these
5461    /// through `column_type_to_data_type` + the existing typed
5462    /// `coerce_value` dispatch, so adding a new PG type to SPG
5463    /// implicitly adds its cast-target form too — no parser change
5464    /// per type. The string carries the lowercase PG type ident
5465    /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
5466    /// a clear message when the type isn't known.
5467    Named(String),
5468}
5469
5470impl fmt::Display for CastTarget {
5471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5472        f.write_str(match self {
5473            Self::Int => "int",
5474            Self::BigInt => "bigint",
5475            Self::Float => "float",
5476            Self::Text => "text",
5477            Self::Bool => "bool",
5478            Self::Vector => "vector",
5479            Self::Interval => "interval",
5480            Self::Timestamptz => "timestamptz",
5481            Self::Json => "json",
5482            Self::Jsonb => "jsonb",
5483            Self::RegType => "regtype",
5484            Self::RegClass => "regclass",
5485            Self::Date => "date",
5486            Self::Timestamp => "timestamp",
5487            Self::TextArray => "TEXT[]",
5488            Self::IntArray => "INT[]",
5489            Self::BigIntArray => "BIGINT[]",
5490            Self::TsVector => "tsvector",
5491            Self::TsQuery => "tsquery",
5492            Self::Uuid => "uuid",
5493            Self::Bytea => "bytea",
5494            // v7.37.5 — `Self::Named` carries its own canonical name.
5495            Self::Named(name) => return f.write_str(name),
5496        })
5497    }
5498}
5499
5500#[derive(Debug, Clone, PartialEq)]
5501pub enum Literal {
5502    Integer(i64),
5503    Float(f64),
5504    /// Exact decimal literal — a bare `12.34`-style token, kept as
5505    /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
5506    /// before it becomes a `Value::Numeric`. PG parses such literals as
5507    /// `numeric`, not `double precision`. (Scientific/huge literals stay
5508    /// `Float`.)
5509    Numeric {
5510        unscaled: i128,
5511        /// v7.39 (round 271) — widened to u16. At u8 a literal with more
5512        /// than 255 decimal places could not be represented, and the
5513        /// conversion's `.expect("lexer-validated decimal")` aborted the
5514        /// query with an internal error on SQL PG accepts.
5515        scale: u16,
5516    },
5517    /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
5518    /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
5519    /// `Value::NumericBig` at eval; previously such literals fell back to double.
5520    NumericBig(String),
5521    String(String),
5522    /// v7.38.8 — a temporal constant that has already been decoded.
5523    ///
5524    /// Without these the only way to carry one through the AST was as
5525    /// text, and a predicate comparing a `timestamp` column against a
5526    /// literal then coerced that text back into a timestamp ONCE PER
5527    /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
5528    /// profile. `constfold` produced text for the same reason: its exit
5529    /// had nothing else to hand back.
5530    ///
5531    /// `text` keeps the spelling so `Display` round-trips byte for byte,
5532    /// the way `Interval` already does and for the same reason: this
5533    /// node is printed in EXPLAIN, in dumps and in error messages, and
5534    /// none of those should change because the value stopped being
5535    /// carried as a string. The enum already holds a `String` and an
5536    /// `i128`, so neither variant widens it.
5537    Timestamp {
5538        micros: i64,
5539        text: String,
5540    },
5541    /// Days since the epoch `Value::Date` counts from. See
5542    /// [`Literal::Timestamp`].
5543    Date {
5544        days: i32,
5545        text: String,
5546    },
5547    Bool(bool),
5548    Null,
5549    /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
5550    Vector(Vec<f32>),
5551    /// TEXT[] value carried through the prepared-bind path
5552    /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
5553    /// text form, so the array rides the AST natively).
5554    TextArray(Vec<Option<String>>),
5555    /// INT[] value carried through the prepared-bind path.
5556    IntArray(Vec<Option<i32>>),
5557    /// BIGINT[] value carried through the prepared-bind path.
5558    BigIntArray(Vec<Option<i64>>),
5559    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
5560    /// Three independent dimensions: `months` (variable-length;
5561    /// year/month), `days` (fixed 86400 seconds at non-DST, but
5562    /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
5563    /// stays distinguishable), and `micros` (sub-day; can carry).
5564    /// `text` keeps the original spelling so Display round-trips
5565    /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
5566    Interval {
5567        months: i32,
5568        days: i32,
5569        micros: i64,
5570        text: String,
5571    },
5572}
5573
5574#[derive(Debug, Clone, PartialEq, Eq)]
5575pub struct ColumnName {
5576    pub qualifier: Option<String>,
5577    pub name: String,
5578}
5579
5580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5581pub enum BinOp {
5582    Or,
5583    And,
5584    Eq,
5585    NotEq,
5586    /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
5587    /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
5588    /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
5589    /// non-NULL behaviour matches `<>` / `=` exactly. Common in
5590    /// PG-style JOIN ON predicates and pg_dump output.
5591    IsDistinctFrom,
5592    IsNotDistinctFrom,
5593    /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
5594    /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
5595    /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
5596    /// is a real division (round 351).
5597    IntDiv,
5598    Lt,
5599    LtEq,
5600    Gt,
5601    GtEq,
5602    Add,
5603    Sub,
5604    Mul,
5605    Div,
5606    /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
5607    /// precedence as Mul/Div; result type follows left operand.
5608    Mod,
5609    /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
5610    /// operands of equal dimension; engine returns `Value::Float(d)`.
5611    L2Distance,
5612    /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
5613    GeomParallel,
5614    /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
5615    OverLeft,
5616    OverRight,
5617    /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
5618    GeomPerp,
5619    /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
5620    GeomSameAs,
5621    /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
5622    /// object to the left-hand one.
5623    ClosestPoint,
5624    /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
5625    GeomHoriz,
5626    /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
5627    /// more similar" remains true (matches pgvector's published convention).
5628    InnerProduct,
5629    /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
5630    CosineDistance,
5631    /// SQL string concatenation `||`. NULL propagates.
5632    Concat,
5633    /// Bitwise OR `|` on integers.
5634    BitOr,
5635    /// Bitwise AND `&` on integers.
5636    BitAnd,
5637    /// Bitwise XOR `#` on integers and equal-length bit strings.
5638    BitXor,
5639    /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
5640    /// sides as truth values and returns their exclusive-or (`1 XOR 0`
5641    /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
5642    /// MySQL dialect produces it; PG has no logical XOR. Its precedence
5643    /// sits between OR (loosest) and AND.
5644    LogicalXor,
5645    /// v4.14 `json -> key` — element access by string key (object)
5646    /// or integer index (array). Returns a JSON value.
5647    JsonGet,
5648    /// v4.14 `json ->> key` — same access, returns the result as
5649    /// TEXT (unwraps a top-level JSON string; renders other scalars
5650    /// as their canonical text).
5651    JsonGetText,
5652    /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
5653    /// text array literal like `'{a,0,b}'`. Returns JSON.
5654    JsonGetPath,
5655    /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
5656    JsonGetPathText,
5657    /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
5658    /// when every key/value in `sub_json` is structurally present in
5659    /// the left side. Matches PG semantics (top-level + recursive).
5660    JsonContains,
5661    /// `@?` — jsonb path existence (jsonb_path_exists).
5662    JsonPathExists,
5663    /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
5664    /// `a <@ b` is defined as `b @> a` (same semantics, swapped
5665    /// sides). Eval dispatch reuses `JsonContains` with swapped args.
5666    JsonContainedBy,
5667    /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
5668    /// returns BOOL. For an object, true if `key` is an existing
5669    /// member name; for an array, true if any element is the string
5670    /// `key` (PG semantics).
5671    JsonKeyExists,
5672    /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
5673    /// returns BOOL.
5674    JsonKeysAny,
5675    /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
5676    /// returns BOOL.
5677    JsonKeysAll,
5678    /// `jsonb #- path_text[]` — delete the value at a nested path.
5679    /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
5680    JsonDeletePath,
5681    /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
5682    /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
5683    /// tsvector` and engine eval normalises either ordering.
5684    TsMatch,
5685    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
5686    /// `<<`. LHS network is strictly inside RHS network (no equality).
5687    InetContainedBy,
5688    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
5689    /// `<<=`. LHS network ⊆ RHS network.
5690    InetContainedByEq,
5691    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
5692    /// LHS network strictly contains RHS network.
5693    InetContains,
5694    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
5695    /// LHS network ⊇ RHS network.
5696    InetContainsEq,
5697    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
5698    /// True iff either network contains any address of the other.
5699    InetOverlap,
5700    /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
5701    /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
5702    Intersects,
5703    /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
5704    /// (point, box).
5705    IsBelow,
5706    IsAbove,
5707    /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
5708    /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
5709    /// where `'A' < 'a'` is false under a non-C collation, which is the
5710    /// whole reason the operator family exists — it is what makes a LIKE
5711    /// prefix index-usable. pg_dump writes these into index definitions.
5712    PatternLt,
5713    PatternLtEq,
5714    PatternGt,
5715    PatternGtEq,
5716}
5717
5718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5719pub enum UnOp {
5720    Not,
5721    Neg,
5722    /// Bitwise NOT `~` on integers.
5723    BitNot,
5724    /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
5725    /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
5726    /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
5727    /// while PG18 and MariaDB accept every one of them.
5728    ///
5729    /// It is not a no-op to drop at parse time — PG refuses it on
5730    /// non-numeric operands ("operator does not exist: + boolean"), so the
5731    /// operand's type has to be seen at eval.
5732    Plus,
5733}
5734
5735// --- Display impls (round-trip-safe) --------------------------------------
5736
5737impl Statement {
5738    /// v7.18 — classify whether the statement is read-only at
5739    /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
5740    /// route SELECT-shaped traffic through the fan-out
5741    /// `AsyncReadHandle` (no writer-lock contention) while
5742    /// keeping DML / DDL / TX-control on the single-writer path.
5743    ///
5744    /// The classification matches what
5745    /// `Engine::execute_readonly_with_cancel` accepts: anything
5746    /// that does NOT mutate catalog, statistics, session state,
5747    /// or transaction state. WaitForWalPosition is included
5748    /// (engine returns `Unsupported`, but the classification is
5749    /// semantically read-only — no mutation). Empty is excluded
5750    /// out of an abundance of caution — the no-op routes
5751    /// through the writer so any future side effect lands
5752    /// uniformly.
5753    ///
5754    /// **Not connection-state aware**. `SET LOCAL` / `RESET`
5755    /// affect session parameters and must run on the writer
5756    /// engine that owns the session state; they classify as
5757    /// writer-path here. Same for `BEGIN` / `COMMIT` /
5758    /// `ROLLBACK` / `SAVEPOINT` — transaction control is
5759    /// always writer-path.
5760    /// v7.39 (round 435) — does this statement implicitly COMMIT an open
5761    /// transaction under MySQL?
5762    ///
5763    /// PG runs DDL inside the transaction; MySQL commits before (and after)
5764    /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
5765    /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
5766    /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
5767    /// nested START TRANSACTION; and measured NOT to fire for `CREATE
5768    /// TEMPORARY TABLE`, `SET`, or a SELECT.
5769    ///
5770    /// A positive list, not "everything that is not DML": a statement
5771    /// wrongly listed here commits a client's data early, which is as bad as
5772    /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
5773    /// COMPACT) are left out — a MySQL session never sends them.
5774    #[must_use]
5775    pub fn mysql_implicit_commit(&self) -> bool {
5776        match self {
5777            // MySQL's documented exception, measured on MariaDB 11: a
5778            // TEMPORARY table is not DDL for this purpose and does not
5779            // commit. (Round 435 got this for free because the parser then
5780            // lowered that spelling to `Statement::Empty`; round 436 made it
5781            // a real CREATE TABLE, and the round-435 pin caught it.)
5782            Self::CreateTable(c) => !c.temporary,
5783            // MySQL commits the open transaction and opens a fresh one.
5784            Self::Begin { .. }
5785            | Self::DropTable { .. }
5786            | Self::DropIndex { .. }
5787            | Self::CreateIndex(_)
5788            | Self::AlterIndex { .. }
5789            | Self::AlterTable(_)
5790            | Self::Truncate { .. }
5791            | Self::Analyze { .. }
5792            | Self::CreateStatistics { .. }
5793            | Self::DropStatistics { .. }
5794            | Self::CreateView { .. }
5795            | Self::DropView { .. }
5796            | Self::CreateMaterializedView { .. }
5797            | Self::RefreshMaterializedView { .. }
5798            | Self::DropMaterializedView { .. }
5799            | Self::CreateSequence(_)
5800            | Self::AlterSequence { .. }
5801            | Self::DropSequence { .. }
5802            | Self::CreateFunction(_)
5803            | Self::DropFunction { .. }
5804            | Self::CreateTrigger(_)
5805            | Self::DropTrigger { .. }
5806            | Self::CreateRule(_)
5807            | Self::DropRule { .. }
5808            | Self::CreateType(_)
5809            | Self::DropType { .. }
5810            | Self::AlterTypeAddValue { .. }
5811            | Self::AlterTypeRenameValue { .. }
5812            | Self::CreateDomain(_)
5813            | Self::AlterDomain { .. }
5814            | Self::DropDomain { .. }
5815            | Self::CreateSchema { .. }
5816            | Self::DropSchema { .. }
5817            | Self::CreateUser { .. }
5818            | Self::DropUser { .. }
5819            | Self::Grant { .. }
5820            | Self::Revoke { .. }
5821            | Self::CreatePolicy(_)
5822            | Self::AlterPolicy(_)
5823            | Self::DropPolicy { .. }
5824            | Self::CommentOn { .. }
5825            | Self::CreateExtension { .. } => true,
5826            _ => false,
5827        }
5828    }
5829
5830    #[must_use]
5831    pub fn is_readonly(&self) -> bool {
5832        match self {
5833            Statement::RenameTables(_) => false,
5834            // v7.39 (round 288) — SET CONSTRAINTS changes transaction
5835            // state, and IMMEDIATE can run the deferred checks there and
5836            // then; writer-path.
5837            Statement::SetConstraints { .. } => false,
5838            // v7.39 (round 695) — it writes nothing (SPG has no
5839            // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
5840            // writer and a read-only session refuses it there too.
5841            Statement::AlterSystem { .. } => false,
5842            // Same shape: a no-op here, a writer to PG, so a read-only
5843            // session refuses it as PG's would.
5844            Statement::NoOpPreventedInTransaction { .. } => false,
5845            Statement::DropDatabase { .. } => false,
5846            // v7.39 (round 696) — they perform nothing, so nothing is
5847            // written; PG classes LOCK and the OWNED BY pair as writers and
5848            // a read-only session refuses them there.
5849            Statement::ValidateOnly { .. } => false,
5850            // v7.39 (round 750) — a credential rotation persists.
5851            Statement::AlterRolePassword { .. } => true,
5852            Statement::DropAggregate { .. } => false,
5853            // v7.39 (round 547) — records a GUC default in the catalog.
5854            Statement::SetDbRoleSetting(_) => false,
5855            // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5856            // but they name a relation and PG refuses one that is not
5857            // there, so they are not read-only in the sense this asks.
5858            Statement::Maintain { .. } => false,
5859            // v7.39 (round 277) — the prepared-statement surface is
5860            // session state, like SET; writer-path so it lands on the
5861            // engine that owns the session. EXECUTE may also run a
5862            // write, and its body is only known at execution time.
5863            Statement::Prepare { .. }
5864            | Statement::Execute { .. }
5865            | Statement::Deallocate(_)
5866            | Statement::Call(_)
5867            | Statement::PrepareTransaction(_)
5868            | Statement::CreateStatistics { .. }
5869            | Statement::DropStatistics { .. }
5870            // v7.39 (round 318, V51) — KILL signals another connection;
5871            // it must run on the writer path that owns the registry hook.
5872            | Statement::Kill { .. }
5873            // v7.39 (round 320, V53) — DISCARD throws session state away;
5874            // writer path, like SET / RESET.
5875            | Statement::Discard(_)
5876            // v7.39.2 — `USE <db>` writes session state, the same way
5877            // SET does, and takes the same path.
5878            | Statement::UseDatabase(_) => false,
5879            // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5880            // locks MUTATES the lock table, so it is not a read. Left as
5881            // a read it went to the read-only executor and the locking
5882            // pre-pass never ran at all — the clause was honoured only
5883            // inside an explicit transaction, and silently ignored in
5884            // autocommit, which is where a queue worker runs it.
5885            Statement::Select(s) if s.locking.is_some() => false,
5886            Statement::Select(_)
5887            | Statement::CopyTo { .. }
5888            | Statement::CopyToFile { .. }
5889            | Statement::Explain(_)
5890            | Statement::ShowTables
5891            | Statement::ShowDatabases
5892            | Statement::ShowCreateTable(_)
5893            | Statement::ShowIndexes(_)
5894            | Statement::ShowStatus
5895            | Statement::ShowVariables
5896            | Statement::ShowVariablesLike(_)
5897            | Statement::ShowProcesslist
5898            | Statement::ShowColumns(_)
5899            | Statement::ShowUsers
5900            | Statement::ShowPublications
5901            | Statement::ShowSubscriptions
5902            | Statement::WaitForWalPosition { .. } => true,
5903            // Everything else mutates catalog, statistics,
5904            // session state, or transaction state — writer path.
5905            // Listed explicitly so a new Statement variant fails
5906            // the match exhaustiveness check and forces a
5907            // classification decision at add-site.
5908            Statement::Empty
5909            // v7.39 (round 169) — VACUUM mutates storage (reclaims
5910            // tombstoned versions): writer path.
5911            | Statement::Vacuum { .. }
5912            | Statement::DropTable { .. }
5913            | Statement::DropIndex { .. }
5914            | Statement::CreateTable(_)
5915            | Statement::CreateExtension(_)
5916            | Statement::DoBlock(_)
5917            | Statement::CreateIndex(_)
5918            | Statement::Insert(_)
5919            | Statement::Update(_)
5920            | Statement::Delete(_)
5921            | Statement::Merge(_)
5922            | Statement::Begin(_)
5923            | Statement::Commit
5924            | Statement::Rollback
5925            | Statement::Savepoint(_)
5926            | Statement::RollbackToSavepoint(_)
5927            | Statement::ReleaseSavepoint(_)
5928            | Statement::CreateUser(_)
5929            | Statement::DropUser { .. }
5930            | Statement::SetRole(_)
5931            | Statement::Grant(_)
5932            | Statement::Revoke(_)
5933            | Statement::CreatePolicy(_)
5934            | Statement::AlterPolicy(_)
5935            | Statement::DropPolicy(_)
5936            | Statement::AlterIndex(_)
5937            | Statement::AlterTable(_)
5938            | Statement::CreatePublication(_)
5939            | Statement::DropPublication { .. }
5940            | Statement::CreateSubscription(_)
5941            | Statement::DropSubscription { .. }
5942            | Statement::Analyze(_)
5943            | Statement::Truncate { .. }
5944            | Statement::CompactColdSegments
5945            | Statement::SetParameter { .. }
5946            | Statement::SetParameterList(_)
5947            | Statement::SetUserVars(..)
5948            | Statement::SetTransaction { .. }
5949            | Statement::ShowParameter(_)
5950            | Statement::ResetParameter(_)
5951            | Statement::CreateFunction(_)
5952            | Statement::CreateTrigger(_)
5953            | Statement::DropTrigger { .. }
5954            | Statement::CreateRule(_)
5955            | Statement::DropRule { .. }
5956            | Statement::DropFunction { .. }
5957            | Statement::CreateSequence(_)
5958            | Statement::AlterSequence(_)
5959            | Statement::DropSequence { .. }
5960            | Statement::CreateView(_)
5961            | Statement::DropView { .. }
5962            | Statement::CreateMaterializedView(_)
5963            | Statement::RefreshMaterializedView { .. }
5964            | Statement::DropMaterializedView { .. }
5965            | Statement::CreateType(_)
5966            | Statement::AlterTypeAddValue { .. }
5967            | Statement::AlterTypeRenameValue { .. }
5968            | Statement::CommentOn { .. }
5969            | Statement::DropType { .. }
5970            | Statement::CreateDomain(_)
5971            | Statement::DropDomain { .. }
5972            | Statement::CreateSchema { .. }
5973            | Statement::DropSchema { .. }
5974            // v7.39 (round 218) — cursors mutate per-session cursor state
5975            // (open/position/close) on the writer engine: writer path.
5976            | Statement::DeclareCursor { .. }
5977            | Statement::FetchCursor { .. }
5978            | Statement::MoveCursor { .. }
5979            | Statement::CloseCursor { .. }
5980            // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5981            // state / the notification queue: writer path.
5982            | Statement::Listen(_)
5983            | Statement::Notify { .. }
5984            | Statement::Unlisten(_)
5985            | Statement::CopyFromFile { .. }
5986            | Statement::AlterDomain { .. } => false,
5987        }
5988    }
5989}
5990
5991/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5992/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5993#[derive(Debug, Clone, PartialEq, Eq)]
5994pub struct GrantStatement {
5995    /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5996    /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5997    /// is why they keep the case the user typed.
5998    pub privileges: Vec<GrantPriv>,
5999    /// What the privileges are on.
6000    pub object: GrantObject,
6001    /// The roles granted to / revoked from. An empty string entry = PUBLIC.
6002    pub grantees: Vec<String>,
6003    /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
6004    /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
6005    /// privilege itself).
6006    pub grant_option: bool,
6007}
6008
6009/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
6010/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
6011/// An empty column list means the privilege is table-wide.
6012#[derive(Debug, Clone, PartialEq, Eq)]
6013pub struct GrantPriv {
6014    pub word: String,
6015    pub columns: Vec<String>,
6016}
6017
6018/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
6019/// privileges; every other object class parses and is accepted as a no-op, so
6020/// a pg_dump that grants on schemas / sequences / functions still restores.
6021#[derive(Debug, Clone, PartialEq, Eq)]
6022#[non_exhaustive]
6023pub enum GrantObject {
6024    /// `ON [TABLE] a, b` — the enforced case.
6025    Tables(Vec<String>),
6026    /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
6027    /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
6028    /// granted roles; the grantees are the members.
6029    Roles(Vec<String>),
6030    /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
6031    /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
6032    Sequences(Vec<String>),
6033    /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
6034    Schemas(Vec<String>),
6035    /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
6036    Databases(Vec<String>),
6037    /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
6038    /// (SPG keys functions by name); the argument list parses and is dropped.
6039    Functions(Vec<(String, Option<Vec<String>>)>),
6040    /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
6041    /// every table at GRANT time, exactly like PG.
6042    AllTablesInSchema,
6043    /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
6044    /// message.
6045    Other(String),
6046}
6047
6048impl GrantStatement {
6049    /// Round-trip text. `grant = false` renders the REVOKE form.
6050    fn render(&self, grant: bool) -> alloc::string::String {
6051        use core::fmt::Write as _;
6052        let mut s = alloc::string::String::new();
6053        let privs = if self.privileges.is_empty() {
6054            alloc::string::String::from("ALL")
6055        } else {
6056            let parts: Vec<_> = self
6057                .privileges
6058                .iter()
6059                .map(|p| {
6060                    if p.columns.is_empty() {
6061                        p.word.clone()
6062                    } else {
6063                        let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
6064                        alloc::format!("{} ({})", p.word, cols.join(", "))
6065                    }
6066                })
6067                .collect();
6068            parts.join(", ")
6069        };
6070        let obj = match &self.object {
6071            GrantObject::Tables(t) => {
6072                let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
6073                alloc::format!("TABLE {}", names.join(", "))
6074            }
6075            GrantObject::Roles(r) => {
6076                let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
6077                names.join(", ")
6078            }
6079            GrantObject::Sequences(n) => {
6080                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6081                alloc::format!("SEQUENCE {}", names.join(", "))
6082            }
6083            GrantObject::Schemas(n) => {
6084                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6085                alloc::format!("SCHEMA {}", names.join(", "))
6086            }
6087            GrantObject::Databases(n) => {
6088                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
6089                alloc::format!("DATABASE {}", names.join(", "))
6090            }
6091            GrantObject::Functions(n) => {
6092                let names: Vec<_> = n
6093                    .iter()
6094                    .map(|(name, args)| match args {
6095                        Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
6096                        None => quote_ident(name),
6097                    })
6098                    .collect();
6099                alloc::format!("FUNCTION {}", names.join(", "))
6100            }
6101            GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
6102            GrantObject::Other(k) => k.clone(),
6103        };
6104        let who: Vec<_> = self
6105            .grantees
6106            .iter()
6107            .map(|g| {
6108                if g.is_empty() {
6109                    "PUBLIC".into()
6110                } else {
6111                    quote_ident(g)
6112                }
6113            })
6114            .collect();
6115        if let GrantObject::Roles(_) = &self.object {
6116            let _ = if grant {
6117                write!(s, "GRANT {obj} TO {}", who.join(", "))
6118            } else {
6119                write!(s, "REVOKE {obj} FROM {}", who.join(", "))
6120            };
6121            return s;
6122        }
6123        if grant {
6124            let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
6125            if self.grant_option {
6126                s.push_str(" WITH GRANT OPTION");
6127            }
6128        } else {
6129            s.push_str("REVOKE ");
6130            if self.grant_option {
6131                s.push_str("GRANT OPTION FOR ");
6132            }
6133            let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
6134        }
6135        s
6136    }
6137}
6138
6139impl fmt::Display for Statement {
6140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6141        match self {
6142            Self::Empty => Ok(()),
6143            // v7.39 (round 695) — deparsed the way PG writes it.
6144            // v7.39 (round 696) — never deparsed into a dump (nothing is
6145            // stored), so the shortest faithful spelling of what it was.
6146            Self::DropAggregate { if_exists, items } => {
6147                f.write_str("DROP AGGREGATE ")?;
6148                if *if_exists {
6149                    f.write_str("IF EXISTS ")?;
6150                }
6151                for (i, (name, args)) in items.iter().enumerate() {
6152                    if i > 0 {
6153                        f.write_str(", ")?;
6154                    }
6155                    match args {
6156                        Some(a) => write!(f, "{name}({})", a.join(", "))?,
6157                        None => write!(f, "{name}(*)")?,
6158                    }
6159                }
6160                Ok(())
6161            }
6162            Self::AlterRolePassword { name, password } => {
6163                write!(f, "ALTER ROLE {}", quote_ident(name))?;
6164                match password {
6165                    Some(_) => f.write_str(" PASSWORD '<redacted>'"),
6166                    None => f.write_str(" PASSWORD NULL"),
6167                }
6168            }
6169            Self::ValidateOnly { kind, names } => match kind {
6170                ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
6171                ValidateOnlyKind::RoleName => {
6172                    write!(f, "DROP OWNED BY {}", names.join(", "))
6173                }
6174                ValidateOnlyKind::SessionAuthorization => {
6175                    write!(f, "SET SESSION AUTHORIZATION {}", names.join(", "))
6176                }
6177                ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
6178                ValidateOnlyKind::ExtensionAvailable => {
6179                    write!(f, "CREATE EXTENSION {}", names.join(", "))
6180                }
6181                ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
6182                ValidateOnlyKind::CollationName => {
6183                    write!(f, "DROP COLLATION {}", names.join(", "))
6184                }
6185                ValidateOnlyKind::TsConfigName => {
6186                    write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
6187                }
6188                ValidateOnlyKind::EventTriggerName => {
6189                    write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
6190                }
6191                ValidateOnlyKind::TablespaceName => {
6192                    write!(f, "DROP TABLESPACE {}", names.join(", "))
6193                }
6194                ValidateOnlyKind::LargeObjectOid => {
6195                    write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
6196                }
6197                ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
6198                ValidateOnlyKind::AggregateName => {
6199                    write!(f, "ALTER AGGREGATE {}", names.join(", "))
6200                }
6201                ValidateOnlyKind::ConversionName => {
6202                    write!(f, "DROP CONVERSION {}", names.join(", "))
6203                }
6204                ValidateOnlyKind::LanguageName => {
6205                    write!(f, "DROP LANGUAGE {}", names.join(", "))
6206                }
6207                ValidateOnlyKind::ExtensionInstalled => {
6208                    write!(f, "DROP EXTENSION {}", names.join(", "))
6209                }
6210            },
6211            Self::AlterSystem { parameter } => match parameter {
6212                Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
6213                None => f.write_str("ALTER SYSTEM RESET ALL"),
6214            },
6215            // v7.39 (round 547) — round-trips as PG writes it.
6216            Self::SetDbRoleSetting(st) => {
6217                match (&st.database, &st.role) {
6218                    (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
6219                    (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
6220                    (None, None) => f.write_str("ALTER ROLE ALL")?,
6221                }
6222                if let (Some(d), Some(_)) = (&st.database, &st.role) {
6223                    write!(f, " IN DATABASE {d}")?;
6224                }
6225                match (&st.param, &st.value) {
6226                    (None, _) => f.write_str(" RESET ALL"),
6227                    (Some(p), None) => write!(f, " RESET {p}"),
6228                    (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
6229                }
6230            }
6231            Self::Maintain {
6232                kind,
6233                concurrently,
6234                target,
6235            } => {
6236                f.write_str(match kind {
6237                    crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
6238                    _ => "REINDEX ",
6239                })?;
6240                if *concurrently {
6241                    f.write_str("CONCURRENTLY ")?;
6242                }
6243                if let Some(t) = target {
6244                    f.write_str(t)?;
6245                }
6246                Ok(())
6247            }
6248            Self::DropDatabase { name, if_exists } => {
6249                f.write_str("DROP DATABASE ")?;
6250                if *if_exists {
6251                    f.write_str("IF EXISTS ")?;
6252                }
6253                f.write_str(name)
6254            }
6255            Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
6256            Self::SetConstraints { names, deferred } => {
6257                f.write_str("SET CONSTRAINTS ")?;
6258                if names.is_empty() {
6259                    f.write_str("ALL")?;
6260                } else {
6261                    for (i, n) in names.iter().enumerate() {
6262                        if i > 0 {
6263                            f.write_str(", ")?;
6264                        }
6265                        f.write_str(n)?;
6266                    }
6267                }
6268                f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
6269            }
6270            // v7.39 (round 277) — the source text is kept verbatim so
6271            // `pg_prepared_statements.statement` can report it the way
6272            // PG does (the whole PREPARE statement, not just the body).
6273            Self::Prepare { source, .. } => f.write_str(source),
6274            Self::Execute { name, args } => {
6275                write!(f, "EXECUTE {}", quote_ident(name))?;
6276                if !args.is_empty() {
6277                    f.write_str("(")?;
6278                    for (i, a) in args.iter().enumerate() {
6279                        if i > 0 {
6280                            f.write_str(", ")?;
6281                        }
6282                        write!(f, "{a}")?;
6283                    }
6284                    f.write_str(")")?;
6285                }
6286                Ok(())
6287            }
6288            Self::CreateStatistics {
6289                name,
6290                if_not_exists,
6291                kinds,
6292                columns,
6293                table,
6294            } => {
6295                f.write_str("CREATE STATISTICS ")?;
6296                if *if_not_exists {
6297                    f.write_str("IF NOT EXISTS ")?;
6298                }
6299                write!(f, "{}", quote_ident(name))?;
6300                if !kinds.is_empty() {
6301                    write!(f, " ({})", kinds.join(", "))?;
6302                }
6303                write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
6304            }
6305            Self::DropStatistics { name, if_exists } => {
6306                f.write_str("DROP STATISTICS ")?;
6307                if *if_exists {
6308                    f.write_str("IF EXISTS ")?;
6309                }
6310                write!(f, "{}", quote_ident(name))
6311            }
6312            Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
6313            Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
6314            Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
6315            Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
6316            Self::DeclareCursor {
6317                name,
6318                scroll,
6319                hold,
6320                query,
6321            } => {
6322                write!(f, "DECLARE {} ", quote_ident(name))?;
6323                match scroll {
6324                    Some(true) => f.write_str("SCROLL ")?,
6325                    Some(false) => f.write_str("NO SCROLL ")?,
6326                    None => {}
6327                }
6328                f.write_str("CURSOR ")?;
6329                if *hold {
6330                    f.write_str("WITH HOLD ")?;
6331                }
6332                write!(f, "FOR {query}")
6333            }
6334            Self::FetchCursor { name, direction } => {
6335                write!(f, "FETCH {direction} FROM {}", quote_ident(name))
6336            }
6337            Self::MoveCursor { name, direction } => {
6338                write!(f, "MOVE {direction} FROM {}", quote_ident(name))
6339            }
6340            Self::CloseCursor { name } => match name {
6341                Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
6342                None => f.write_str("CLOSE ALL"),
6343            },
6344            Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
6345            Self::Notify { channel, payload } => {
6346                write!(f, "NOTIFY {}", quote_ident(channel))?;
6347                if let Some(p) = payload {
6348                    write!(f, ", '{}'", p.replace('\'', "''"))?;
6349                }
6350                Ok(())
6351            }
6352            Self::Unlisten(ch) => match ch {
6353                Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
6354                None => f.write_str("UNLISTEN *"),
6355            },
6356            Self::CopyTo {
6357                table,
6358                columns,
6359                query,
6360                options,
6361            } => {
6362                if let Some(q) = query {
6363                    write!(f, "COPY ({q})")?;
6364                } else {
6365                    write!(f, "COPY {table}")?;
6366                    if let Some(cols) = columns {
6367                        write!(f, " ({})", cols.join(", "))?;
6368                    }
6369                }
6370                write!(f, " TO STDOUT")?;
6371                let mut parts: Vec<String> = Vec::new();
6372                if options.format == CopyFormat::Csv {
6373                    parts.push("FORMAT csv".to_string());
6374                }
6375                if options.header {
6376                    parts.push("HEADER true".to_string());
6377                }
6378                if let Some(d) = options.delimiter {
6379                    parts.push(alloc::format!("DELIMITER '{d}'"));
6380                }
6381                if let Some(n) = &options.null_str {
6382                    parts.push(alloc::format!("NULL '{n}'"));
6383                }
6384                if let Some(q) = options.quote {
6385                    parts.push(alloc::format!("QUOTE '{q}'"));
6386                }
6387                if !parts.is_empty() {
6388                    write!(f, " WITH ({})", parts.join(", "))?;
6389                }
6390                Ok(())
6391            }
6392            Self::CopyFromFile {
6393                table,
6394                columns,
6395                path,
6396                options,
6397            } => {
6398                write!(f, "COPY {table}")?;
6399                if let Some(cols) = columns {
6400                    write!(f, " ({})", cols.join(", "))?;
6401                }
6402                write!(f, " FROM '{path}'")?;
6403                let mut parts: Vec<String> = Vec::new();
6404                if options.format == CopyFormat::Csv {
6405                    parts.push("FORMAT csv".to_string());
6406                }
6407                if options.header {
6408                    parts.push("HEADER true".to_string());
6409                }
6410                if let Some(d) = options.delimiter {
6411                    parts.push(alloc::format!("DELIMITER '{d}'"));
6412                }
6413                if let Some(n) = &options.null_str {
6414                    parts.push(alloc::format!("NULL '{n}'"));
6415                }
6416                if let Some(q) = options.quote {
6417                    parts.push(alloc::format!("QUOTE '{q}'"));
6418                }
6419                if !parts.is_empty() {
6420                    write!(f, " WITH ({})", parts.join(", "))?;
6421                }
6422                Ok(())
6423            }
6424            Self::CopyToFile {
6425                table,
6426                columns,
6427                query,
6428                path,
6429                options,
6430            } => {
6431                if let Some(q) = query {
6432                    write!(f, "COPY ({q})")?;
6433                } else {
6434                    write!(f, "COPY {table}")?;
6435                    if let Some(cols) = columns {
6436                        write!(f, " ({})", cols.join(", "))?;
6437                    }
6438                }
6439                write!(f, " TO '{path}'")?;
6440                let mut parts: Vec<String> = Vec::new();
6441                if options.format == CopyFormat::Csv {
6442                    parts.push("FORMAT csv".to_string());
6443                }
6444                if options.header {
6445                    parts.push("HEADER true".to_string());
6446                }
6447                if let Some(d) = options.delimiter {
6448                    parts.push(alloc::format!("DELIMITER '{d}'"));
6449                }
6450                if let Some(n) = &options.null_str {
6451                    parts.push(alloc::format!("NULL '{n}'"));
6452                }
6453                if let Some(q) = options.quote {
6454                    parts.push(alloc::format!("QUOTE '{q}'"));
6455                }
6456                if !parts.is_empty() {
6457                    write!(f, " WITH ({})", parts.join(", "))?;
6458                }
6459                Ok(())
6460            }
6461            Self::AlterDomain { name, action } => {
6462                write!(f, "ALTER DOMAIN {name} ")?;
6463                match action {
6464                    AlterDomainAction::AddConstraint { name: cn, check } => match cn {
6465                        Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
6466                        None => write!(f, "ADD CHECK ({check})"),
6467                    },
6468                    AlterDomainAction::DropConstraint {
6469                        name: cn,
6470                        if_exists,
6471                    } => {
6472                        if *if_exists {
6473                            write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
6474                        } else {
6475                            write!(f, "DROP CONSTRAINT {cn}")
6476                        }
6477                    }
6478                    AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
6479                    AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
6480                    AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
6481                    AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
6482                    AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
6483                }
6484            }
6485            Self::Truncate {
6486                tables,
6487                restart_identity,
6488                cascade,
6489                only,
6490            } => {
6491                f.write_str("TRUNCATE TABLE ")?;
6492                if *only {
6493                    f.write_str("ONLY ")?;
6494                }
6495                for (i, t) in tables.iter().enumerate() {
6496                    if i > 0 {
6497                        f.write_str(", ")?;
6498                    }
6499                    f.write_str(t)?;
6500                }
6501                if *restart_identity {
6502                    f.write_str(" RESTART IDENTITY")?;
6503                }
6504                if *cascade {
6505                    f.write_str(" CASCADE")?;
6506                }
6507                Ok(())
6508            }
6509            Self::DropTable { names, if_exists } => {
6510                f.write_str("DROP TABLE ")?;
6511                if *if_exists {
6512                    f.write_str("IF EXISTS ")?;
6513                }
6514                for (i, n) in names.iter().enumerate() {
6515                    if i > 0 {
6516                        f.write_str(", ")?;
6517                    }
6518                    write!(f, "{}", quote_ident(n))?;
6519                }
6520                Ok(())
6521            }
6522            Self::DropIndex {
6523                name,
6524                if_exists,
6525                table,
6526            } => {
6527                f.write_str("DROP INDEX ")?;
6528                if *if_exists {
6529                    f.write_str("IF EXISTS ")?;
6530                }
6531                write!(f, "{}", quote_ident(name))?;
6532                if let Some(t) = table {
6533                    write!(f, " ON {}", quote_ident(t))?;
6534                }
6535                Ok(())
6536            }
6537            Self::Select(s) => s.fmt(f),
6538            Self::CreateTable(s) => s.fmt(f),
6539            Self::CreateIndex(s) => s.fmt(f),
6540            Self::Insert(s) => s.fmt(f),
6541            Self::Update(s) => s.fmt(f),
6542            Self::Delete(s) => s.fmt(f),
6543            Self::Merge(s) => s.fmt(f),
6544            Self::Vacuum { table, analyze } => {
6545                f.write_str("VACUUM")?;
6546                if *analyze {
6547                    f.write_str(" ANALYZE")?;
6548                }
6549                if let Some(t) = table {
6550                    write!(f, " {}", quote_ident(t))?;
6551                }
6552                Ok(())
6553            }
6554            Self::Begin(modes) => {
6555                f.write_str("BEGIN")?;
6556                if let Some(level) = modes.isolation {
6557                    write!(f, " ISOLATION LEVEL {level}")?;
6558                }
6559                match modes.read_only {
6560                    Some(true) => f.write_str(" READ ONLY")?,
6561                    Some(false) => f.write_str(" READ WRITE")?,
6562                    None => {}
6563                }
6564                Ok(())
6565            }
6566            Self::Commit => f.write_str("COMMIT"),
6567            Self::Rollback => f.write_str("ROLLBACK"),
6568            Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
6569            Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
6570            Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
6571            Self::ShowTables => f.write_str("SHOW TABLES"),
6572            Self::ShowDatabases => f.write_str("SHOW DATABASES"),
6573            Self::UseDatabase(n) => write!(f, "USE {}", quote_ident(n)),
6574            Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
6575            Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
6576            Self::ShowStatus => f.write_str("SHOW STATUS"),
6577            Self::ShowVariables => f.write_str("SHOW VARIABLES"),
6578            Self::ShowVariablesLike(p) => {
6579                write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
6580            }
6581            Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
6582            Self::Discard(t) => write!(f, "DISCARD {t}"),
6583            Self::Kill { query_only, id } => {
6584                if *query_only {
6585                    write!(f, "KILL QUERY {id}")
6586                } else {
6587                    write!(f, "KILL CONNECTION {id}")
6588                }
6589            }
6590            Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
6591            Self::CreateUser(s) => write!(
6592                f,
6593                "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
6594                quote_ident(&s.name),
6595                s.role
6596            ),
6597            Self::DropUser { name, if_exists } => {
6598                let ie = if *if_exists { "IF EXISTS " } else { "" };
6599                write!(f, "DROP USER {ie}{}", quote_ident(name))
6600            }
6601            Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
6602            Self::SetRole(None) => f.write_str("RESET ROLE"),
6603            Self::Grant(g) => write!(f, "{}", g.render(true)),
6604            Self::Revoke(g) => write!(f, "{}", g.render(false)),
6605            Self::CreatePolicy(s) => {
6606                write!(
6607                    f,
6608                    "CREATE POLICY {} ON {}",
6609                    quote_ident(&s.name),
6610                    quote_ident(&s.table)
6611                )?;
6612                if !s.permissive {
6613                    f.write_str(" AS RESTRICTIVE")?;
6614                }
6615                if !matches!(s.cmd, PolicyCmd::All) {
6616                    let w = match s.cmd {
6617                        PolicyCmd::Select => "SELECT",
6618                        PolicyCmd::Insert => "INSERT",
6619                        PolicyCmd::Update => "UPDATE",
6620                        PolicyCmd::Delete => "DELETE",
6621                        PolicyCmd::All => unreachable!(),
6622                    };
6623                    write!(f, " FOR {w}")?;
6624                }
6625                if !s.roles.is_empty() {
6626                    write!(f, " TO {}", s.roles.join(", "))?;
6627                }
6628                if let Some(u) = &s.using {
6629                    write!(f, " USING ({u})")?;
6630                }
6631                if let Some(c) = &s.with_check {
6632                    write!(f, " WITH CHECK ({c})")?;
6633                }
6634                Ok(())
6635            }
6636            Self::AlterPolicy(s) => {
6637                write!(
6638                    f,
6639                    "ALTER POLICY {} ON {}",
6640                    quote_ident(&s.name),
6641                    quote_ident(&s.table)
6642                )?;
6643                if let Some(nn) = &s.rename_to {
6644                    return write!(f, " RENAME TO {}", quote_ident(nn));
6645                }
6646                if let Some(roles) = &s.roles {
6647                    write!(f, " TO {}", roles.join(", "))?;
6648                }
6649                if let Some(u) = &s.using {
6650                    write!(f, " USING ({u})")?;
6651                }
6652                if let Some(c) = &s.with_check {
6653                    write!(f, " WITH CHECK ({c})")?;
6654                }
6655                Ok(())
6656            }
6657            Self::DropPolicy(s) => {
6658                f.write_str("DROP POLICY ")?;
6659                if s.if_exists {
6660                    f.write_str("IF EXISTS ")?;
6661                }
6662                write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
6663            }
6664            Self::ShowUsers => f.write_str("SHOW USERS"),
6665            Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
6666            Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
6667            Self::CreateSubscription(s) => {
6668                write!(
6669                    f,
6670                    "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
6671                    quote_ident(&s.name),
6672                    s.conn_str.replace('\'', "''")
6673                )?;
6674                for (i, p) in s.publications.iter().enumerate() {
6675                    if i > 0 {
6676                        f.write_str(", ")?;
6677                    }
6678                    write!(f, "{}", quote_ident(p))?;
6679                }
6680                Ok(())
6681            }
6682            Self::DropSubscription { name, if_exists } => {
6683                let opt = if *if_exists { "IF EXISTS " } else { "" };
6684                write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
6685            }
6686            Self::WaitForWalPosition { pos, timeout_ms } => {
6687                write!(f, "WAIT FOR WAL POSITION {pos}")?;
6688                if let Some(ms) = timeout_ms {
6689                    write!(f, " WITH TIMEOUT {ms}")?;
6690                }
6691                Ok(())
6692            }
6693            Self::RenameTables(pairs) => {
6694                f.write_str("RENAME TABLE ")?;
6695                for (i, (from, to)) in pairs.iter().enumerate() {
6696                    if i > 0 {
6697                        f.write_str(", ")?;
6698                    }
6699                    write!(f, "{} TO {}", quote_ident(from), quote_ident(to))?;
6700                }
6701                Ok(())
6702            }
6703            Self::Analyze(None) => f.write_str("ANALYZE"),
6704            Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
6705            Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
6706            Self::Explain(e) => {
6707                if e.suggest {
6708                    write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
6709                } else if e.analyze {
6710                    write!(f, "EXPLAIN ANALYZE {}", e.inner)
6711                } else {
6712                    write!(f, "EXPLAIN {}", e.inner)
6713                }
6714            }
6715            Self::AlterIndex(a) => {
6716                write!(f, "ALTER INDEX ")?;
6717                match &a.target {
6718                    // Parameters are consumed, not stored; the shortest
6719                    // faithful spelling.
6720                    AlterIndexTarget::StorageParams => {
6721                        write!(f, "{} SET ()", quote_ident(&a.name))
6722                    }
6723                    AlterIndexTarget::Rebuild { encoding } => {
6724                        write!(f, "{} REBUILD", quote_ident(&a.name))?;
6725                        if let Some(enc) = encoding {
6726                            write!(f, " WITH (encoding = {enc})")?;
6727                        }
6728                        Ok(())
6729                    }
6730                    AlterIndexTarget::Rename { new, if_exists } => {
6731                        if *if_exists {
6732                            f.write_str("IF EXISTS ")?;
6733                        }
6734                        write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
6735                    }
6736                }
6737            }
6738            Self::AlterTable(a) => {
6739                write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
6740                for (i, t) in a.targets.iter().enumerate() {
6741                    if i > 0 {
6742                        f.write_str(", ")?;
6743                    }
6744                    fmt_alter_target(f, t)?;
6745                }
6746                Ok(())
6747            }
6748            Self::CreatePublication(p) => {
6749                write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
6750                match &p.scope {
6751                    PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
6752                    PublicationScope::ForTables(ts) => {
6753                        f.write_str(" FOR TABLE ")?;
6754                        for (i, t) in ts.iter().enumerate() {
6755                            if i > 0 {
6756                                f.write_str(", ")?;
6757                            }
6758                            write!(f, "{}", quote_ident(t))?;
6759                        }
6760                        Ok(())
6761                    }
6762                    PublicationScope::TablesInSchema(schema) => {
6763                        write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
6764                        Ok(())
6765                    }
6766                    PublicationScope::AllTablesExcept(ts) => {
6767                        f.write_str(" FOR ALL TABLES EXCEPT ")?;
6768                        for (i, t) in ts.iter().enumerate() {
6769                            if i > 0 {
6770                                f.write_str(", ")?;
6771                            }
6772                            write!(f, "{}", quote_ident(t))?;
6773                        }
6774                        Ok(())
6775                    }
6776                }
6777            }
6778            Self::CreateExtension(name) => {
6779                write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
6780            }
6781            Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
6782            Self::DropPublication { name, if_exists } => {
6783                let opt = if *if_exists { "IF EXISTS " } else { "" };
6784                write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
6785            }
6786            Self::SetParameter { name, value, local } => {
6787                write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
6788                match value {
6789                    SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
6790                    SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
6791                    SetValue::Default => f.write_str("DEFAULT"),
6792                    SetValue::Null => f.write_str("NULL"),
6793                }
6794            }
6795            Self::SetTransaction { modes } => {
6796                f.write_str("SET TRANSACTION")?;
6797                if let Some(isolation) = modes.isolation {
6798                    let name = match isolation {
6799                        IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
6800                        IsolationLevel::ReadCommitted => "READ COMMITTED",
6801                        IsolationLevel::RepeatableRead => "REPEATABLE READ",
6802                        IsolationLevel::Serializable => "SERIALIZABLE",
6803                    };
6804                    write!(f, " ISOLATION LEVEL {name}")?;
6805                }
6806                match modes.read_only {
6807                    Some(true) => f.write_str(" READ ONLY")?,
6808                    Some(false) => f.write_str(" READ WRITE")?,
6809                    None => {}
6810                }
6811                Ok(())
6812            }
6813            Self::ShowParameter(name) => write!(f, "SHOW {name}"),
6814            Self::SetUserVars(assigns, _) => {
6815                f.write_str("SET ")?;
6816                for (i, (name, value)) in assigns.iter().enumerate() {
6817                    if i > 0 {
6818                        f.write_str(", ")?;
6819                    }
6820                    write!(f, "@{name} = {value}")?;
6821                }
6822                Ok(())
6823            }
6824            Self::SetParameterList(pairs) => {
6825                f.write_str("SET ")?;
6826                for (i, (name, value)) in pairs.iter().enumerate() {
6827                    if i > 0 {
6828                        f.write_str(", ")?;
6829                    }
6830                    write!(f, "{name} = ")?;
6831                    match value {
6832                        SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
6833                        SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
6834                        SetValue::Default => f.write_str("DEFAULT")?,
6835                        SetValue::Null => f.write_str("NULL")?,
6836                    }
6837                }
6838                Ok(())
6839            }
6840            Self::ResetParameter(None) => f.write_str("RESET ALL"),
6841            Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
6842            Self::CreateFunction(s) => s.fmt(f),
6843            Self::CreateTrigger(s) => s.fmt(f),
6844            Self::DropTrigger {
6845                name,
6846                table,
6847                if_exists,
6848            } => {
6849                f.write_str("DROP TRIGGER ")?;
6850                if *if_exists {
6851                    f.write_str("IF EXISTS ")?;
6852                }
6853                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6854            }
6855            Self::DropFunction {
6856                name,
6857                args,
6858                if_exists,
6859            } => {
6860                f.write_str("DROP FUNCTION ")?;
6861                if *if_exists {
6862                    f.write_str("IF EXISTS ")?;
6863                }
6864                write!(f, "{}", quote_ident(name))?;
6865                if let Some(a) = args {
6866                    write!(f, "({})", a.join(", "))?;
6867                }
6868                Ok(())
6869            }
6870            Self::CreateSequence(s) => s.fmt(f),
6871            Self::AlterSequence(s) => s.fmt(f),
6872            Self::DropSequence { names, if_exists } => {
6873                f.write_str("DROP SEQUENCE ")?;
6874                if *if_exists {
6875                    f.write_str("IF EXISTS ")?;
6876                }
6877                for (i, n) in names.iter().enumerate() {
6878                    if i > 0 {
6879                        f.write_str(", ")?;
6880                    }
6881                    write!(f, "{}", quote_ident(n))?;
6882                }
6883                Ok(())
6884            }
6885            Self::CreateView(v) => v.fmt(f),
6886            Self::DropView { names, if_exists } => {
6887                f.write_str("DROP VIEW ")?;
6888                if *if_exists {
6889                    f.write_str("IF EXISTS ")?;
6890                }
6891                for (i, n) in names.iter().enumerate() {
6892                    if i > 0 {
6893                        f.write_str(", ")?;
6894                    }
6895                    write!(f, "{}", quote_ident(n))?;
6896                }
6897                Ok(())
6898            }
6899            Self::CreateMaterializedView(v) => v.fmt(f),
6900            Self::RefreshMaterializedView { name, with_data } => {
6901                write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6902                if !*with_data {
6903                    f.write_str(" WITH NO DATA")?;
6904                }
6905                Ok(())
6906            }
6907            Self::DropMaterializedView { names, if_exists } => {
6908                f.write_str("DROP MATERIALIZED VIEW ")?;
6909                if *if_exists {
6910                    f.write_str("IF EXISTS ")?;
6911                }
6912                for (i, n) in names.iter().enumerate() {
6913                    if i > 0 {
6914                        f.write_str(", ")?;
6915                    }
6916                    write!(f, "{}", quote_ident(n))?;
6917                }
6918                Ok(())
6919            }
6920            Self::CreateType(t) => t.fmt(f),
6921            Self::CommentOn {
6922                kind,
6923                name,
6924                comment,
6925            } => {
6926                let body = match comment {
6927                    Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6928                    None => "NULL".into(),
6929                };
6930                write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6931            }
6932            Self::AlterTypeRenameValue {
6933                type_name,
6934                old,
6935                new,
6936            } => write!(
6937                f,
6938                "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6939                quote_ident(type_name),
6940                old.replace('\'', "''"),
6941                new.replace('\'', "''")
6942            ),
6943            Self::AlterTypeAddValue {
6944                type_name,
6945                label,
6946                if_not_exists,
6947                position,
6948            } => {
6949                write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6950                if *if_not_exists {
6951                    write!(f, "IF NOT EXISTS ")?;
6952                }
6953                write!(f, "'{label}'")?;
6954                if let Some((is_before, anchor)) = position {
6955                    write!(
6956                        f,
6957                        " {} '{anchor}'",
6958                        if *is_before { "BEFORE" } else { "AFTER" }
6959                    )?;
6960                }
6961                Ok(())
6962            }
6963            Self::DropType { names, if_exists } => {
6964                f.write_str("DROP TYPE ")?;
6965                if *if_exists {
6966                    f.write_str("IF EXISTS ")?;
6967                }
6968                for (i, n) in names.iter().enumerate() {
6969                    if i > 0 {
6970                        f.write_str(", ")?;
6971                    }
6972                    write!(f, "{}", quote_ident(n))?;
6973                }
6974                Ok(())
6975            }
6976            Self::CreateDomain(d) => d.fmt(f),
6977            Self::DropDomain { names, if_exists } => {
6978                f.write_str("DROP DOMAIN ")?;
6979                if *if_exists {
6980                    f.write_str("IF EXISTS ")?;
6981                }
6982                for (i, n) in names.iter().enumerate() {
6983                    if i > 0 {
6984                        f.write_str(", ")?;
6985                    }
6986                    write!(f, "{}", quote_ident(n))?;
6987                }
6988                Ok(())
6989            }
6990            Self::CreateSchema {
6991                name,
6992                if_not_exists,
6993            } => {
6994                f.write_str("CREATE SCHEMA ")?;
6995                if *if_not_exists {
6996                    f.write_str("IF NOT EXISTS ")?;
6997                }
6998                write!(f, "{}", quote_ident(name))
6999            }
7000            Self::DropSchema { names, if_exists } => {
7001                f.write_str("DROP SCHEMA ")?;
7002                if *if_exists {
7003                    f.write_str("IF EXISTS ")?;
7004                }
7005                for (i, n) in names.iter().enumerate() {
7006                    if i > 0 {
7007                        f.write_str(", ")?;
7008                    }
7009                    write!(f, "{}", quote_ident(n))?;
7010                }
7011                Ok(())
7012            }
7013            Self::CreateRule(r) => {
7014                f.write_str("CREATE ")?;
7015                if r.or_replace {
7016                    f.write_str("OR REPLACE ")?;
7017                }
7018                write!(
7019                    f,
7020                    "RULE {} AS ON {} TO {}",
7021                    quote_ident(&r.name),
7022                    r.event,
7023                    quote_ident(&r.table)
7024                )?;
7025                if let Some(w) = &r.when_condition {
7026                    write!(f, " WHERE {w}")?;
7027                }
7028                f.write_str(if r.instead {
7029                    " DO INSTEAD "
7030                } else {
7031                    " DO ALSO "
7032                })?;
7033                if r.commands.is_empty() {
7034                    f.write_str("NOTHING")?;
7035                } else if r.commands.len() == 1 {
7036                    write!(f, "{}", r.commands[0])?;
7037                } else {
7038                    f.write_str("(")?;
7039                    for (i, c) in r.commands.iter().enumerate() {
7040                        if i > 0 {
7041                            f.write_str("; ")?;
7042                        }
7043                        write!(f, "{c}")?;
7044                    }
7045                    f.write_str(")")?;
7046                }
7047                Ok(())
7048            }
7049            Self::DropRule {
7050                name,
7051                table,
7052                if_exists,
7053            } => {
7054                f.write_str("DROP RULE ")?;
7055                if *if_exists {
7056                    f.write_str("IF EXISTS ")?;
7057                }
7058                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
7059            }
7060        }
7061    }
7062}
7063
7064impl fmt::Display for CreateDomainStatement {
7065    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7066        write!(
7067            f,
7068            "CREATE DOMAIN {} AS {}",
7069            quote_ident(&self.name),
7070            self.base_type
7071        )?;
7072        if let Some(d) = &self.default {
7073            write!(f, " DEFAULT {d}")?;
7074        }
7075        if self.not_null {
7076            f.write_str(" NOT NULL")?;
7077        }
7078        for c in &self.checks {
7079            write!(f, " CHECK ({c})")?;
7080        }
7081        Ok(())
7082    }
7083}
7084
7085impl fmt::Display for CreateTypeStatement {
7086    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7087        write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
7088        match &self.kind {
7089            TypeKind::Enum { labels } => {
7090                f.write_str("ENUM (")?;
7091                for (i, l) in labels.iter().enumerate() {
7092                    if i > 0 {
7093                        f.write_str(", ")?;
7094                    }
7095                    write!(f, "'{}'", l.replace('\'', "''"))?;
7096                }
7097                f.write_str(")")
7098            }
7099            TypeKind::Composite { fields, .. } => {
7100                f.write_str("(")?;
7101                for (i, (n, t)) in fields.iter().enumerate() {
7102                    if i > 0 {
7103                        f.write_str(", ")?;
7104                    }
7105                    write!(f, "{} {}", quote_ident(n), t)?;
7106                }
7107                f.write_str(")")
7108            }
7109        }
7110    }
7111}
7112
7113impl fmt::Display for CreateMaterializedViewStatement {
7114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7115        f.write_str("CREATE MATERIALIZED VIEW ")?;
7116        if self.if_not_exists {
7117            f.write_str("IF NOT EXISTS ")?;
7118        }
7119        write!(f, "{}", quote_ident(&self.name))?;
7120        if !self.columns.is_empty() {
7121            f.write_str(" (")?;
7122            for (i, c) in self.columns.iter().enumerate() {
7123                if i > 0 {
7124                    f.write_str(", ")?;
7125                }
7126                write!(f, "{}", quote_ident(c))?;
7127            }
7128            f.write_str(")")?;
7129        }
7130        write!(f, " AS {}", self.body)?;
7131        if !self.with_data {
7132            f.write_str(" WITH NO DATA")?;
7133        }
7134        Ok(())
7135    }
7136}
7137
7138impl fmt::Display for CreateViewStatement {
7139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7140        f.write_str("CREATE ")?;
7141        if self.or_replace {
7142            f.write_str("OR REPLACE ")?;
7143        }
7144        if self.temporary {
7145            f.write_str("TEMPORARY ")?;
7146        }
7147        f.write_str("VIEW ")?;
7148        if self.if_not_exists {
7149            f.write_str("IF NOT EXISTS ")?;
7150        }
7151        write!(f, "{}", quote_ident(&self.name))?;
7152        if !self.columns.is_empty() {
7153            f.write_str(" (")?;
7154            for (i, c) in self.columns.iter().enumerate() {
7155                if i > 0 {
7156                    f.write_str(", ")?;
7157                }
7158                write!(f, "{}", quote_ident(c))?;
7159            }
7160            f.write_str(")")?;
7161        }
7162        write!(f, " AS {}", self.body)?;
7163        match self.check_option {
7164            Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
7165            Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
7166            None => Ok(()),
7167        }
7168    }
7169}
7170
7171impl fmt::Display for CreateSequenceStatement {
7172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7173        f.write_str("CREATE ")?;
7174        if self.temporary {
7175            f.write_str("TEMPORARY ")?;
7176        }
7177        f.write_str("SEQUENCE ")?;
7178        if self.if_not_exists {
7179            f.write_str("IF NOT EXISTS ")?;
7180        }
7181        write!(f, "{}", quote_ident(&self.name))?;
7182        if let Some(dt) = self.data_type {
7183            write!(f, " AS {dt}")?;
7184        }
7185        write_sequence_options(f, &self.options)
7186    }
7187}
7188
7189impl fmt::Display for AlterSequenceStatement {
7190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7191        f.write_str("ALTER SEQUENCE ")?;
7192        if self.if_exists {
7193            f.write_str("IF EXISTS ")?;
7194        }
7195        write!(f, "{}", quote_ident(&self.name))?;
7196        write_sequence_options(f, &self.options)
7197    }
7198}
7199
7200impl fmt::Display for SequenceDataType {
7201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7202        f.write_str(match self {
7203            Self::SmallInt => "smallint",
7204            Self::Int => "integer",
7205            Self::BigInt => "bigint",
7206        })
7207    }
7208}
7209
7210fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
7211    if let Some(n) = o.increment {
7212        write!(f, " INCREMENT BY {n}")?;
7213    }
7214    match o.min_value {
7215        Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
7216        Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
7217        None => {}
7218    }
7219    match o.max_value {
7220        Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
7221        Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
7222        None => {}
7223    }
7224    if let Some(n) = o.start {
7225        write!(f, " START WITH {n}")?;
7226    }
7227    match o.restart {
7228        Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
7229        Some(None) => f.write_str(" RESTART")?,
7230        None => {}
7231    }
7232    if let Some(n) = o.cache {
7233        write!(f, " CACHE {n}")?;
7234    }
7235    match o.cycle {
7236        Some(true) => f.write_str(" CYCLE")?,
7237        Some(false) => f.write_str(" NO CYCLE")?,
7238        None => {}
7239    }
7240    if let Some(ob) = &o.owned_by {
7241        match ob {
7242            SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
7243            SequenceOwnedBy::Column { table, column } => {
7244                write!(
7245                    f,
7246                    " OWNED BY {}.{}",
7247                    quote_ident(table),
7248                    quote_ident(column)
7249                )?;
7250            }
7251        }
7252    }
7253    Ok(())
7254}
7255
7256impl fmt::Display for CreateFunctionStatement {
7257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7258        f.write_str("CREATE ")?;
7259        if self.or_replace {
7260            f.write_str("OR REPLACE ")?;
7261        }
7262        write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
7263        for (i, arg) in self.args.iter().enumerate() {
7264            if i > 0 {
7265                f.write_str(", ")?;
7266            }
7267            match arg.mode {
7268                FunctionArgMode::In => {}
7269                FunctionArgMode::Out => f.write_str("OUT ")?,
7270                FunctionArgMode::InOut => f.write_str("INOUT ")?,
7271            }
7272            if let Some(name) = &arg.name {
7273                write!(f, "{} ", quote_ident(name))?;
7274            }
7275            match &arg.ty {
7276                FunctionArgType::Typed(t) => write!(f, "{t}")?,
7277                FunctionArgType::Raw(s) => f.write_str(s)?,
7278            }
7279        }
7280        f.write_str(") RETURNS ")?;
7281        match &self.returns {
7282            FunctionReturn::Trigger => f.write_str("TRIGGER")?,
7283            FunctionReturn::Void => f.write_str("VOID")?,
7284            FunctionReturn::Type(t) => write!(f, "{t}")?,
7285            FunctionReturn::Other(s) => f.write_str(s)?,
7286        }
7287        write!(f, " LANGUAGE {} AS $$", self.language)?;
7288        match &self.body {
7289            FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
7290            FunctionBody::Raw(s) => f.write_str(s)?,
7291        }
7292        f.write_str("$$")
7293    }
7294}
7295
7296impl fmt::Display for PlPgSqlBlock {
7297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7298        if !self.declarations.is_empty() {
7299            f.write_str("DECLARE\n")?;
7300            for d in &self.declarations {
7301                write!(f, "  {} ", quote_ident(&d.name))?;
7302                match &d.ty {
7303                    FunctionArgType::Typed(t) => write!(f, "{t}")?,
7304                    FunctionArgType::Raw(s) => f.write_str(s)?,
7305                }
7306                if let Some(e) = &d.default {
7307                    write!(f, " := {e}")?;
7308                }
7309                f.write_str(";\n")?;
7310            }
7311        }
7312        f.write_str("BEGIN\n")?;
7313        for stmt in &self.statements {
7314            writeln!(f, "  {stmt};")?;
7315        }
7316        // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
7317        // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
7318        // parsed block through it — so every exception handler a function
7319        // declared was thrown away AT STORE TIME. The block executed fine while
7320        // it was still an AST (a DO block never round-trips through text), which
7321        // is why only functions and triggers lost theirs.
7322        if !self.exception_handlers.is_empty() {
7323            f.write_str("EXCEPTION\n")?;
7324            for h in &self.exception_handlers {
7325                writeln!(f, "  WHEN {} THEN", h.conditions.join(" OR "))?;
7326                for stmt in &h.body {
7327                    writeln!(f, "    {stmt};")?;
7328                }
7329            }
7330        }
7331        f.write_str("END")
7332    }
7333}
7334
7335impl fmt::Display for PlPgSqlStmt {
7336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7337        match self {
7338            Self::Assign { target, value } => write!(f, "{target} := {value}"),
7339            Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
7340            Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
7341            Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
7342            Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
7343            Self::Return(t) => match t {
7344                ReturnTarget::New => f.write_str("RETURN NEW"),
7345                ReturnTarget::Old => f.write_str("RETURN OLD"),
7346                ReturnTarget::Null => f.write_str("RETURN NULL"),
7347                ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
7348            },
7349            Self::If {
7350                branches,
7351                else_branch,
7352            } => {
7353                for (i, (cond, body)) in branches.iter().enumerate() {
7354                    if i == 0 {
7355                        write!(f, "IF {cond} THEN ")?;
7356                    } else {
7357                        write!(f, " ELSIF {cond} THEN ")?;
7358                    }
7359                    for (j, s) in body.iter().enumerate() {
7360                        if j > 0 {
7361                            f.write_str("; ")?;
7362                        }
7363                        write!(f, "{s}")?;
7364                    }
7365                }
7366                if !else_branch.is_empty() {
7367                    f.write_str(" ELSE ")?;
7368                    for (j, s) in else_branch.iter().enumerate() {
7369                        if j > 0 {
7370                            f.write_str("; ")?;
7371                        }
7372                        write!(f, "{s}")?;
7373                    }
7374                }
7375                f.write_str(" END IF")
7376            }
7377            Self::Raise {
7378                level,
7379                message,
7380                args,
7381            } => {
7382                let lvl = match level {
7383                    RaiseLevel::Notice => "NOTICE",
7384                    RaiseLevel::Warning => "WARNING",
7385                    RaiseLevel::Info => "INFO",
7386                    RaiseLevel::Log => "LOG",
7387                    RaiseLevel::Debug => "DEBUG",
7388                    RaiseLevel::Exception => "EXCEPTION",
7389                };
7390                write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
7391                for a in args {
7392                    write!(f, ", {a}")?;
7393                }
7394                Ok(())
7395            }
7396            Self::EmbeddedSql(s) => write!(f, "{s}"),
7397            Self::Assert { condition, message } => {
7398                write!(f, "ASSERT {condition}")?;
7399                if let Some(m) = message {
7400                    write!(f, ", {m}")?;
7401                }
7402                Ok(())
7403            }
7404            Self::While { condition, body } => {
7405                writeln!(f, "WHILE {condition} LOOP")?;
7406                for s in body {
7407                    writeln!(f, "  {s};")?;
7408                }
7409                f.write_str("END LOOP")
7410            }
7411            Self::ForRange {
7412                var,
7413                start,
7414                end,
7415                reverse,
7416                body,
7417            } => {
7418                write!(f, "FOR {var} IN ")?;
7419                if *reverse {
7420                    f.write_str("REVERSE ")?;
7421                }
7422                writeln!(f, "{start}..{end} LOOP")?;
7423                for s in body {
7424                    writeln!(f, "  {s};")?;
7425                }
7426                f.write_str("END LOOP")
7427            }
7428            Self::Loop { body } => {
7429                writeln!(f, "LOOP")?;
7430                for s in body {
7431                    writeln!(f, "  {s};")?;
7432                }
7433                f.write_str("END LOOP")
7434            }
7435            Self::Exit { when } => {
7436                f.write_str("EXIT")?;
7437                if let Some(c) = when {
7438                    write!(f, " WHEN {c}")?;
7439                }
7440                Ok(())
7441            }
7442            Self::Continue { when } => {
7443                f.write_str("CONTINUE")?;
7444                if let Some(c) = when {
7445                    write!(f, " WHEN {c}")?;
7446                }
7447                Ok(())
7448            }
7449            Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
7450            Self::ForQuery { var, query, body } => {
7451                writeln!(f, "FOR {var} IN ({query}) LOOP")?;
7452                for s in body {
7453                    writeln!(f, "  {s};")?;
7454                }
7455                f.write_str("END LOOP")
7456            }
7457            Self::ForExecute {
7458                var,
7459                sql_expr,
7460                body,
7461            } => {
7462                writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
7463                for s in body {
7464                    writeln!(f, "  {s};")?;
7465                }
7466                f.write_str("END LOOP")
7467            }
7468        }
7469    }
7470}
7471
7472impl fmt::Display for AssignTarget {
7473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7474        match self {
7475            Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
7476            Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
7477            Self::Local(n) => f.write_str(n),
7478        }
7479    }
7480}
7481
7482impl fmt::Display for CreateTriggerStatement {
7483    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7484        f.write_str("CREATE ")?;
7485        if self.or_replace {
7486            f.write_str("OR REPLACE ")?;
7487        }
7488        write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
7489        match self.timing {
7490            TriggerTiming::Before => f.write_str("BEFORE")?,
7491            TriggerTiming::After => f.write_str("AFTER")?,
7492            TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
7493        }
7494        for (i, e) in self.events.iter().enumerate() {
7495            if i == 0 {
7496                f.write_str(" ")?;
7497            } else {
7498                f.write_str(" OR ")?;
7499            }
7500            match e {
7501                TriggerEvent::Insert => f.write_str("INSERT")?,
7502                TriggerEvent::Update => {
7503                    f.write_str("UPDATE")?;
7504                    if !self.update_columns.is_empty() {
7505                        f.write_str(" OF ")?;
7506                        for (j, col) in self.update_columns.iter().enumerate() {
7507                            if j > 0 {
7508                                f.write_str(", ")?;
7509                            }
7510                            f.write_str(&quote_ident(col))?;
7511                        }
7512                    }
7513                }
7514                TriggerEvent::Delete => f.write_str("DELETE")?,
7515                TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
7516            }
7517        }
7518        write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
7519        match self.for_each {
7520            TriggerForEach::Row => f.write_str("ROW")?,
7521            TriggerForEach::Statement => f.write_str("STATEMENT")?,
7522        }
7523        write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
7524    }
7525}
7526
7527impl fmt::Display for CreateIndexStatement {
7528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7529        if self.is_unique {
7530            f.write_str("CREATE UNIQUE INDEX ")?;
7531        } else {
7532            f.write_str("CREATE INDEX ")?;
7533        }
7534        if self.if_not_exists {
7535            f.write_str("IF NOT EXISTS ")?;
7536        }
7537        write!(
7538            f,
7539            "{} ON {} ",
7540            quote_ident(&self.name),
7541            quote_ident(&self.table)
7542        )?;
7543        match self.method {
7544            IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
7545            IndexMethod::Brin => f.write_str("USING brin ")?,
7546            IndexMethod::Gin => f.write_str("USING gin ")?,
7547            IndexMethod::BTree => {}
7548        }
7549        if let Some(expr) = &self.expression {
7550            write!(f, "({})", expr)?;
7551        } else if self.extra_columns.is_empty() {
7552            // v7.15.0 — preserve operator class on round-trip
7553            // (`(col opclass)`) so WAL replay reconstructs the
7554            // engine-routing intent (e.g. `gin_trgm_ops` →
7555            // trigram-GIN build path).
7556            if let Some(op) = &self.opclass {
7557                write!(f, "({} {})", quote_ident(&self.column), op)?;
7558            } else {
7559                write!(f, "({})", quote_ident(&self.column))?;
7560            }
7561        } else {
7562            // v7.9.14 — multi-column key. Emit each column quoted
7563            // so the round-tripped form re-parses to identical AST.
7564            f.write_str("(")?;
7565            write!(f, "{}", quote_ident(&self.column))?;
7566            for c in &self.extra_columns {
7567                write!(f, ", {}", quote_ident(c))?;
7568            }
7569            f.write_str(")")?;
7570        }
7571        if !self.included_columns.is_empty() {
7572            f.write_str(" INCLUDE (")?;
7573            for (i, c) in self.included_columns.iter().enumerate() {
7574                if i > 0 {
7575                    f.write_str(", ")?;
7576                }
7577                write!(f, "{}", quote_ident(c))?;
7578            }
7579            f.write_str(")")?;
7580        }
7581        if let Some(pred) = &self.partial_predicate {
7582            write!(f, " WHERE {}", pred)?;
7583        }
7584        Ok(())
7585    }
7586}
7587
7588impl fmt::Display for CreateTableStatement {
7589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7590        f.write_str("CREATE TABLE ")?;
7591        if self.if_not_exists {
7592            f.write_str("IF NOT EXISTS ")?;
7593        }
7594        write!(f, "{}", quote_ident(&self.name))?;
7595        // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
7596        // no column list and no constraints; the table inherits its
7597        // columns from the parent at engine-DDL time.
7598        if let Some(spec) = &self.partition_of {
7599            write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
7600            return match &spec.bounds {
7601                PartitionOfBoundsAst::Range { lower, upper } => {
7602                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7603                }
7604                PartitionOfBoundsAst::List { values } => {
7605                    f.write_str("FOR VALUES IN (")?;
7606                    for (i, v) in values.iter().enumerate() {
7607                        if i > 0 {
7608                            f.write_str(", ")?;
7609                        }
7610                        write!(f, "{}", v)?;
7611                    }
7612                    f.write_str(")")
7613                }
7614                PartitionOfBoundsAst::Hash { modulus, remainder } => {
7615                    write!(
7616                        f,
7617                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7618                        modulus, remainder
7619                    )
7620                }
7621                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7622            };
7623        }
7624        f.write_str(" (")?;
7625        for (i, col) in self.columns.iter().enumerate() {
7626            if i > 0 {
7627                f.write_str(", ")?;
7628            }
7629            write!(f, "{col}")?;
7630        }
7631        // v7.6.0 — render FK constraints in table-level form, after
7632        // the column list. WAL replay round-trips through Display, so
7633        // every FK must serialise here for replay to reconstruct the
7634        // schema bit-for-bit.
7635        for fk in &self.foreign_keys {
7636            f.write_str(", ")?;
7637            write!(f, "{fk}")?;
7638        }
7639        // v7.13.0 — render table-level constraints (PRIMARY KEY /
7640        // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
7641        // column-level UNIQUE / CHECK get lifted to this list at
7642        // parse time, so emitting only here avoids double-counting.
7643        for tc in &self.table_constraints {
7644            f.write_str(", ")?;
7645            write!(f, "{tc}")?;
7646        }
7647        f.write_str(")")?;
7648        // v7.37.6-B — partition-parent suffix renders after the
7649        // closing column-list paren, before the optional MySQL
7650        // table-options tail (which Display doesn't currently emit).
7651        if let Some(spec) = &self.partition_by {
7652            f.write_str(" PARTITION BY ")?;
7653            match spec.kind {
7654                PartitionKindAst::Range => f.write_str("RANGE ")?,
7655                PartitionKindAst::List => f.write_str("LIST ")?,
7656                PartitionKindAst::Hash => f.write_str("HASH ")?,
7657            }
7658            f.write_str("(")?;
7659            for (i, col) in spec.key_columns.iter().enumerate() {
7660                if i > 0 {
7661                    f.write_str(", ")?;
7662                }
7663                f.write_str(&quote_ident(col))?;
7664            }
7665            f.write_str(")")?;
7666        }
7667        Ok(())
7668    }
7669}
7670
7671fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
7672    match t {
7673        AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
7674        AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
7675            write!(f, "REPLICA IDENTITY USING INDEX {index}")
7676        }
7677        AlterTableTarget::Inherit { parent, detach } => {
7678            if *detach {
7679                write!(f, "NO INHERIT {parent}")
7680            } else {
7681                write!(f, "INHERIT {parent}")
7682            }
7683        }
7684        AlterTableTarget::SetHotTierBytes(n) => {
7685            write!(f, "SET hot_tier_bytes = {n}")
7686        }
7687        AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
7688        AlterTableTarget::DropForeignKey { name, if_exists } => {
7689            f.write_str("DROP CONSTRAINT ")?;
7690            if *if_exists {
7691                f.write_str("IF EXISTS ")?;
7692            }
7693            write!(f, "{}", quote_ident(name))
7694        }
7695        AlterTableTarget::DropIndex { name, if_exists } => {
7696            f.write_str("DROP INDEX ")?;
7697            if *if_exists {
7698                f.write_str("IF EXISTS ")?;
7699            }
7700            write!(f, "{}", quote_ident(name))
7701        }
7702        AlterTableTarget::ModifyColumn {
7703            column,
7704            rename_to,
7705            definition,
7706            position,
7707        } => {
7708            if let Some(new) = rename_to {
7709                write!(
7710                    f,
7711                    "CHANGE COLUMN {} {} {}",
7712                    quote_ident(column),
7713                    quote_ident(new),
7714                    definition.ty
7715                )?;
7716            } else {
7717                write!(f, "MODIFY COLUMN {} {}", quote_ident(column), definition.ty)?;
7718            }
7719            if !definition.nullable {
7720                f.write_str(" NOT NULL")?;
7721            }
7722            write_column_position(f, position.as_ref())
7723        }
7724        AlterTableTarget::RenameIndex { old, new } => {
7725            write!(
7726                f,
7727                "RENAME INDEX {} TO {}",
7728                quote_ident(old),
7729                quote_ident(new)
7730            )
7731        }
7732        AlterTableTarget::SetTableAutoIncrement(n) => write!(f, "AUTO_INCREMENT = {n}"),
7733        AlterTableTarget::SetEngine(name) => write!(f, "ENGINE = {name}"),
7734        AlterTableTarget::ConvertToCharacterSet { charset, collate } => {
7735            write!(f, "CONVERT TO CHARACTER SET {charset}")?;
7736            if let Some(c) = collate {
7737                write!(f, " COLLATE {c}")?;
7738            }
7739            Ok(())
7740        }
7741        AlterTableTarget::AddColumn {
7742            column,
7743            if_not_exists,
7744            position,
7745        } => {
7746            f.write_str("ADD COLUMN ")?;
7747            if *if_not_exists {
7748                f.write_str("IF NOT EXISTS ")?;
7749            }
7750            write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
7751            if !column.nullable {
7752                f.write_str(" NOT NULL")?;
7753            }
7754            if let Some(d) = &column.default {
7755                write!(f, " DEFAULT {d}")?;
7756            }
7757            if column.auto_increment {
7758                f.write_str(" AUTO_INCREMENT")?;
7759            }
7760            if column.is_primary_key {
7761                f.write_str(" PRIMARY KEY")?;
7762            }
7763            Ok(())
7764        }
7765        AlterTableTarget::AlterColumnType {
7766            column,
7767            new_type,
7768            using,
7769            collation,
7770        } => {
7771            write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
7772            if let Some((_, name)) = collation {
7773                write!(f, " COLLATE {}", quote_ident(name))?;
7774            }
7775            if let Some(u) = using {
7776                write!(f, " USING {u}")?;
7777            }
7778            Ok(())
7779        }
7780        AlterTableTarget::DropColumn {
7781            column,
7782            if_exists,
7783            cascade,
7784        } => {
7785            f.write_str("DROP COLUMN ")?;
7786            if *if_exists {
7787                f.write_str("IF EXISTS ")?;
7788            }
7789            write!(f, "{}", quote_ident(column))?;
7790            if *cascade {
7791                f.write_str(" CASCADE")?;
7792            }
7793            Ok(())
7794        }
7795        AlterTableTarget::AddTableConstraint(tc) => {
7796            write!(f, "ADD {tc}")
7797        }
7798        AlterTableTarget::ValidateConstraint { name } => {
7799            write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
7800        }
7801        AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
7802        AlterTableTarget::ClusterOn { index } => match index {
7803            Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
7804            None => f.write_str("SET WITHOUT CLUSTER"),
7805        },
7806        AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
7807            // Round-trip-safe spelling: re-parsing this form lowers
7808            // back to SetColumnAutoIncrement (the nextval default is
7809            // how pg_dump says "serial").
7810            let seq = seq_name
7811                .clone()
7812                .unwrap_or_else(|| alloc::format!("{column}_seq"));
7813            write!(
7814                f,
7815                "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
7816                quote_ident(column)
7817            )
7818        }
7819        AlterTableTarget::RenameColumn { old, new } => {
7820            write!(
7821                f,
7822                "RENAME COLUMN {} TO {}",
7823                quote_ident(old),
7824                quote_ident(new)
7825            )
7826        }
7827        AlterTableTarget::RenameConstraint { old, new } => {
7828            write!(
7829                f,
7830                "RENAME CONSTRAINT {} TO {}",
7831                quote_ident(old),
7832                quote_ident(new)
7833            )
7834        }
7835        AlterTableTarget::RenameTable { new } => {
7836            write!(f, "RENAME TO {}", quote_ident(new))
7837        }
7838        AlterTableTarget::SetTriggerEnabled { which, enabled } => {
7839            f.write_str(if *enabled {
7840                "ENABLE TRIGGER "
7841            } else {
7842                "DISABLE TRIGGER "
7843            })?;
7844            match which {
7845                TriggerSelector::All => f.write_str("ALL"),
7846                TriggerSelector::Named(n) => f.write_str(&quote_ident(n)),
7847            }
7848        }
7849        AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
7850            (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
7851            (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
7852            (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
7853            (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
7854            (None, None) => Ok(()),
7855        },
7856        AlterTableTarget::AttachPartition { child, bounds } => {
7857            write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
7858            match bounds {
7859                PartitionOfBoundsAst::Range { lower, upper } => {
7860                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7861                }
7862                PartitionOfBoundsAst::List { values } => {
7863                    f.write_str("FOR VALUES IN (")?;
7864                    for (i, v) in values.iter().enumerate() {
7865                        if i > 0 {
7866                            f.write_str(", ")?;
7867                        }
7868                        write!(f, "{}", v)?;
7869                    }
7870                    f.write_str(")")
7871                }
7872                PartitionOfBoundsAst::Hash { modulus, remainder } => {
7873                    write!(
7874                        f,
7875                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7876                        modulus, remainder
7877                    )
7878                }
7879                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7880            }
7881        }
7882        AlterTableTarget::DetachPartition {
7883            child,
7884            concurrently,
7885            finalize,
7886        } => {
7887            write!(f, "DETACH PARTITION {}", quote_ident(child))?;
7888            if *concurrently {
7889                f.write_str(" CONCURRENTLY")?;
7890            }
7891            if *finalize {
7892                f.write_str(" FINALIZE")?;
7893            }
7894            Ok(())
7895        }
7896        AlterTableTarget::AlterColumnSetDefault {
7897            column,
7898            default_expr,
7899        } => write!(
7900            f,
7901            "ALTER COLUMN {} SET DEFAULT {}",
7902            quote_ident(column),
7903            default_expr
7904        ),
7905        AlterTableTarget::AlterColumnDropDefault { column } => {
7906            write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
7907        }
7908        AlterTableTarget::AlterColumnSetNotNull { column } => {
7909            write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
7910        }
7911        AlterTableTarget::AlterColumnDropNotNull { column } => {
7912            write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
7913        }
7914        AlterTableTarget::AlterColumnRestart { column, with } => {
7915            write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
7916            if let Some(n) = with {
7917                write!(f, " WITH {n}")?;
7918            }
7919            Ok(())
7920        }
7921        AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
7922            write!(
7923                f,
7924                "ALTER COLUMN {} DROP EXPRESSION{}",
7925                quote_ident(column),
7926                if *if_exists { " IF EXISTS" } else { "" }
7927            )
7928        }
7929        AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7930            write!(
7931                f,
7932                "ALTER COLUMN {} DROP IDENTITY{}",
7933                quote_ident(column),
7934                if *if_exists { " IF EXISTS" } else { "" }
7935            )
7936        }
7937        AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7938            write!(
7939                f,
7940                "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7941                quote_ident(column)
7942            )
7943        }
7944    }
7945}
7946
7947impl fmt::Display for TableConstraint {
7948    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7949        match self {
7950            Self::PrimaryKey { name, columns, .. } => {
7951                if let Some(n) = name {
7952                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7953                }
7954                f.write_str("PRIMARY KEY (")?;
7955                for (i, c) in columns.iter().enumerate() {
7956                    if i > 0 {
7957                        f.write_str(", ")?;
7958                    }
7959                    f.write_str(&quote_ident(c))?;
7960                }
7961                f.write_str(")")
7962            }
7963            Self::Unique {
7964                name,
7965                columns,
7966                nulls_not_distinct,
7967                ..
7968            } => {
7969                if let Some(n) = name {
7970                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7971                }
7972                f.write_str("UNIQUE ")?;
7973                if *nulls_not_distinct {
7974                    f.write_str("NULLS NOT DISTINCT ")?;
7975                }
7976                f.write_str("(")?;
7977                for (i, c) in columns.iter().enumerate() {
7978                    if i > 0 {
7979                        f.write_str(", ")?;
7980                    }
7981                    f.write_str(&quote_ident(c))?;
7982                }
7983                f.write_str(")")
7984            }
7985            Self::Check {
7986                name,
7987                expr,
7988                not_valid,
7989            } => {
7990                if let Some(n) = name {
7991                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7992                }
7993                write!(f, "CHECK ({expr})")?;
7994                if *not_valid {
7995                    write!(f, " NOT VALID")?;
7996                }
7997                Ok(())
7998            }
7999            Self::Index {
8000                name,
8001                columns,
8002                prefix_lengths,
8003            } => {
8004                f.write_str("KEY ")?;
8005                if let Some(n) = name {
8006                    write!(f, "{} ", quote_ident(n))?;
8007                }
8008                f.write_str("(")?;
8009                for (i, c) in columns.iter().enumerate() {
8010                    if i > 0 {
8011                        f.write_str(", ")?;
8012                    }
8013                    f.write_str(&quote_ident(c))?;
8014                    // v7.40.0 — the declared prefix rounds back with it.
8015                    if let Some(Some(p)) = prefix_lengths.get(i) {
8016                        write!(f, "({p})")?;
8017                    }
8018                }
8019                f.write_str(")")
8020            }
8021            Self::FulltextIndex { name, columns } => {
8022                // Mysqldump emits `FULLTEXT KEY name (cols)` —
8023                // Display rounds back to that shape so dump
8024                // replay reproduces the input verbatim.
8025                f.write_str("FULLTEXT KEY ")?;
8026                if let Some(n) = name {
8027                    write!(f, "{} ", quote_ident(n))?;
8028                }
8029                f.write_str("(")?;
8030                for (i, c) in columns.iter().enumerate() {
8031                    if i > 0 {
8032                        f.write_str(", ")?;
8033                    }
8034                    f.write_str(&quote_ident(c))?;
8035                }
8036                f.write_str(")")
8037            }
8038            Self::Exclude {
8039                name,
8040                method,
8041                elements,
8042            } => {
8043                if let Some(n) = name {
8044                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
8045                }
8046                f.write_str("EXCLUDE ")?;
8047                if let Some(m) = method {
8048                    write!(f, "USING {m} ")?;
8049                }
8050                f.write_str("(")?;
8051                for (i, (col, op)) in elements.iter().enumerate() {
8052                    if i > 0 {
8053                        f.write_str(", ")?;
8054                    }
8055                    write!(f, "{} WITH {op}", quote_ident(col))?;
8056                }
8057                f.write_str(")")
8058            }
8059        }
8060    }
8061}
8062
8063impl fmt::Display for ForeignKeyConstraint {
8064    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8065        if let Some(name) = &self.name {
8066            write!(f, "CONSTRAINT {} ", quote_ident(name))?;
8067        }
8068        f.write_str("FOREIGN KEY (")?;
8069        for (i, c) in self.columns.iter().enumerate() {
8070            if i > 0 {
8071                f.write_str(", ")?;
8072            }
8073            f.write_str(&quote_ident(c))?;
8074        }
8075        write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
8076        if !self.parent_columns.is_empty() {
8077            f.write_str(" (")?;
8078            for (i, c) in self.parent_columns.iter().enumerate() {
8079                if i > 0 {
8080                    f.write_str(", ")?;
8081                }
8082                f.write_str(&quote_ident(c))?;
8083            }
8084            f.write_str(")")?;
8085        }
8086        // Only render non-default actions to keep Display output
8087        // close to user input. SPG's default is RESTRICT (matches
8088        // SQL spec).
8089        if self.on_delete != FkAction::Restrict {
8090            write!(f, " ON DELETE {}", self.on_delete)?;
8091        }
8092        if self.on_update != FkAction::Restrict {
8093            write!(f, " ON UPDATE {}", self.on_update)?;
8094        }
8095        Ok(())
8096    }
8097}
8098
8099impl fmt::Display for FkAction {
8100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8101        match self {
8102            Self::Restrict => f.write_str("RESTRICT"),
8103            Self::Cascade => f.write_str("CASCADE"),
8104            Self::SetNull => f.write_str("SET NULL"),
8105            Self::SetDefault => f.write_str("SET DEFAULT"),
8106            Self::NoAction => f.write_str("NO ACTION"),
8107        }
8108    }
8109}
8110
8111impl fmt::Display for ColumnDef {
8112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8113        // v7.30.1 (mailrs round-24 class audit) — the type position
8114        // must re-parse to the same ColumnDef: a user-defined type
8115        // reference and the MySQL inline ENUM / SET value lists all
8116        // lower `ty` to Text, so rendering `ty` lost them.
8117        write!(f, "{}", quote_ident(&self.name))?;
8118        if let Some(ut) = &self.user_type_ref {
8119            write!(f, " {}", quote_ident(ut))?;
8120        } else if let Some(variants) = &self.inline_enum_variants {
8121            write_variant_list(f, "ENUM", variants)?;
8122        } else if let Some(variants) = &self.inline_set_variants {
8123            write_variant_list(f, "SET", variants)?;
8124        } else {
8125            write!(f, " {}", self.ty)?;
8126        }
8127        if self.is_unsigned {
8128            f.write_str(" UNSIGNED")?;
8129        }
8130        // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
8131        // DDL. Only emits when non-default so the typical output
8132        // stays unchanged.
8133        match self.collation {
8134            Collation::Binary => {}
8135            Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
8136        }
8137        if let Some(d) = &self.default {
8138            write!(f, " DEFAULT {d}")?;
8139        }
8140        if self.auto_increment {
8141            f.write_str(" AUTO_INCREMENT")?;
8142        }
8143        if !self.nullable {
8144            f.write_str(" NOT NULL")?;
8145        }
8146        // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
8147        // is NOT lifted to a table-level constraint at parse time
8148        // (unlike UNIQUE / CHECK), so the WAL round trip of a
8149        // prepared CREATE TABLE silently dropped the primary key.
8150        if self.is_primary_key {
8151            f.write_str(" PRIMARY KEY")?;
8152        }
8153        // The parser accepts only CURRENT_TIMESTAMP here (stored as
8154        // now()), so that spelling is the lossless round trip.
8155        if self.on_update_runtime.is_some() {
8156            f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
8157        }
8158        // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
8159        // replay reconstructs the computed-column declaration. The
8160        // expression sits inside a single set of parens; STORED is
8161        // the only variant the parser accepts.
8162        if let Some(gen_expr) = &self.generated_stored_expr {
8163            write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
8164        }
8165        Ok(())
8166    }
8167}
8168
8169/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
8170/// types (MySQL flavour; `ty` is Text underneath).
8171fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
8172    write!(f, " {kw}(")?;
8173    for (i, v) in variants.iter().enumerate() {
8174        if i > 0 {
8175            f.write_str(", ")?;
8176        }
8177        write!(f, "'{}'", v.replace('\'', "''"))?;
8178    }
8179    f.write_str(")")
8180}
8181
8182impl fmt::Display for InsertStatement {
8183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8184        write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
8185        if let Some(cols) = &self.columns {
8186            f.write_str(" (")?;
8187            for (i, c) in cols.iter().enumerate() {
8188                if i > 0 {
8189                    f.write_str(", ")?;
8190                }
8191                f.write_str(&quote_ident(c))?;
8192            }
8193            f.write_str(")")?;
8194        }
8195        // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
8196        // skipping the VALUES list (mailrs round-5 G4).
8197        if let Some(sel) = &self.select_source {
8198            write!(f, " {sel}")?;
8199        } else {
8200            f.write_str(" VALUES ")?;
8201            for (ri, row) in self.rows.iter().enumerate() {
8202                if ri > 0 {
8203                    f.write_str(", ")?;
8204                }
8205                f.write_str("(")?;
8206                for (i, v) in row.iter().enumerate() {
8207                    if i > 0 {
8208                        f.write_str(", ")?;
8209                    }
8210                    write!(f, "{v}")?;
8211                }
8212                f.write_str(")")?;
8213            }
8214        }
8215        // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
8216        // Display round trip: WAL persistence renders the bind-final
8217        // AST through this impl, and a replayed bare INSERT turns a
8218        // legal upsert no-op into a UNIQUE violation that refuses to
8219        // open the catalog.
8220        if let Some(oc) = &self.on_conflict {
8221            write!(f, " {oc}")?;
8222        }
8223        write_returning(self.returning.as_deref(), f)?;
8224        Ok(())
8225    }
8226}
8227
8228/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
8229/// parser produced, so the AST→SQL round trip preserves upsert
8230/// semantics (WAL replay depends on it).
8231impl fmt::Display for OnConflictClause {
8232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8233        f.write_str("ON CONFLICT")?;
8234        if let Some(name) = &self.constraint_name {
8235            write!(f, " ON CONSTRAINT {name}")?;
8236        }
8237        if !self.target_columns.is_empty() {
8238            f.write_str(" (")?;
8239            for (i, c) in self.target_columns.iter().enumerate() {
8240                if i > 0 {
8241                    f.write_str(", ")?;
8242                }
8243                f.write_str(&quote_ident(c))?;
8244            }
8245            f.write_str(")")?;
8246        }
8247        if let Some(w) = &self.index_where {
8248            write!(f, " WHERE {w}")?;
8249        }
8250        match &self.action {
8251            OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
8252            OnConflictAction::Update {
8253                assignments,
8254                where_,
8255            } => {
8256                f.write_str(" DO UPDATE SET ")?;
8257                for (i, (col, expr)) in assignments.iter().enumerate() {
8258                    if i > 0 {
8259                        f.write_str(", ")?;
8260                    }
8261                    write!(f, "{} = {expr}", quote_ident(col))?;
8262                }
8263                if let Some(w) = where_ {
8264                    write!(f, " WHERE {w}")?;
8265                }
8266                Ok(())
8267            }
8268        }
8269    }
8270}
8271
8272/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
8273/// tail for the three DML Display impls.
8274fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8275    let Some(items) = ret else {
8276        return Ok(());
8277    };
8278    f.write_str(" RETURNING ")?;
8279    for (i, item) in items.iter().enumerate() {
8280        if i > 0 {
8281            f.write_str(", ")?;
8282        }
8283        write!(f, "{item}")?;
8284    }
8285    Ok(())
8286}
8287
8288impl fmt::Display for UpdateStatement {
8289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8290        write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
8291        for (i, (col, expr)) in self.assignments.iter().enumerate() {
8292            if i > 0 {
8293                f.write_str(", ")?;
8294            }
8295            write!(f, "{} = {expr}", quote_ident(col))?;
8296        }
8297        if let Some(w) = &self.where_ {
8298            write!(f, " WHERE {w}")?;
8299        }
8300        // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
8301        if let Some(ol) = self.order_limit.as_deref() {
8302            if !ol.order_by.is_empty() {
8303                f.write_str(" ORDER BY ")?;
8304                for (i, o) in ol.order_by.iter().enumerate() {
8305                    if i > 0 {
8306                        f.write_str(", ")?;
8307                    }
8308                    write!(f, "{}", o.expr)?;
8309                    if o.desc {
8310                        f.write_str(" DESC")?;
8311                    }
8312                    match o.nulls_first {
8313                        Some(true) => f.write_str(" NULLS FIRST")?,
8314                        Some(false) => f.write_str(" NULLS LAST")?,
8315                        None => {}
8316                    }
8317                }
8318            }
8319            if let Some(n) = ol.limit {
8320                write!(f, " LIMIT {n}")?;
8321            }
8322        }
8323        write_returning(self.returning.as_deref(), f)?;
8324        Ok(())
8325    }
8326}
8327
8328impl fmt::Display for DeleteStatement {
8329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8330        write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
8331        if let Some(w) = &self.where_ {
8332            write!(f, " WHERE {w}")?;
8333        }
8334        write_returning(self.returning.as_deref(), f)?;
8335        Ok(())
8336    }
8337}
8338
8339impl fmt::Display for CteBody {
8340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8341        match self {
8342            Self::Select(s) => write!(f, "{s}"),
8343            Self::Insert(s) => write!(f, "{s}"),
8344            Self::Update(s) => write!(f, "{s}"),
8345            Self::Delete(s) => write!(f, "{s}"),
8346            Self::Merge(s) => write!(f, "{s}"),
8347        }
8348    }
8349}
8350
8351impl fmt::Display for MergeStatement {
8352    // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
8353    // (it round-trips for the cases tests cover, not for
8354    // round-tripping every edge of the surface).
8355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8356        fmt_with_clause(&self.ctes, f)?;
8357        f.write_str("MERGE INTO ")?;
8358        write!(f, "{}", quote_ident(&self.target))?;
8359        if let Some(a) = &self.target_alias {
8360            write!(f, " {}", quote_ident(a))?;
8361        }
8362        f.write_str(" USING ")?;
8363        if let Some(sub) = &self.source_select {
8364            write!(f, "({sub})")?;
8365        } else {
8366            write!(f, "{}", quote_ident(&self.source))?;
8367        }
8368        if let Some(a) = &self.source_alias {
8369            write!(f, " {}", quote_ident(a))?;
8370        }
8371        if !self.source_column_aliases.is_empty() {
8372            f.write_str("(")?;
8373            for (i, c) in self.source_column_aliases.iter().enumerate() {
8374                if i > 0 {
8375                    f.write_str(", ")?;
8376                }
8377                write!(f, "{}", quote_ident(c))?;
8378            }
8379            f.write_str(")")?;
8380        }
8381        write!(f, " ON {}", self.on)?;
8382        for clause in &self.clauses {
8383            f.write_str(" WHEN ")?;
8384            f.write_str(match clause.matched {
8385                MergeMatched::Matched => "MATCHED",
8386                MergeMatched::NotMatched => "NOT MATCHED",
8387                MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
8388            })?;
8389            if let Some(c) = &clause.condition {
8390                write!(f, " AND {c}")?;
8391            }
8392            f.write_str(" THEN ")?;
8393            match &clause.action {
8394                MergeAction::Insert { columns, values } => {
8395                    f.write_str("INSERT ")?;
8396                    // A column list is optional (round 146): the bare
8397                    // `INSERT VALUES (…)` form maps positionally.
8398                    if !columns.is_empty() {
8399                        f.write_str("(")?;
8400                        for (i, c) in columns.iter().enumerate() {
8401                            if i > 0 {
8402                                f.write_str(", ")?;
8403                            }
8404                            write!(f, "{}", quote_ident(c))?;
8405                        }
8406                        f.write_str(") ")?;
8407                    }
8408                    f.write_str("VALUES (")?;
8409                    for (i, v) in values.iter().enumerate() {
8410                        if i > 0 {
8411                            f.write_str(", ")?;
8412                        }
8413                        write!(f, "{v}")?;
8414                    }
8415                    f.write_str(")")?;
8416                }
8417                MergeAction::Update { assignments } => {
8418                    f.write_str("UPDATE SET ")?;
8419                    for (i, (c, e)) in assignments.iter().enumerate() {
8420                        if i > 0 {
8421                            f.write_str(", ")?;
8422                        }
8423                        write!(f, "{} = {e}", quote_ident(c))?;
8424                    }
8425                }
8426                MergeAction::Delete => f.write_str("DELETE")?,
8427                MergeAction::DoNothing => f.write_str("DO NOTHING")?,
8428            }
8429        }
8430        if let Some(items) = &self.returning {
8431            f.write_str(" RETURNING ")?;
8432            for (i, it) in items.iter().enumerate() {
8433                if i > 0 {
8434                    f.write_str(", ")?;
8435                }
8436                write!(f, "{it}")?;
8437            }
8438        }
8439        Ok(())
8440    }
8441}
8442
8443/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
8444/// carry a CTE list and must round-trip it identically.
8445fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
8446    if ctes.is_empty() {
8447        return Ok(());
8448    }
8449    f.write_str("WITH ")?;
8450    if ctes.iter().any(|c| c.recursive) {
8451        f.write_str("RECURSIVE ")?;
8452    }
8453    for (i, cte) in ctes.iter().enumerate() {
8454        if i > 0 {
8455            f.write_str(", ")?;
8456        }
8457        f.write_str(&quote_ident(&cte.name))?;
8458        if !cte.column_overrides.is_empty() {
8459            f.write_str(" (")?;
8460            for (ci, c) in cte.column_overrides.iter().enumerate() {
8461                if ci > 0 {
8462                    f.write_str(", ")?;
8463                }
8464                f.write_str(&quote_ident(c))?;
8465            }
8466            f.write_str(")")?;
8467        }
8468        write!(f, " AS ({})", cte.body)?;
8469    }
8470    f.write_str(" ")
8471}
8472
8473impl fmt::Display for SelectStatement {
8474    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8475        // v7.30.1 (mailrs round-24 class audit) — the WITH clause
8476        // must survive the round trip; a CTE-using statement
8477        // re-parsed without it references undefined tables.
8478        fmt_with_clause(&self.ctes, f)?;
8479        write_bare_select(self, f)?;
8480        for (kind, peer) in &self.unions {
8481            f.write_str(match kind {
8482                UnionKind::Distinct => " UNION ",
8483                UnionKind::All => " UNION ALL ",
8484                UnionKind::Intersect => " INTERSECT ",
8485                UnionKind::IntersectAll => " INTERSECT ALL ",
8486                UnionKind::Except => " EXCEPT ",
8487                UnionKind::ExceptAll => " EXCEPT ALL ",
8488            })?;
8489            write_bare_select(peer, f)?;
8490        }
8491        if !self.order_by.is_empty() {
8492            f.write_str(" ORDER BY ")?;
8493            for (i, o) in self.order_by.iter().enumerate() {
8494                if i > 0 {
8495                    f.write_str(", ")?;
8496                }
8497                write!(f, "{}", o.expr)?;
8498                if o.desc {
8499                    f.write_str(" DESC")?;
8500                }
8501                match o.nulls_first {
8502                    Some(true) => f.write_str(" NULLS FIRST")?,
8503                    Some(false) => f.write_str(" NULLS LAST")?,
8504                    None => {}
8505                }
8506            }
8507        }
8508        // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
8509        // exists in the FETCH FIRST spelling; rendering it as LIMIT
8510        // dropped the tie-extension semantics on replay. The parser
8511        // accepts OFFSET before FETCH, so keep that order here.
8512        if self.limit_with_ties {
8513            if let Some(o) = &self.offset {
8514                write!(f, " OFFSET {o}")?;
8515            }
8516            if let Some(n) = &self.limit {
8517                write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
8518            }
8519        } else {
8520            if let Some(n) = &self.limit {
8521                write!(f, " LIMIT {n}")?;
8522            }
8523            if let Some(o) = &self.offset {
8524                write!(f, " OFFSET {o}")?;
8525            }
8526        }
8527        Ok(())
8528    }
8529}
8530
8531fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8532    f.write_str("SELECT ")?;
8533    if s.distinct {
8534        f.write_str("DISTINCT ")?;
8535    }
8536    write_bare_select_body(s, f)
8537}
8538
8539fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8540    for (i, item) in s.items.iter().enumerate() {
8541        if i > 0 {
8542            f.write_str(", ")?;
8543        }
8544        write!(f, "{item}")?;
8545    }
8546    if let Some(t) = &s.from {
8547        write!(f, " FROM {t}")?;
8548    }
8549    if let Some(e) = &s.where_ {
8550        write!(f, " WHERE {e}")?;
8551    }
8552    if let Some(gs) = &s.group_by {
8553        f.write_str(" GROUP BY ")?;
8554        for (i, g) in gs.iter().enumerate() {
8555            if i > 0 {
8556                f.write_str(", ")?;
8557            }
8558            write!(f, "{g}")?;
8559        }
8560    } else if s.group_by_all {
8561        // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
8562        // shortcut parses to group_by: None + this flag; dropping
8563        // it turned an aggregate query into a bare projection on
8564        // re-parse.
8565        f.write_str(" GROUP BY ALL")?;
8566    }
8567    if let Some(h) = &s.having {
8568        write!(f, " HAVING {h}")?;
8569    }
8570    Ok(())
8571}
8572
8573impl fmt::Display for SelectItem {
8574    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8575        match self {
8576            Self::Wildcard => f.write_str("*"),
8577            Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
8578            Self::Expr { expr, alias } => {
8579                write!(f, "{expr}")?;
8580                if let Some(a) = alias {
8581                    write!(f, " AS {}", quote_ident(a))?;
8582                }
8583                Ok(())
8584            }
8585        }
8586    }
8587}
8588
8589impl fmt::Display for FromClause {
8590    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8591        write!(f, "{}", self.primary)?;
8592        for j in &self.joins {
8593            match j.kind {
8594                JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
8595                JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
8596                JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
8597                JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
8598                JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
8599                JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
8600            }
8601            if let Some(on) = &j.on {
8602                write!(f, " ON {on}")?;
8603            }
8604        }
8605        Ok(())
8606    }
8607}
8608
8609/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
8610/// for NESTED). Kept close to the parser's grammar so it re-parses.
8611fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
8612    for (i, c) in cols.iter().enumerate() {
8613        if i > 0 {
8614            f.write_str(", ")?;
8615        }
8616        match c {
8617            JsonTableColumn::Ordinality { name } => {
8618                write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
8619            }
8620            JsonTableColumn::Nested { path, columns } => {
8621                write!(f, "NESTED PATH '{path}' COLUMNS (")?;
8622                fmt_json_table_columns(f, columns)?;
8623                f.write_str(")")?;
8624            }
8625            JsonTableColumn::Regular {
8626                name,
8627                ty,
8628                path,
8629                exists,
8630                format_json,
8631                wrapper,
8632                on_empty,
8633                on_error,
8634            } => {
8635                write!(f, "{} {ty}", quote_ident(name))?;
8636                if *format_json {
8637                    f.write_str(" FORMAT JSON")?;
8638                }
8639                if *exists {
8640                    write!(f, " EXISTS PATH '{path}'")?;
8641                } else {
8642                    write!(f, " PATH '{path}'")?;
8643                }
8644                if *wrapper {
8645                    f.write_str(" WITH WRAPPER")?;
8646                }
8647                if let JsonTableOnBehavior::Error = on_empty {
8648                    f.write_str(" ERROR ON EMPTY")?;
8649                } else if let JsonTableOnBehavior::Default(e) = on_empty {
8650                    write!(f, " DEFAULT {e} ON EMPTY")?;
8651                }
8652                if let JsonTableOnBehavior::Error = on_error {
8653                    f.write_str(" ERROR ON ERROR")?;
8654                } else if let JsonTableOnBehavior::Default(e) = on_error {
8655                    write!(f, " DEFAULT {e} ON ERROR")?;
8656                }
8657            }
8658        }
8659    }
8660    Ok(())
8661}
8662
8663impl fmt::Display for TableRef {
8664    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8665        // v7.30.1 (mailrs round-24 class audit) — the dynamic
8666        // table-ref shapes must round-trip: rendering only the
8667        // (synthetic) name turned LATERAL / unnest() /
8668        // generate_series() into references to nonexistent tables
8669        // on re-parse.
8670        // v7.39 (round 205) — JSON_TABLE round-trips through Display
8671        // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
8672        if let Some(jt) = &self.json_table {
8673            write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
8674            if !jt.passing.is_empty() {
8675                f.write_str(" PASSING ")?;
8676                for (i, (n, e)) in jt.passing.iter().enumerate() {
8677                    if i > 0 {
8678                        f.write_str(", ")?;
8679                    }
8680                    write!(f, "{e} AS {}", quote_ident(n))?;
8681                }
8682            }
8683            f.write_str(" COLUMNS (")?;
8684            fmt_json_table_columns(f, &jt.columns)?;
8685            f.write_str(")")?;
8686            if let Some(a) = &self.alias {
8687                write!(f, " AS {}", quote_ident(a))?;
8688            }
8689            return Ok(());
8690        }
8691        if let Some(inner) = &self.lateral_subquery {
8692            write!(f, "LATERAL ({inner})")?;
8693            if let Some(a) = &self.alias {
8694                write!(f, " AS {}", quote_ident(a))?;
8695                // v7.37 D.28 — a derived table on the lateral_subquery channel
8696                // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
8697                // lowers here). Rendering the alias without the column list lost
8698                // the column names on re-parse (a view body round-trips through
8699                // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
8700                if !self.unnest_column_aliases.is_empty() {
8701                    f.write_str(" (")?;
8702                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8703                        if i > 0 {
8704                            f.write_str(", ")?;
8705                        }
8706                        f.write_str(&quote_ident(c))?;
8707                    }
8708                    f.write_str(")")?;
8709                }
8710            }
8711            return Ok(());
8712        }
8713        if let Some(expr) = &self.unnest_expr {
8714            write!(f, "UNNEST({expr})")?;
8715            if let Some(a) = &self.alias {
8716                write!(f, " AS {}", quote_ident(a))?;
8717                if !self.unnest_column_aliases.is_empty() {
8718                    f.write_str(" (")?;
8719                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8720                        if i > 0 {
8721                            f.write_str(", ")?;
8722                        }
8723                        f.write_str(&quote_ident(c))?;
8724                    }
8725                    f.write_str(")")?;
8726                }
8727            }
8728            return Ok(());
8729        }
8730        // 7.38.1 S5.1 — a FROM-position table function must re-render
8731        // as the CALL, not its bare name: ARRAY(subquery) desugars by
8732        // re-parsing the subquery's canonical text, and a dropped
8733        // argument list turned `pg_options_to_table(x)` into a
8734        // relation lookup that does not exist.
8735        if let Some(call) = &self.table_fn_call {
8736            let (fn_name, args) = call.as_ref();
8737            write!(f, "{fn_name}(")?;
8738            for (i, a) in args.iter().enumerate() {
8739                if i > 0 {
8740                    f.write_str(", ")?;
8741                }
8742                write!(f, "{a}")?;
8743            }
8744            f.write_str(")")?;
8745            if let Some(a) = &self.alias {
8746                write!(f, " AS {}", quote_ident(a))?;
8747                if !self.unnest_column_aliases.is_empty() {
8748                    f.write_str("(")?;
8749                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8750                        if i > 0 {
8751                            f.write_str(", ")?;
8752                        }
8753                        write!(f, "{}", quote_ident(c))?;
8754                    }
8755                    f.write_str(")")?;
8756                }
8757            }
8758            return Ok(());
8759        }
8760        if let Some(args) = &self.generate_series_args {
8761            f.write_str("generate_series(")?;
8762            for (i, a) in args.iter().enumerate() {
8763                if i > 0 {
8764                    f.write_str(", ")?;
8765                }
8766                write!(f, "{a}")?;
8767            }
8768            f.write_str(")")?;
8769            if let Some(a) = &self.alias {
8770                write!(f, " AS {}", quote_ident(a))?;
8771            }
8772            return Ok(());
8773        }
8774        write!(f, "{}", quote_ident(&self.name))?;
8775        if let Some(seg) = self.as_of_segment {
8776            write!(f, " AS OF SEGMENT {seg}")?;
8777        }
8778        if let Some(a) = &self.alias {
8779            write!(f, " AS {}", quote_ident(a))?;
8780        }
8781        Ok(())
8782    }
8783}
8784
8785impl fmt::Display for ColumnName {
8786    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8787        if let Some(q) = &self.qualifier {
8788            write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
8789        } else {
8790            write!(f, "{}", quote_ident(&self.name))
8791        }
8792    }
8793}
8794
8795/// v7.39 (round 311) — render the left spine of an AND / OR chain
8796/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
8797/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
8798/// SAME operator flattens; anything else is an ordinary operand.
8799fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
8800    if let Expr::Binary {
8801        lhs,
8802        op: inner,
8803        rhs,
8804    } = e
8805        && *inner == op
8806    {
8807        write_bool_chain(f, lhs, op)?;
8808        return write!(f, " {op} {rhs}");
8809    }
8810    write!(f, "{e}")
8811}
8812
8813/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
8814/// form `pg_get_constraintdef(oid, true)` and friends return.
8815///
8816/// The default [`fmt::Display`] parenthesises every operator node, which
8817/// is what PG's non-pretty deparse does and what makes the text
8818/// round-trip. Pretty drops the pairs the grammar can put back, and the
8819/// rule is NOT plain precedence minimisation — measured against PG 18.4
8820/// across 37 shapes:
8821///
8822///   * the boolean layer follows precedence (NOT > AND > OR): an OR
8823///     under an AND keeps its parens, an AND under an OR does not, and a
8824///     comparison under any of them does not (`NOT a > 1`);
8825///   * an associative chain flattens completely, even where the source
8826///     nested it to the right (`a AND (b AND c)` prints as one chain);
8827///   * but an operand of a comparison or arithmetic operator keeps its
8828///     parens whenever it is itself an operator expression — so
8829///     `(a + b) > 10` and `(- a) + b`, even though precedence alone
8830///     would not require either. A cast, function call, column or
8831///     literal in that position does not (`a::text = t`,
8832///     `length(code) > 2`); a cast counts as compound exactly when the
8833///     thing it casts is (`((a + b)::text) = t`).
8834///
8835/// Anything outside that layer defers to `Display`, which is never
8836/// wrong — only more parenthesised than PG would print.
8837#[must_use]
8838pub fn pretty_expr(e: &Expr) -> String {
8839    let mut out = String::new();
8840    write_pretty(&mut out, e, PrettyParent::None, false, false);
8841    out
8842}
8843
8844/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
8845/// writes it.
8846///
8847/// MariaDB names the offending expression in its out-of-range message
8848/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
8849/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
8850/// MySQL client, for a cast the client had just written the other way.
8851#[must_use]
8852pub fn pretty_expr_mysql(e: &Expr) -> String {
8853    let mut out = String::new();
8854    write_pretty(&mut out, e, PrettyParent::None, false, true);
8855    out
8856}
8857
8858/// v7.39 (round 505) — how strongly an expression suggests its own column
8859/// name. A cast keeps its argument's name only when that name is STRONG;
8860/// otherwise the cast reports the type it casts to.
8861///
8862/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
8863/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
8864/// itself `text` — so `case` and a function name cannot be the same kind of
8865/// answer, even though a bare `CASE …` does report `case`.
8866#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8867enum NameStrength {
8868    /// Nothing to go on — PG reports `?column?`.
8869    None,
8870    /// A name, but one a cast overrides: `case`, or a type name.
8871    Weak,
8872    /// A name a cast keeps: a column, or the function that produced it.
8873    Strong,
8874}
8875
8876/// v7.39 (round 505) — the column name PG18 gives a projected expression
8877/// that carries no `AS` alias. `None` means `?column?`.
8878///
8879/// SPG used to print the parsed expression back out, which matched neither
8880/// oracle and made name-keyed row access miss on both wires:
8881///
8882/// | query        | PG18       | SPG (before) |
8883/// |--------------|------------|--------------|
8884/// | `upper(s)`   | `upper`    | `upper(s)`   |
8885/// | `a+b`        | `?column?` | `(a + b)`    |
8886/// | `'lit'`      | `?column?` | `'lit'`      |
8887/// | `CASE …`     | `case`     | `CASE WHEN (a = 1) THEN …` |
8888///
8889/// Every rule below is one of those measurements, taken with `\gdesc`
8890/// against PG18: a call is named for its function, a cast recurses into its
8891/// argument and falls back to the type, a scalar subquery takes the name of
8892/// the column it selects, and operators have no name at all.
8893#[must_use]
8894pub fn figure_column_name(expr: &Expr) -> Option<String> {
8895    let (name, _) = figure_name_inner(expr);
8896    name
8897}
8898
8899/// The name a function reports, which is not always the name SPG parsed it
8900/// under: `count(*)` is held as `count_star` so the star arity survives the
8901/// AST, and that internal spelling must not reach a client. PG18 reports
8902/// `count`.
8903/// v7.39.13 — public, because Describe was naming the same call from a
8904/// second map that did not have this entry.
8905///
8906/// `count(*)` is held as `count_star` so the star arity survives the
8907/// AST. The projection mapped it back and the extended protocol's
8908/// Describe did not, so `SELECT count(*) OVER ()` answered `count` in
8909/// the row stream and `count_star` to `\gdesc` — an ORM-visible column
8910/// name, and two answers to one question. Reported by sentori against
8911/// 7.39.12.
8912#[must_use]
8913pub fn canonical_function_name(name: &str) -> String {
8914    match name {
8915        "count_star" => "count".to_string(),
8916        other => other.to_ascii_lowercase(),
8917    }
8918}
8919
8920/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
8921/// reports when its operand has none of its own. Only the spellings that
8922/// differ from what the user writes need an entry; everything else is
8923/// already its own typname.
8924fn cast_target_typname(target: &CastTarget) -> String {
8925    let written = target.to_string().to_ascii_lowercase();
8926    let base = written.strip_suffix("[]").unwrap_or(&written);
8927    let mapped = match base {
8928        "bigint" => "int8",
8929        "integer" | "int" => "int4",
8930        "smallint" => "int2",
8931        "boolean" => "bool",
8932        "double precision" => "float8",
8933        "real" => "float4",
8934        "character varying" => "varchar",
8935        "character" => "bpchar",
8936        "timestamp with time zone" => "timestamptz",
8937        "timestamp without time zone" => "timestamp",
8938        "time without time zone" => "time",
8939        "decimal" => "numeric",
8940        other => other,
8941    };
8942    if written.ends_with("[]") {
8943        alloc::format!("_{mapped}")
8944    } else {
8945        String::from(mapped)
8946    }
8947}
8948
8949fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8950    let strong = |n: String| (Some(n), NameStrength::Strong);
8951    match expr {
8952        // A column keeps its own name, qualifier and all discarded:
8953        // `lbl.a` reports `a`.
8954        Expr::Column(c) => strong(c.name.clone()),
8955        // Calls are named for the function. This covers the shapes that
8956        // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8957        // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8958        // because PG resolves them to functions before naming them.
8959        Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8960            strong(canonical_function_name(name))
8961        }
8962        Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8963        Expr::Extract { .. } => strong("extract".to_string()),
8964        Expr::Exists { .. } => strong("exists".to_string()),
8965        Expr::Array(_) => strong("array".to_string()),
8966        // `(expr).field` is named for the field, as a column would be.
8967        Expr::FieldAccess { field, .. } => strong(field.clone()),
8968        // v7.39.12 — PostgreSQL names a subscript after its operand, so
8969        // `arr[1]` is `arr`. There was no arm, so it fell through to
8970        // `?column?`. Reported by sentori against 7.39.11 — the same
8971        // naming defect v7.38.20 closed, reached through a different
8972        // expression. Weak, like the field access above it: an outer
8973        // cast or function still names the column.
8974        Expr::ArraySubscript { target, .. } => (figure_name_inner(target).0, NameStrength::Weak),
8975        // A cast prefers its argument's name and settles for the type:
8976        // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8977        Expr::Cast {
8978            expr: inner,
8979            target,
8980        } => match figure_name_inner(inner) {
8981            (Some(n), NameStrength::Strong) => strong(n),
8982            // v7.38.7 — the fallback is the target type's INTERNAL name,
8983            // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8984            // the `bigint` the user typed. Measured on PG18 alongside
8985            // `CAST(7 AS bigint)`, which answers `int8` too.
8986            _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8987        },
8988        // A scalar subquery reports whatever its single output column
8989        // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8990        Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8991        // `CASE …` names itself, but weakly — a cast around it wins.
8992        Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8993        // A literal that carries its own type names itself for that type:
8994        // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8995        // reports nothing. Weak, like any other type name.
8996        Expr::Literal(Literal::Interval { .. }) => {
8997            (Some("interval".to_string()), NameStrength::Weak)
8998        }
8999        // A wrapper that adds no name of its own.
9000        Expr::Variadic(inner) => figure_name_inner(inner),
9001        Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
9002        // Everything else — operators, comparisons, IS NULL, LIKE, IN,
9003        // literals, placeholders — reports `?column?`.
9004        _ => (None, NameStrength::None),
9005    }
9006}
9007
9008/// The name a scalar subquery's single projected column reports.
9009fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
9010    match sel.items.as_slice() {
9011        [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
9012        [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
9013        _ => (None, NameStrength::None),
9014    }
9015}
9016
9017/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
9018fn pretty_prec(e: &Expr) -> u8 {
9019    match e {
9020        Expr::Binary { op, .. } => match op {
9021            // v7.39 (round 407) — this deparse ladder mirrors the parser's:
9022            // OR < XOR < AND < NOT < comparison < additive < multiplicative.
9023            // XOR (MySQL-only) sits between OR and AND, so AND and everything
9024            // above shifted +1 to open rung 2 for it.
9025            BinOp::Or => 1,
9026            BinOp::LogicalXor => 2,
9027            BinOp::And => 3,
9028            BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
9029            BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
9030            // Everything else in this enum is a comparison-shaped
9031            // operator; they share one level, as in the grammar.
9032            _ => 5,
9033        },
9034        Expr::Unary { op, .. } => match op {
9035            UnOp::Not => 4,
9036            UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
9037        },
9038        _ => u8::MAX,
9039    }
9040}
9041
9042/// Is this node an operator expression — the thing an arithmetic or
9043/// comparison parent keeps parentheses around? A cast inherits the
9044/// answer from what it casts.
9045fn pretty_is_compound(e: &Expr) -> bool {
9046    match e {
9047        Expr::Binary { .. } | Expr::Unary { .. } => true,
9048        Expr::Cast { expr, .. } => pretty_is_compound(expr),
9049        _ => false,
9050    }
9051}
9052
9053/// `parent` describes the enclosing operator: its binding power, and
9054/// whether it is a comparison (which keeps parens around any operator
9055/// operand) or a NOT (which keeps them at equal power too).
9056#[derive(Clone, Copy, PartialEq)]
9057enum PrettyParent {
9058    /// Nothing encloses this node.
9059    None,
9060    /// A comparison-shaped operator: an operator operand always keeps
9061    /// its parens, whatever precedence would allow.
9062    Comparison,
9063    /// Arithmetic / concatenation: precedence decides.
9064    Arith(u8),
9065    /// A boolean connective: precedence decides.
9066    Bool(u8),
9067    /// `NOT`: precedence decides, but equal power still needs parens so
9068    /// `NOT (NOT a > 1)` does not collapse.
9069    Not,
9070}
9071
9072fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
9073    let prec = pretty_prec(e);
9074    let is_unary_sign = matches!(
9075        e,
9076        Expr::Unary {
9077            op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
9078            ..
9079        }
9080    );
9081    let needs = match parent {
9082        PrettyParent::None => false,
9083        PrettyParent::Comparison => pretty_is_compound(e),
9084        // A sign always keeps its parens under an operator — PG writes
9085        // `(- a) + b` even though precedence would not require it.
9086        PrettyParent::Arith(p) => {
9087            is_unary_sign
9088                || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
9089                    && (prec < p || (prec == p && is_rhs)))
9090        }
9091        PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
9092        PrettyParent::Not => {
9093            matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
9094        }
9095    };
9096    if needs {
9097        out.push('(');
9098    }
9099    match e {
9100        Expr::Binary { lhs, op, rhs } => {
9101            let child = match op {
9102                BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
9103                BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
9104                    PrettyParent::Arith(prec)
9105                }
9106                _ => PrettyParent::Comparison,
9107            };
9108            write_pretty(out, lhs, child, false, mysql);
9109            out.push(' ');
9110            out.push_str(&alloc::format!("{op}"));
9111            out.push(' ');
9112            // AND / OR are associative, so an explicitly right-nested
9113            // chain still prints as one chain.
9114            let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
9115            write_pretty(out, rhs, child, rhs_is_rhs, mysql);
9116        }
9117        Expr::Unary { op, expr } => match op {
9118            UnOp::Not => {
9119                out.push_str("NOT ");
9120                write_pretty(out, expr, PrettyParent::Not, false, mysql);
9121            }
9122            UnOp::Neg => {
9123                out.push_str("- ");
9124                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9125            }
9126            UnOp::Plus => {
9127                out.push_str("+ ");
9128                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9129            }
9130            UnOp::BitNot => {
9131                out.push('~');
9132                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9133            }
9134        },
9135        Expr::Cast { expr, target } => {
9136            if mysql {
9137                // MySQL's own spelling, which is what its error messages
9138                // quote back.
9139                out.push_str("cast(");
9140                write_pretty(out, expr, PrettyParent::None, false, mysql);
9141                out.push_str(&alloc::format!(
9142                    " as {})",
9143                    target.to_string().to_lowercase()
9144                ));
9145            } else {
9146                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9147                out.push_str(&alloc::format!("::{target}"));
9148            }
9149        }
9150        Expr::IsNull { expr, negated } => {
9151            write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
9152            out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
9153        }
9154        other => out.push_str(&alloc::format!("{other}")),
9155    }
9156    if needs {
9157        out.push(')');
9158    }
9159}
9160
9161const fn pretty_prec_not() -> u8 {
9162    // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
9163    // when the XOR insertion shifted the deparse ladder up by one).
9164    4
9165}
9166
9167impl fmt::Display for Expr {
9168    #[allow(clippy::too_many_lines)]
9169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9170        match self {
9171            Self::Literal(l) => write!(f, "{l}"),
9172            Self::Column(c) => write!(f, "{c}"),
9173            Self::Placeholder(n) => write!(f, "${n}"),
9174            // Round-trips as the spelling PG's docs lead with.
9175            Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
9176            Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
9177            // Round-trips with the name quoted, which is how PG spells a
9178            // collation everywhere: `"en_US.utf8"`, `"C"`.
9179            Self::Collate { expr, collation } => {
9180                write!(f, "{expr} COLLATE {}", quote_ident(collation))
9181            }
9182            // v7.39 (round 311) — an AND / OR chain that nests to the
9183            // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
9184            // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
9185            // its parentheses, because that is a different grouping as
9186            // written. Both halves measured against PG 18.4's deparse,
9187            // which flattens a same-operator left chain at parse time and
9188            // leaves `a AND (b AND c)` alone.
9189            Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
9190                f.write_str("(")?;
9191                write_bool_chain(f, lhs, *op)?;
9192                write!(f, " {op} {rhs}")?;
9193                f.write_str(")")
9194            }
9195            Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
9196            Self::Unary { op, expr } => match op {
9197                UnOp::Not => write!(f, "(NOT {expr})"),
9198                // A space after the sign, as PG's deparse writes it.
9199                UnOp::Neg => write!(f, "(- {expr})"),
9200                UnOp::Plus => write!(f, "(+ {expr})"),
9201                UnOp::BitNot => write!(f, "(~{expr})"),
9202            },
9203            // The OPERAND carries the parentheses, not the cast:
9204            // `(a)::text`, `((a + b))::text`. PG words it this way, and
9205            // it is what keeps `a::text = t` from reading as a cast of
9206            // the comparison.
9207            Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
9208            Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
9209            Self::AggregateOrdered {
9210                call,
9211                order_by,
9212                distinct,
9213                filter,
9214            } => {
9215                let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
9216                    for (i, o) in order_by.iter().enumerate() {
9217                        if i > 0 {
9218                            f.write_str(", ")?;
9219                        }
9220                        write!(f, "{}", o.expr)?;
9221                        if o.desc {
9222                            f.write_str(" DESC")?;
9223                        }
9224                        match o.nulls_first {
9225                            Some(true) => f.write_str(" NULLS FIRST")?,
9226                            Some(false) => f.write_str(" NULLS LAST")?,
9227                            None => {}
9228                        }
9229                    }
9230                    Ok(())
9231                };
9232                // Ordered-set aggregates (`percentile_cont(f) WITHIN
9233                // GROUP (ORDER BY x)`) render the in-parens args as the
9234                // direct argument and the sort spec under WITHIN GROUP —
9235                // not as an in-argument ORDER BY.
9236                let ordered_set = matches!(
9237                    call.as_ref(),
9238                    Expr::FunctionCall { name, .. }
9239                        if matches!(
9240                            name.to_ascii_lowercase().as_str(),
9241                            "percentile_cont" | "percentile_disc" | "mode"
9242                        )
9243                );
9244                if ordered_set {
9245                    write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
9246                    fmt_order_by(f)?;
9247                    f.write_str(")")?;
9248                } else {
9249                    // `name([DISTINCT ]args [ORDER BY …])` — peel the
9250                    // inner call's parens to splice modifiers.
9251                    let inner = alloc::format!("{call}");
9252                    let body = inner.strip_suffix(')').unwrap_or(&inner);
9253                    let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
9254                    write!(f, "{head}(")?;
9255                    if *distinct {
9256                        f.write_str("DISTINCT ")?;
9257                    }
9258                    write!(f, "{args_part}")?;
9259                    if !order_by.is_empty() {
9260                        f.write_str(" ORDER BY ")?;
9261                        fmt_order_by(f)?;
9262                    }
9263                    f.write_str(")")?;
9264                }
9265                if let Some(cond) = filter {
9266                    write!(f, " FILTER (WHERE {cond})")?;
9267                }
9268                Ok(())
9269            }
9270            Self::IsNull { expr, negated } => {
9271                if *negated {
9272                    write!(f, "({expr} IS NOT NULL)")
9273                } else {
9274                    write!(f, "({expr} IS NULL)")
9275                }
9276            }
9277            Self::BoolTest {
9278                expr,
9279                value,
9280                negated,
9281            } => {
9282                let word = match value {
9283                    Some(true) => "TRUE",
9284                    Some(false) => "FALSE",
9285                    None => "UNKNOWN",
9286                };
9287                if *negated {
9288                    write!(f, "({expr} IS NOT {word})")
9289                } else {
9290                    write!(f, "({expr} IS {word})")
9291                }
9292            }
9293            Self::FunctionCall { name, args } => {
9294                write!(f, "{name}(")?;
9295                for (i, a) in args.iter().enumerate() {
9296                    if i > 0 {
9297                        f.write_str(", ")?;
9298                    }
9299                    write!(f, "{a}")?;
9300                }
9301                f.write_str(")")
9302            }
9303            Self::Like {
9304                expr,
9305                pattern,
9306                negated,
9307                case_insensitive,
9308            } => {
9309                let op = match (negated, case_insensitive) {
9310                    (false, false) => "LIKE",
9311                    (true, false) => "NOT LIKE",
9312                    (false, true) => "ILIKE",
9313                    (true, true) => "NOT ILIKE",
9314                };
9315                write!(f, "({expr} {op} {pattern})")
9316            }
9317            Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
9318            Self::WindowFunction {
9319                name,
9320                args,
9321                partition_by,
9322                order_by,
9323                frame,
9324                null_treatment,
9325                filter,
9326            } => {
9327                write!(f, "{name}(")?;
9328                for (i, a) in args.iter().enumerate() {
9329                    if i > 0 {
9330                        f.write_str(", ")?;
9331                    }
9332                    write!(f, "{a}")?;
9333                }
9334                f.write_str(")")?;
9335                // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
9336                // OVER; it round-trips so a window body's Display re-parses.
9337                if let Some(cond) = filter {
9338                    write!(f, " FILTER (WHERE {cond})")?;
9339                }
9340                // v7.30.1 (mailrs round-24 class audit) — IGNORE
9341                // NULLS sits between the arg list and OVER; dropping
9342                // it reverted replayed queries to RESPECT NULLS.
9343                if matches!(null_treatment, NullTreatment::Ignore) {
9344                    f.write_str(" IGNORE NULLS")?;
9345                }
9346                f.write_str(" OVER (")?;
9347                if !partition_by.is_empty() {
9348                    f.write_str("PARTITION BY ")?;
9349                    for (i, p) in partition_by.iter().enumerate() {
9350                        if i > 0 {
9351                            f.write_str(", ")?;
9352                        }
9353                        write!(f, "{p}")?;
9354                    }
9355                }
9356                if !order_by.is_empty() {
9357                    if !partition_by.is_empty() {
9358                        f.write_str(" ")?;
9359                    }
9360                    f.write_str("ORDER BY ")?;
9361                    for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
9362                        if i > 0 {
9363                            f.write_str(", ")?;
9364                        }
9365                        write!(f, "{e}")?;
9366                        if *desc {
9367                            f.write_str(" DESC")?;
9368                        }
9369                        match nulls_first {
9370                            Some(true) => f.write_str(" NULLS FIRST")?,
9371                            Some(false) => f.write_str(" NULLS LAST")?,
9372                            None => {}
9373                        }
9374                    }
9375                }
9376                if let Some(fr) = frame {
9377                    if !partition_by.is_empty() || !order_by.is_empty() {
9378                        f.write_str(" ")?;
9379                    }
9380                    let k = match fr.kind {
9381                        FrameKind::Rows => "ROWS",
9382                        FrameKind::Range => "RANGE",
9383                        FrameKind::Groups => "GROUPS",
9384                    };
9385                    if let Some(end) = &fr.end {
9386                        write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
9387                    } else {
9388                        write!(f, "{k} {}", fr.start)?;
9389                    }
9390                }
9391                f.write_str(")")
9392            }
9393            Self::ScalarSubquery(s) => write!(f, "({s})"),
9394            Self::Exists { subquery, negated } => {
9395                if *negated {
9396                    write!(f, "NOT EXISTS ({subquery})")
9397                } else {
9398                    write!(f, "EXISTS ({subquery})")
9399                }
9400            }
9401            Self::InSubquery {
9402                expr,
9403                subquery,
9404                negated,
9405            } => {
9406                if *negated {
9407                    write!(f, "({expr} NOT IN ({subquery}))")
9408                } else {
9409                    write!(f, "({expr} IN ({subquery}))")
9410                }
9411            }
9412            Self::RowInSubquery {
9413                row,
9414                subquery,
9415                negated,
9416            } => {
9417                write!(f, "(")?;
9418                for (i, e) in row.iter().enumerate() {
9419                    if i > 0 {
9420                        write!(f, ", ")?;
9421                    }
9422                    write!(f, "{e}")?;
9423                }
9424                let kw = if *negated { ") NOT IN (" } else { ") IN (" };
9425                write!(f, "{kw}{subquery})")
9426            }
9427            Self::RowCmpSubquery { row, op, subquery } => {
9428                write!(f, "(")?;
9429                for (i, e) in row.iter().enumerate() {
9430                    if i > 0 {
9431                        write!(f, ", ")?;
9432                    }
9433                    write!(f, "{e}")?;
9434                }
9435                write!(f, ") {op} ({subquery})")
9436            }
9437            Self::InList {
9438                expr,
9439                list,
9440                negated,
9441            } => {
9442                let kw = if *negated { " NOT IN (" } else { " IN (" };
9443                write!(f, "({expr}{kw}")?;
9444                for (i, e) in list.iter().enumerate() {
9445                    if i > 0 {
9446                        f.write_str(", ")?;
9447                    }
9448                    write!(f, "{e}")?;
9449                }
9450                f.write_str("))")
9451            }
9452            Self::Array(items) => {
9453                f.write_str("ARRAY[")?;
9454                for (i, e) in items.iter().enumerate() {
9455                    if i > 0 {
9456                        f.write_str(", ")?;
9457                    }
9458                    write!(f, "{e}")?;
9459                }
9460                f.write_str("]")
9461            }
9462            Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
9463            Self::ArraySlice { target, lo, hi } => {
9464                write!(f, "({target}[")?;
9465                if let Some(l) = lo {
9466                    write!(f, "{l}")?;
9467                }
9468                write!(f, ":")?;
9469                if let Some(h) = hi {
9470                    write!(f, "{h}")?;
9471                }
9472                write!(f, "])")
9473            }
9474            Self::AnyAll {
9475                expr,
9476                op,
9477                array,
9478                is_any,
9479            } => {
9480                let kw = if *is_any { "ANY" } else { "ALL" };
9481                write!(f, "({expr} {op} {kw}({array}))")
9482            }
9483            Self::Case {
9484                operand,
9485                branches,
9486                else_branch,
9487            } => {
9488                f.write_str("CASE")?;
9489                if let Some(op) = operand {
9490                    write!(f, " {op}")?;
9491                }
9492                for (w, t) in branches {
9493                    write!(f, " WHEN {w} THEN {t}")?;
9494                }
9495                if let Some(e) = else_branch {
9496                    write!(f, " ELSE {e}")?;
9497                }
9498                f.write_str(" END")
9499            }
9500        }
9501    }
9502}
9503
9504/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
9505/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
9506pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
9507    use alloc::string::ToString;
9508    if scale == 0 {
9509        return alloc::format!("{unscaled}");
9510    }
9511    let neg = unscaled < 0;
9512    let digits = alloc::format!("{}", unscaled.unsigned_abs());
9513    let scale = scale as usize;
9514    let (int_part, frac_part) = if digits.len() > scale {
9515        (
9516            digits[..digits.len() - scale].to_string(),
9517            digits[digits.len() - scale..].to_string(),
9518        )
9519    } else {
9520        ("0".to_string(), alloc::format!("{digits:0>scale$}"))
9521    };
9522    alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
9523}
9524
9525/// A single-quoted SQL string, with an embedded quote doubled.
9526fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
9527    f.write_str("'")?;
9528    for c in s.chars() {
9529        if c == '\'' {
9530            f.write_str("''")?;
9531        } else {
9532            write!(f, "{c}")?;
9533        }
9534    }
9535    f.write_str("'")
9536}
9537
9538impl fmt::Display for Literal {
9539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9540        match self {
9541            Self::Integer(n) => write!(f, "{n}"),
9542            Self::Float(x) => {
9543                let s = format!("{x}");
9544                // Default Display for an integral f64 (e.g. 1.0) emits "1",
9545                // which would round-trip back to Integer. Force a dot.
9546                if s.contains('.') || s.contains('e') || s.contains('E') {
9547                    f.write_str(&s)
9548                } else {
9549                    write!(f, "{s}.0")
9550                }
9551            }
9552            Self::Numeric { unscaled, scale } => {
9553                // Render the exact decimal `unscaled / 10^scale`, preserving
9554                // scale (trailing zeros) — round-trips to the same literal.
9555                f.write_str(&render_exact_decimal(*unscaled, *scale))
9556            }
9557            Self::NumericBig(s) => f.write_str(s),
9558            // Printed exactly as the text form was, so a reader cannot
9559            // tell whether the constant was decoded or not.
9560            Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
9561            Self::String(s) => write_quoted(f, s),
9562            Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
9563            Self::Null => f.write_str("NULL"),
9564            // PG external array form. Display round-trip re-enters
9565            // through the column-typed text coerce, same as pgwire.
9566            Self::TextArray(items) => {
9567                f.write_str("'{")?;
9568                for (i, it) in items.iter().enumerate() {
9569                    if i > 0 {
9570                        f.write_str(",")?;
9571                    }
9572                    match it {
9573                        None => f.write_str("NULL")?,
9574                        Some(s) => {
9575                            f.write_str("\"")?;
9576                            for c in s.chars() {
9577                                match c {
9578                                    // array-element escapes
9579                                    '"' | '\\' => write!(f, "\\{c}")?,
9580                                    // the OUTER wrapper is a SQL string
9581                                    // literal — embedded quotes must
9582                                    // double, or the rendered form
9583                                    // (WAL replay parses it back) is
9584                                    // invalid SQL
9585                                    '\'' => f.write_str("''")?,
9586                                    _ => write!(f, "{c}")?,
9587                                }
9588                            }
9589                            f.write_str("\"")?;
9590                        }
9591                    }
9592                }
9593                f.write_str("}'")
9594            }
9595            Self::IntArray(items) => {
9596                f.write_str("'{")?;
9597                for (i, it) in items.iter().enumerate() {
9598                    if i > 0 {
9599                        f.write_str(",")?;
9600                    }
9601                    match it {
9602                        None => f.write_str("NULL")?,
9603                        Some(n) => write!(f, "{n}")?,
9604                    }
9605                }
9606                f.write_str("}'")
9607            }
9608            Self::BigIntArray(items) => {
9609                f.write_str("'{")?;
9610                for (i, it) in items.iter().enumerate() {
9611                    if i > 0 {
9612                        f.write_str(",")?;
9613                    }
9614                    match it {
9615                        None => f.write_str("NULL")?,
9616                        Some(n) => write!(f, "{n}")?,
9617                    }
9618                }
9619                f.write_str("}'")
9620            }
9621            Self::Vector(v) => {
9622                f.write_str("[")?;
9623                for (i, x) in v.iter().enumerate() {
9624                    if i > 0 {
9625                        f.write_str(", ")?;
9626                    }
9627                    let s = format!("{x}");
9628                    // Mirror Float Display: force a dot so re-parse stays
9629                    // numerically literal.
9630                    if s.contains('.') || s.contains('e') || s.contains('E') {
9631                        f.write_str(&s)?;
9632                    } else {
9633                        write!(f, "{s}.0")?;
9634                    }
9635                }
9636                f.write_str("]")
9637            }
9638            Self::Interval { text, .. } => {
9639                f.write_str("INTERVAL '")?;
9640                for c in text.chars() {
9641                    if c == '\'' {
9642                        f.write_str("''")?;
9643                    } else {
9644                        write!(f, "{c}")?;
9645                    }
9646                }
9647                f.write_str("'")
9648            }
9649        }
9650    }
9651}
9652
9653impl fmt::Display for BinOp {
9654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9655        f.write_str(match self {
9656            Self::Or => "OR",
9657            Self::And => "AND",
9658            Self::Eq => "=",
9659            Self::NotEq => "<>",
9660            Self::IsDistinctFrom => "IS DISTINCT FROM",
9661            Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
9662            Self::IntDiv => "DIV",
9663            Self::Lt => "<",
9664            Self::LtEq => "<=",
9665            Self::Gt => ">",
9666            Self::GtEq => ">=",
9667            Self::Add => "+",
9668            Self::Sub => "-",
9669            Self::Mul => "*",
9670            Self::Div => "/",
9671            Self::Mod => "%",
9672            Self::L2Distance => "<->",
9673            Self::GeomParallel => "?||",
9674            Self::OverLeft => "&<",
9675            Self::OverRight => "&>",
9676            Self::GeomPerp => "?-|",
9677            Self::GeomSameAs => "~=",
9678            Self::ClosestPoint => "##",
9679            Self::GeomHoriz => "?-",
9680            Self::InnerProduct => "<#>",
9681            Self::CosineDistance => "<=>",
9682            Self::Concat => "||",
9683            Self::BitOr => "|",
9684            Self::BitAnd => "&",
9685            Self::BitXor => "#",
9686            Self::LogicalXor => "xor",
9687            Self::JsonGet => "->",
9688            Self::JsonGetText => "->>",
9689            Self::JsonGetPath => "#>",
9690            Self::JsonGetPathText => "#>>",
9691            Self::JsonContains => "@>",
9692            Self::JsonPathExists => "@?",
9693            Self::JsonContainedBy => "<@",
9694            Self::JsonKeyExists => "?",
9695            Self::JsonKeysAny => "?|",
9696            Self::JsonKeysAll => "?&",
9697            Self::JsonDeletePath => "#-",
9698            Self::TsMatch => "@@",
9699            Self::InetContainedBy => "<<",
9700            Self::InetContainedByEq => "<<=",
9701            Self::InetContains => ">>",
9702            Self::InetContainsEq => ">>=",
9703            Self::InetOverlap => "&&",
9704            Self::Intersects => "?#",
9705            Self::IsBelow => "<^",
9706            Self::IsAbove => ">^",
9707            Self::PatternLt => "~<~",
9708            Self::PatternLtEq => "~<=~",
9709            Self::PatternGt => "~>~",
9710            Self::PatternGtEq => "~>=~",
9711        })
9712    }
9713}
9714
9715/// Quote `s` as a PG double-quoted identifier when required (keyword,
9716/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
9717/// Otherwise return it as-is. Returns an owned `String` to keep the call site
9718/// uniform.
9719pub(crate) fn quote_ident(s: &str) -> String {
9720    let needs_quote = match s.chars().next() {
9721        None => true,
9722        Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
9723        _ => {
9724            s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
9725                || s.chars().any(|c| c.is_ascii_uppercase())
9726                || is_keyword(s)
9727        }
9728    };
9729    if !needs_quote {
9730        return s.to_string();
9731    }
9732    let mut out = String::with_capacity(s.len() + 2);
9733    out.push('"');
9734    for c in s.chars() {
9735        if c == '"' {
9736            out.push_str("\"\"");
9737        } else {
9738            out.push(c);
9739        }
9740    }
9741    out.push('"');
9742    out
9743}
9744
9745fn is_keyword(s: &str) -> bool {
9746    matches!(
9747        &*s.to_ascii_lowercase(),
9748        "select"
9749            | "from"
9750            | "where"
9751            | "as"
9752            | "null"
9753            | "true"
9754            | "false"
9755            | "and"
9756            | "or"
9757            | "not"
9758            | "create"
9759            | "table"
9760            | "insert"
9761            | "into"
9762            | "values"
9763            | "index"
9764            | "on"
9765            | "begin"
9766            | "commit"
9767            | "rollback"
9768            | "is"
9769            | "between"
9770            | "in"
9771            | "like"
9772            | "group"
9773            | "distinct"
9774            | "union"
9775            | "all"
9776            | "join"
9777            | "inner"
9778            | "left"
9779            | "cross"
9780            | "outer"
9781            | "default"
9782            | "savepoint"
9783            | "release"
9784            | "to"
9785            | "having"
9786            | "show"
9787            | "extract"
9788            | "offset"
9789            | "asc"
9790            | "desc"
9791            | "interval"
9792    )
9793}
9794
9795#[cfg(test)]
9796mod tests {
9797    use super::*;
9798    use alloc::vec;
9799
9800    #[test]
9801    fn integer_literal_renders_without_dot() {
9802        assert_eq!(Literal::Integer(42).to_string(), "42");
9803    }
9804
9805    #[test]
9806    fn integral_float_keeps_dot() {
9807        assert_eq!(Literal::Float(1.0).to_string(), "1.0");
9808        assert_eq!(Literal::Float(1.5).to_string(), "1.5");
9809        assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
9810    }
9811
9812    #[test]
9813    fn string_literal_doubles_quote() {
9814        assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
9815    }
9816
9817    #[test]
9818    fn bool_and_null_render_uppercase() {
9819        assert_eq!(Literal::Bool(true).to_string(), "TRUE");
9820        assert_eq!(Literal::Bool(false).to_string(), "FALSE");
9821        assert_eq!(Literal::Null.to_string(), "NULL");
9822    }
9823
9824    #[test]
9825    fn binary_op_always_parenthesised() {
9826        let e = Expr::Binary {
9827            lhs: Box::new(Expr::Literal(Literal::Integer(1))),
9828            op: BinOp::Add,
9829            rhs: Box::new(Expr::Literal(Literal::Integer(2))),
9830        };
9831        assert_eq!(e.to_string(), "(1 + 2)");
9832    }
9833
9834    #[test]
9835    fn select_star_from_table() {
9836        let s = SelectStatement {
9837            locking: None,
9838            items: vec![SelectItem::Wildcard],
9839            from: Some(FromClause {
9840                primary: TableRef {
9841                    name: "users".into(),
9842                    alias: None,
9843                    only: false,
9844                    as_of_segment: None,
9845                    unnest_expr: None,
9846                    unnest_column_aliases: Vec::new(),
9847                    with_ordinality: false,
9848                    generate_series_args: None,
9849                    lateral_subquery: None,
9850                    jsonb_each_text_arg: None,
9851                    table_fn_call: None,
9852                    rows_from: None,
9853                    json_table: None,
9854                    scalar_fn_item: false,
9855                },
9856                joins: vec![],
9857            }),
9858            where_: None,
9859            group_by: None,
9860            group_by_all: false,
9861            having: None,
9862            unions: vec![],
9863            order_by: Vec::new(),
9864            limit: None,
9865            offset: None,
9866            limit_with_ties: false,
9867            window_check_exprs: Vec::new(),
9868            distinct: false,
9869            distinct_on: Vec::new(),
9870            ctes: vec![],
9871        };
9872        assert_eq!(s.to_string(), "SELECT * FROM users");
9873    }
9874
9875    #[test]
9876    fn quote_ident_for_uppercase_and_keyword() {
9877        assert_eq!(quote_ident("foo"), "foo");
9878        assert_eq!(quote_ident("Foo"), "\"Foo\"");
9879        assert_eq!(quote_ident("select"), "\"select\"");
9880        assert_eq!(quote_ident(""), "\"\"");
9881        assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
9882    }
9883}