Skip to main content

spg_sql/
ast.rs

1//! AST for the PG-dialect subset SPG accepts in v0.2.
2//!
3//! `Display` is implemented so that for any AST `a` produced by [`crate::parser`],
4//! re-parsing `format!("{a}")` yields a structurally equal AST. Binary and
5//! unary operators always emit parentheses to remove any precedence
6//! ambiguity — round-trip safety wins over prettiness.
7
8use alloc::boxed::Box;
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use core::fmt;
13
14/// `COPY … TO STDOUT` output format. `text` is PG's default
15/// (tab-separated, `\N` nulls, backslash escapes); `csv` follows
16/// RFC-4180-style quoting.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum CopyFormat {
19    #[default]
20    Text,
21    Csv,
22}
23
24/// Options for `COPY … TO STDOUT [WITH] (…)`. Defaults reproduce the
25/// bare `COPY … TO STDOUT` text-format behaviour, so an empty option
26/// list is a no-op. `delimiter` / `null_str` / `quote` fall back to the
27/// per-format defaults (text: `\t` / `\N`; csv: `,` / `` / `"`) when
28/// unset.
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct CopyOptions {
31    pub format: CopyFormat,
32    pub header: bool,
33    pub delimiter: Option<char>,
34    pub null_str: Option<String>,
35    pub quote: Option<char>,
36    /// v7.39 (round 247) — CSV `ESCAPE`: the character that precedes a
37    /// quote (or itself) inside a quoted cell. Defaults to the quote
38    /// character (PG's doubling behavior).
39    pub escape: Option<char>,
40    /// v7.39 (round 247) — CSV `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`:
41    /// columns whose non-NULL cells always quote. `Some(vec![])` is the
42    /// `*` spelling (every column).
43    pub force_quote: Option<Vec<String>>,
44    /// v7.39 (round 265) — CSV `FORCE_NOT_NULL (col, …)`: for these
45    /// columns an UNQUOTED empty field reads as the empty string rather
46    /// than NULL (probed). COPY FROM only.
47    pub force_not_null: Option<Vec<String>>,
48    /// v7.39 (round 265) — CSV `FORCE_NULL (col, …)`: for these columns
49    /// a QUOTED empty field (`""`) also reads as NULL (probed). COPY
50    /// FROM only.
51    pub force_null: Option<Vec<String>>,
52}
53
54/// v7.39 (round 218) — FETCH / MOVE cursor direction. PG grammar: single-row
55/// forms (NEXT / PRIOR / FIRST / LAST / ABSOLUTE n / RELATIVE n) return at
56/// most one row; multi-row forms (bare n / ALL / FORWARD [n|ALL] /
57/// BACKWARD [n|ALL]) stream a run. A negative bare/FORWARD count means
58/// BACKWARD (normalized at execution).
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum CursorDirection {
61    Next,
62    Prior,
63    First,
64    Last,
65    Absolute(i64),
66    Relative(i64),
67    /// Bare `FETCH n` / `FORWARD n` (negative = backward n).
68    Count(i64),
69    /// `ALL` / `FORWARD ALL`.
70    All,
71    Backward(i64),
72    BackwardAll,
73}
74
75impl fmt::Display for CursorDirection {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::Next => f.write_str("NEXT"),
79            Self::Prior => f.write_str("PRIOR"),
80            Self::First => f.write_str("FIRST"),
81            Self::Last => f.write_str("LAST"),
82            Self::Absolute(n) => write!(f, "ABSOLUTE {n}"),
83            Self::Relative(n) => write!(f, "RELATIVE {n}"),
84            Self::Count(n) => write!(f, "FORWARD {n}"),
85            Self::All => f.write_str("ALL"),
86            Self::Backward(n) => write!(f, "BACKWARD {n}"),
87            Self::BackwardAll => f.write_str("BACKWARD ALL"),
88        }
89    }
90}
91
92/// v7.39 (round 320, V53) — what a `DISCARD` throws away.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum DiscardTarget {
95    All,
96    Plans,
97    Sequences,
98    Temp,
99}
100
101impl fmt::Display for DiscardTarget {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.write_str(match self {
104            Self::All => "ALL",
105            Self::Plans => "PLANS",
106            Self::Sequences => "SEQUENCES",
107            Self::Temp => "TEMP",
108        })
109    }
110}
111
112/// v7.39 (round 535) — which maintenance statement, and therefore what
113/// its target names. Measured on PG18: INDEX / TABLE / CLUSTER name a
114/// relation, SCHEMA names a schema, and SYSTEM / DATABASE name neither
115/// in a way SPG can refuse.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum MaintainKind {
118    ReindexRelation,
119    ReindexSchema,
120    /// `REINDEX SYSTEM` / `REINDEX DATABASE`, and a bare `CLUSTER`.
121    Whole,
122    ClusterRelation,
123}
124
125/// v7.39 (round 547) — see [`Statement::SetDbRoleSetting`]. Boxed in the
126/// enum so the variant costs one pointer.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SetDbRoleSettingStatement {
129    pub database: Option<String>,
130    pub role: Option<String>,
131    pub param: Option<String>,
132    pub value: Option<String>,
133}
134
135/// v7.39 (round 696) — which operand a [`Statement::ValidateOnly`] names,
136/// and therefore which catalog answers whether it exists.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ValidateOnlyKind {
139    /// `LOCK TABLE <t> [, …]` — the relation must exist.
140    LockTable,
141    /// Every role named must exist: `DROP OWNED BY <r> [, …]`,
142    /// `REASSIGN OWNED BY <r> [, …] TO <r>`, and (round 697)
143    /// `SET SESSION AUTHORIZATION <r>`.
144    RoleName,
145    /// `SECURITY LABEL …` — PG refuses unconditionally, because no label
146    /// provider is loaded. SPG has none either.
147    SecurityLabel,
148    /// v7.39 (round 697) — `CREATE EXTENSION <e>`: the extension must be
149    /// AVAILABLE (PG: `extension "x" is not available`).
150    ExtensionAvailable,
151    /// v7.39 (round 708) — `ALTER TYPE <t> <any no-op form>`: the TYPE must
152    /// exist (PG: `type "x" does not exist`); the action itself stays a
153    /// no-op (PG genuinely renames; that residual is recorded).
154    TypeName,
155    /// v7.39 (round 708) — `ALTER AGGREGATE name(args) …`: names[0] is the
156    /// aggregate, the rest its argument type names (`*` = the `(*)` form).
157    /// Existence only; the action no-ops (PG really renames built-ins —
158    /// measured — and SPG does not model that).
159    AggregateName,
160    /// v7.39 (round 708) — `DROP CONVERSION <c>`: SPG ships no conversions,
161    /// so every name answers PG's `conversion "x" does not exist`.
162    ConversionName,
163    /// v7.39 (round 708) — `DROP LANGUAGE <l>`: an unknown language does
164    /// not exist; a shipped one is required (PG's two wordings, measured).
165    LanguageName,
166    /// v7.39 (round 709) — a collation name: performable or PG's
167    /// `collation "x" for encoding "UTF8" does not exist`.
168    CollationName,
169    /// v7.39 (round 709) — a text search configuration name.
170    TsConfigName,
171    /// v7.39 (round 709) — an event trigger name. SPG has none, so the
172    /// not-found answer is total.
173    EventTriggerName,
174    /// v7.39 (round 709) — a tablespace name. SPG has none beyond PG's two
175    /// built-ins, whose drop PG refuses with `permission denied` (measured).
176    TablespaceName,
177    /// v7.39 (round 709) — a large-object oid (names[0], decimal). The
178    /// registry is real (round 287), so the check is a lookup.
179    LargeObjectOid,
180    /// v7.39 (round 706) — `CREATE SERVER` / `CREATE FOREIGN TABLE` /
181    /// `CREATE FOREIGN DATA WRAPPER`. SPG has no foreign-data
182    /// infrastructure at all, so PG's refusals (`foreign-data wrapper "x"
183    /// does not exist`, `server "x" does not exist`) cannot be copied —
184    /// PG can refuse because the missing piece is installable there.
185    /// Accepted with a WARNING, the extension resolution (round 697):
186    /// refusing turns a dump that restores today into one that needs
187    /// editing, and silent acceptance was the actual defect.
188    ForeignInfra,
189    /// v7.39 (round 697) — `DROP EXTENSION <e>`: it must be installed
190    /// (PG: `extension "x" does not exist`).
191    ExtensionInstalled,
192}
193
194#[derive(Debug, Clone, PartialEq)]
195#[allow(clippy::large_enum_variant)] // Statement::Select dominates; Boxing would touch every match site
196pub enum Statement {
197    /// v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET <name>`.
198    ///
199    /// It used to be swallowed with the rest of the ALTER no-ops, which meant
200    /// `ALTER SYSTEM SET nosuch_guc = 1` was ACCEPTED where PG18 answers
201    /// `unrecognized configuration parameter`. SPG still applies nothing —
202    /// there is no postgresql.auto.conf to write — but a name it does not
203    /// know is now refused rather than swallowed.
204    ///
205    /// `None` is `RESET ALL`, which names no parameter.
206    AlterSystem {
207        parameter: Option<String>,
208    },
209    /// `DROP DATABASE [IF EXISTS] <name>`. SPG is single-database, so
210    /// this never succeeds; the name and the flag are carried so the
211    /// engine can answer with PG's wording for the two cases PG itself
212    /// has — an unknown name, or the database you are connected to.
213    DropDatabase {
214        name: String,
215        if_exists: bool,
216    },
217    /// A statement SPG accepts as a no-op but PG refuses inside a
218    /// transaction block — today `CREATE DATABASE` / `DROP DATABASE`,
219    /// which are no-ops here because SPG is single-database.
220    ///
221    /// The no-op path they used to share (`Statement::Empty`) also
222    /// carries CREATE ROLE, CREATE CAST and a dozen others that PG is
223    /// happy to run inside a transaction, so the object has to be named
224    /// to refuse the right ones.
225    NoOpPreventedInTransaction {
226        what: String,
227        /// v7.38.18 — `CREATE DATABASE … LC_COLLATE 'de_DE.utf8'` is in
228        /// every PostgreSQL bootstrap script there is, and SPG threw the
229        /// whole statement away. Being single-database makes the NAME a
230        /// no-op; it does not make the collation one, and a database
231        /// that quietly sorts by the container's `LANG` instead of the
232        /// one the script asked for is a silent difference in every
233        /// `ORDER BY` it will ever run.
234        ///
235        /// `LOCALE` and `LC_COLLATE` both land here; `LC_CTYPE` does
236        /// not, because SPG has no separate ctype.
237        collation: Option<String>,
238        /// v7.38.19 — the database's name, so `pg_database` can list one
239        /// that was created and can be connected to. It was thrown away
240        /// with the rest of the statement.
241        name: Option<String>,
242    },
243    /// v7.39 (round 696) — statements SPG performs nothing for, but whose
244    /// OPERAND PG validates before performing nothing either.
245    ///
246    /// All four used to be consumed whole by `is_dump_noise_statement`,
247    /// which meant `LOCK TABLE nosuch` and `DROP OWNED BY nosuchrole` were
248    /// ACCEPTED where PG18 errors. Accepting a statement that names
249    /// something that does not exist is the F29 shape: the caller is told
250    /// their intent was understood when the object it referred to is not
251    /// there.
252    ///
253    /// They share one variant because they share one rule — resolve the
254    /// name, refuse if absent, otherwise no-op — and four variants would be
255    /// four places for that rule to drift.
256    /// v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS] name(argtypes)[, …]`.
257    /// Consumed whole by the dump-noise list before, so `DROP AGGREGATE
258    /// nosuch(int)` reported success. PG validates every named aggregate's
259    /// EXISTENCE first (measured: a list with one unknown fails on the
260    /// unknown even when an earlier entry exists), renders the signature
261    /// with canonical type names (`int` → `integer`), and refuses to drop a
262    /// built-in (`cannot drop function sum(integer) because it is required
263    /// by the database system`). Every SPG aggregate is a built-in, so the
264    /// outcome is one of those two errors — or the IF EXISTS no-op.
265    ///
266    /// `args` holds the argument type names as written; `None` is the
267    /// `(*)` spelling.
268    DropAggregate {
269        if_exists: bool,
270        items: Vec<(String, Option<Vec<String>>)>,
271    },
272    /// v7.39 (round 750) — `ALTER ROLE|USER <name> … PASSWORD 'x' |
273    /// PASSWORD NULL`. The one attribute of the no-op family with a
274    /// SECURITY consequence: it was silently dropped (ledgered r710),
275    /// so a rotated credential never rotated. `None` = PASSWORD NULL
276    /// (the role keeps existing but can no longer password-auth).
277    AlterRolePassword {
278        name: String,
279        password: Option<String>,
280    },
281    ValidateOnly {
282        kind: ValidateOnlyKind,
283        /// The names the statement referred to. Empty means the form names
284        /// nothing (`SECURITY LABEL`, whose refusal is unconditional).
285        names: Vec<String>,
286    },
287
288    /// v7.39 (round 547) — `ALTER ROLE … SET/RESET` and
289    /// `ALTER DATABASE … SET/RESET`: the GUC defaults a session picks up
290    /// when it starts. Both used to land in the pg_dump no-op tail, so
291    /// the statement reported success and changed nothing.
292    ///
293    /// `database` / `role` are `None` for PG's oid 0 — `ALTER ROLE ALL`
294    /// sets both to None. `param` is `None` for RESET ALL. `value` is
295    /// `None` for RESET of one parameter.
296    SetDbRoleSetting(Box<SetDbRoleSettingStatement>),
297    /// v7.39 (round 288) — `SET CONSTRAINTS { ALL | <name>… }
298    /// { DEFERRED | IMMEDIATE }`. `deferred` carries the timing; the
299    /// name list is not yet honoured (ALL is what pg_dump emits and
300    /// what a circular-FK restore needs), so a named form applies to
301    /// all deferrable constraints too rather than silently doing
302    /// nothing.
303    /// v7.39 (round 308) — `SET CONSTRAINTS { ALL | name [, …] }
304    /// { DEFERRED | IMMEDIATE }`. An empty `names` is the ALL form;
305    /// otherwise the timing applies only to the constraints listed.
306    SetConstraints {
307        names: Vec<String>,
308        deferred: bool,
309    },
310
311    /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
312    /// [CASCADE | RESTRICT]`. Engine removes the matching tables
313    /// (each one) from the catalog; IF EXISTS makes the drop
314    /// idempotent. CASCADE / RESTRICT trailers parsed silently
315    /// (SPG always cascades index drops on table drop).
316    DropTable {
317        names: Vec<String>,
318        if_exists: bool,
319    },
320    /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
321    /// matching index across whichever table holds it.
322    DropIndex {
323        name: String,
324        if_exists: bool,
325    },
326    /// v7.14.0 — empty / comment-only statement. The lexer strips
327    /// `--` line comments and `/* … */` block comments (including
328    /// the MySQL conditional `/*!NNNNN … */` form) before the
329    /// parser ever sees them; a SQL chunk that contains nothing
330    /// else lands here. Engine returns CommandOk no-op so
331    /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
332    /// wrapped in conditional comments, etc.) load cleanly.
333    /// v7.39 (round 277) — SQL-level `PREPARE <name> [(type, …)] AS
334    /// <stmt>`. Session-scoped; the body keeps its `$N` placeholders
335    /// and is substituted at EXECUTE time.
336    Prepare {
337        name: String,
338        /// Declared parameter type names, in order. Empty when the
339        /// `(type, …)` list was omitted (PG infers them).
340        param_types: Vec<String>,
341        body: alloc::boxed::Box<Statement>,
342        /// The statement's own source text, which
343        /// `pg_prepared_statements.statement` reports verbatim.
344        source: String,
345    },
346    /// v7.39 (round 277) — `EXECUTE <name> [(arg, …)]`.
347    Execute {
348        name: String,
349        args: Vec<Expr>,
350    },
351    /// v7.39 (round 277) — `DEALLOCATE {<name> | ALL}`. `None` = ALL.
352    Deallocate(Option<String>),
353    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
354    /// [(kind, …)] ON <col>, … FROM <table>`. SPG records the object so
355    /// dumps restore and reflection is honest; the planner does not
356    /// consult it yet.
357    CreateStatistics {
358        name: String,
359        if_not_exists: bool,
360        /// Requested kinds as PG's single letters (`d` ndistinct,
361        /// `f` dependencies, `m` mcv). Empty = PG's default set.
362        kinds: Vec<String>,
363        columns: Vec<String>,
364        table: String,
365    },
366    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
367    DropStatistics {
368        name: String,
369        if_exists: bool,
370    },
371    /// v7.39 (round 278) — `CALL <proc>(…)`. Parses; the engine
372    /// reports that the procedure does not exist, because SPG has no
373    /// procedure catalog. Carried as a statement rather than raised at
374    /// parse time so the failure is a missing OBJECT (42883), not a
375    /// syntax error.
376    Call(String),
377    /// v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'`. Same shape:
378    /// 2PC is unavailable, which PG itself reports when
379    /// `max_prepared_transactions` is 0.
380    PrepareTransaction(String),
381    Empty,
382    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE]
383    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. The
384    /// canonical driver path for streaming large result sets (psycopg2
385    /// named cursors, JDBC setFetchSize).
386    DeclareCursor {
387        name: String,
388        /// `None` = neither keyword (PG default: backward allowed when the
389        /// plan supports it — always, for SPG's materialized cursors);
390        /// `Some(true)` = SCROLL; `Some(false)` = NO SCROLL (backward
391        /// fetch errors 55000).
392        scroll: Option<bool>,
393        /// `WITH HOLD` — survives the creating transaction's COMMIT.
394        hold: bool,
395        query: Box<Statement>,
396    },
397    /// v7.39 (round 218) — `FETCH [<direction>] [FROM|IN] <name>`.
398    FetchCursor {
399        name: String,
400        direction: CursorDirection,
401    },
402    /// v7.39 (round 218) — `MOVE [<direction>] [FROM|IN] <name>`: FETCH
403    /// without returning rows; the command tag carries the move count.
404    MoveCursor {
405        name: String,
406        direction: CursorDirection,
407    },
408    /// v7.39 (round 218) — `CLOSE <name>` / `CLOSE ALL` (`None` = ALL).
409    CloseCursor {
410        name: Option<String>,
411    },
412    /// v7.39 (round 222) — `LISTEN <channel>`: subscribe this session to
413    /// async notifications on the channel.
414    Listen(String),
415    /// v7.39 (round 222) — `NOTIFY <channel> [, '<payload>']`. Delivered at
416    /// COMMIT (PG semantics: transactional, deduplicated within the tx);
417    /// immediately under autocommit.
418    Notify {
419        channel: String,
420        payload: Option<String>,
421    },
422    /// v7.39 (round 222) — `UNLISTEN <channel>` / `UNLISTEN *` (`None` = *).
423    Unlisten(Option<String>),
424    /// `COPY table [(cols)] TO STDOUT` — the engine renders the
425    /// visible rows in COPY text format (tab-separated, `\N`
426    /// nulls, backslash escapes) as a single-text-column result
427    /// set; the wire layer streams CopyData from it.
428    CopyTo {
429        table: String,
430        columns: Option<Vec<String>>,
431        /// v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT`: an
432        /// arbitrary SELECT/VALUES/CTE (a whole [`Statement`], so set-ops and
433        /// VALUES ride through unchanged) whose result set is streamed in COPY
434        /// format. `Some` overrides `table`/`columns` (which are empty then);
435        /// `None` is the classic `COPY <table> …` shape.
436        query: Option<Box<Statement>>,
437        /// v7.37.x — `WITH (FORMAT csv, HEADER, DELIMITER, NULL, QUOTE)`
438        /// and the legacy `WITH CSV HEADER …` spelling. Default =
439        /// text format, no header (bare `COPY … TO STDOUT`).
440        options: CopyOptions,
441    },
442    /// v7.39 (round 249) — `COPY table [(cols)] FROM '<path>' [(opts)]`.
443    /// The engine is no_std and cannot read the file itself: the host
444    /// (embedded / server / tooling) reads the path and hands the bytes to
445    /// `Engine::copy_from_buffer`. Dispatching this statement straight to
446    /// the engine reports that contract.
447    CopyFromFile {
448        table: String,
449        columns: Option<Vec<String>>,
450        path: String,
451        options: CopyOptions,
452    },
453    /// v7.39 (round 249/252) — `COPY <table> [(cols)] TO '<file>'` (and
454    /// the `COPY (<query>) TO '<file>'` form). The engine is no_std and
455    /// cannot write the file itself: the host renders the payload via
456    /// `Engine::copy_to_buffer` and writes the path.
457    CopyToFile {
458        table: String,
459        columns: Option<Vec<String>>,
460        query: Option<Box<Statement>>,
461        path: String,
462        options: CopyOptions,
463    },
464    Select(SelectStatement),
465    CreateTable(CreateTableStatement),
466    /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
467    /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
468    /// no-op so PG dumps that include extension declarations
469    /// (notably `pgvector`) load against SPG without splitting
470    /// init scripts. mailrs migration follow-up F3.
471    CreateExtension(String),
472    /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
473    /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
474    /// the engine executes it at top level (mailrs round-10
475    /// A.2). Pre-v7.16.2 the parser discarded the body and the
476    /// engine returned CommandOk — a SEV-1 silent no-op that
477    /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
478    /// $$` idempotent migrations into invisible no-ops.
479    DoBlock(PlPgSqlBlock),
480    CreateIndex(CreateIndexStatement),
481    Insert(InsertStatement),
482    /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
483    Update(UpdateStatement),
484    /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
485    Delete(DeleteStatement),
486    /// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ `MERGE` statement.
487    /// `MERGE INTO target [alias] USING source [alias] ON cond
488    /// WHEN MATCHED [AND cond] THEN { UPDATE SET … | DELETE | DO NOTHING }
489    /// WHEN NOT MATCHED [AND cond] THEN { INSERT (cols) VALUES (vals) | DO NOTHING }
490    /// [WHEN …]`. SPG v7.17 supports table-based source (subquery
491    /// source is a follow-up); BY SOURCE / BY TARGET and RETURNING
492    /// are also follow-ups.
493    Merge(MergeStatement),
494    /// v7.39 (round 169) — `VACUUM [(opts)] [FULL|FREEZE|VERBOSE|ANALYZE]
495    /// [<table>]`. Was a parse-time no-op from the pre-MVCC era; with the
496    /// in-place MVCC gate ON, tombstoned versions are REAL bloat and a
497    /// customer's manual VACUUM must actually reclaim. `analyze` mirrors
498    /// the `VACUUM ANALYZE` spelling.
499    Vacuum {
500        table: Option<String>,
501        analyze: bool,
502    },
503    /// `BEGIN` / `START TRANSACTION` — with an optional explicit
504    /// `ISOLATION LEVEL …` mode (`None` = use the session default). PG
505    /// applies the level for the duration of this transaction only.
506    Begin(TransactionModes),
507    Commit,
508    Rollback,
509    /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
510    /// stack so a later `ROLLBACK TO <name>` can undo just the work
511    /// since this point.
512    Savepoint(String),
513    /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
514    /// named savepoint and discard later savepoints. Does not end the
515    /// transaction.
516    RollbackToSavepoint(String),
517    /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
518    /// rolling back. Keeps the work done since then.
519    ReleaseSavepoint(String),
520    /// `SHOW TABLES` — return the list of tables in the catalog.
521    ShowTables,
522    /// v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES` /
523    /// `SHOW SCHEMAS`. SPG is single-database; the executor
524    /// returns the canonical MySQL set so the mysql / MariaDB
525    /// client populates its database selector.
526    ShowDatabases,
527    /// v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE TABLE <t>`
528    /// returns a 2-column row `(Table, "Create Table")` carrying
529    /// the synthesized DDL. mysqldump emits this for every
530    /// table at scrape time.
531    ShowCreateTable(String),
532    /// v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES FROM <t>`
533    /// (also `SHOW INDEX`, `SHOW KEYS`).
534    ShowIndexes(String),
535    /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS`.
536    ShowStatus,
537    /// v7.17.0 Phase 3.P0-61 — MySQL `SHOW VARIABLES`.
538    ShowVariables,
539    /// r1067 — MySQL `SHOW VARIABLES LIKE 'pattern'` (sysbench-tpcc
540    /// probes isolation with it at connect).
541    ShowVariablesLike(String),
542    /// v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
543    ShowProcesslist,
544    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
545    /// pgbouncer sends `DISCARD ALL` between pooled client sessions to make
546    /// the connection look brand new to the next client; it used to be
547    /// swallowed as dump noise, so nothing was discarded.
548    Discard(DiscardTarget),
549    /// v7.39 (round 318, V51) — MySQL `KILL [CONNECTION | QUERY] <expr>`.
550    /// The id is an expression because MariaDB accepts one
551    /// (`KILL connection_id()` is the documented way to drop your own
552    /// connection). `query_only` is the `QUERY` form: stop the target's
553    /// running statement but leave it connected.
554    Kill {
555        query_only: bool,
556        id: Box<Expr>,
557    },
558    /// `SHOW COLUMNS FROM <table>` — return one row per column with
559    /// its declared name / type / nullability.
560    ShowColumns(String),
561    /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
562    /// Role is optional; defaults to `readonly` when omitted.
563    CreateUser(CreateUserStatement),
564    /// `DROP USER 'name'` (v4.1). v7.39 (read01 round 58) — `IF EXISTS` is
565    /// carried through: PG skips with a NOTICE rather than erroring.
566    DropUser {
567        name: String,
568        if_exists: bool,
569    },
570    /// v7.39 (RLS) — `SET ROLE { name | NONE | DEFAULT }` / `RESET ROLE`.
571    /// `Some(name)` switches the session's effective role (drives
572    /// `current_user` and RLS enforcement); `None` resets to the login
573    /// identity (the Admin superuser).
574    SetRole(Option<String>),
575    /// v7.39 (read01 round 57) — `GRANT <privs> ON <object> TO <roles>`.
576    Grant(GrantStatement),
577    /// v7.39 (read01 round 57) — `REVOKE [GRANT OPTION FOR] <privs> ON
578    /// <object> FROM <roles>`.
579    Revoke(GrantStatement),
580    /// v7.39 (RLS) — `CREATE POLICY name ON table …`.
581    CreatePolicy(CreatePolicyStatement),
582    /// v7.39 (RLS) — `ALTER POLICY name ON table …`.
583    AlterPolicy(AlterPolicyStatement),
584    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
585    DropPolicy(DropPolicyStatement),
586    /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
587    ShowUsers,
588    /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
589    /// single-column text table describing the rewritten plan tree
590    /// for `inner`. `analyze` triggers an actual exec to attach
591    /// observed row counts and elapsed micros to each node.
592    Explain(ExplainStatement),
593    /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
594    /// Synchronous rebuild of an NSW index. With the optional
595    /// encoding clause, every stored cell at the indexed column is
596    /// also re-encoded through `coerce_value` before the new graph
597    /// builds.
598    AlterIndex(AlterIndexStatement),
599    /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
600    /// The only setting in v6.7.2 is `hot_tier_bytes`, which
601    /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
602    /// for the named table.
603    AlterTable(AlterTableStatement),
604    /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
605    /// The catalog row lives in `spg_publications`. Publisher-side
606    /// WAL filtering arrives in v6.1.5.
607    CreatePublication(CreatePublicationStatement),
608    /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
609    /// no-op when the publication does not exist.
610    DropPublication {
611        name: String,
612        /// v7.39 (round 754, F31-B4) — `IF EXISTS` quietly skips a
613        /// missing publication; the bare form refuses with PG's
614        /// sentence (PG18-measured — the old "silent no-op" note on
615        /// the executor was wrong).
616        if_exists: bool,
617    },
618    /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
619    /// publication ordered by name with `(name, scope_summary,
620    /// table_count)` columns. The scope summary is the human-
621    /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
622    /// TABLES EXCEPT …`; `table_count` is `NULL` for the
623    /// `AllTables` scope and the table-list length otherwise.
624    ShowPublications,
625    /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
626    /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
627    /// in `spg_subscriptions`; when the subscription is
628    /// `enabled = true` (default) the server spawns a
629    /// background worker that connects to `conn` and drains the
630    /// requested publication(s) into the local engine.
631    CreateSubscription(CreateSubscriptionStatement),
632    /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
633    /// PUBLICATION, silent no-op when absent. Stops the
634    /// associated worker thread before removing the row.
635    DropSubscription {
636        name: String,
637        /// v7.39 (round 754, F31-B4) — same contract as
638        /// [`Statement::DropPublication`].
639        if_exists: bool,
640    },
641    /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
642    /// subscription ordered by name with `(name, conn_str,
643    /// publications, enabled, last_received_pos)`.
644    ShowSubscriptions,
645    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
646    /// Blocks until the local server's apply position reaches
647    /// `<pos>` or `<ms>` elapses. Server-layer command: the
648    /// engine refuses it (`EngineError::Unsupported`) since
649    /// `lag_state` lives in `spg-server`'s `ServerState`.
650    WaitForWalPosition {
651        pos: u64,
652        /// `None` → wait forever; `Some(ms)` → return after `ms`
653        /// milliseconds even if the target isn't reached.
654        timeout_ms: Option<u64>,
655    },
656    /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
657    /// table; `ANALYZE <name>` re-stats just one. Populates
658    /// `spg_statistic` with per-column null_frac + n_distinct +
659    /// 100-bucket equi-depth histogram.
660    Analyze(Option<String>),
661    /// v7.39 (round 535) — `REINDEX { INDEX | TABLE | SCHEMA | DATABASE
662    /// | SYSTEM } [CONCURRENTLY] <name>` and `CLUSTER [VERBOSE]
663    /// [<table> [USING <index>]]`.
664    ///
665    /// SPG has neither index bloat nor a clustering order to rebuild, so
666    /// the work is a no-op — but PG VALIDATES the target, and both were
667    /// swallowed at parse time, so `REINDEX TABLE typo` reported success.
668    /// The name is carried now so the engine can say what PG says.
669    Maintain {
670        kind: MaintainKind,
671        /// `REINDEX … CONCURRENTLY`. Carried for the same reason as
672        /// [`CreateIndexStatement::concurrently`]: PG bars the
673        /// CONCURRENTLY form inside a transaction block and allows the
674        /// plain one.
675        concurrently: bool,
676        /// `None` for the whole-database forms, which name nothing.
677        target: Option<String>,
678    },
679    /// v7.37.17 (17.6 sibling) — `TRUNCATE [TABLE] [ONLY] <name>
680    /// [, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE |
681    /// RESTRICT]`. Clears every row from each named table. SPG's
682    /// SEQUENCE identity is per-table; RESTART IDENTITY reinitializes
683    /// the associated sequence to its starting value. CASCADE
684    /// currently walks direct FK-referring tables and truncates
685    /// them too (PG's semantics). The ONLY modifier (skip partitions)
686    /// and RESTRICT (default) are accepted with no effect since
687    /// SPG's declarative partitions are always truncated together.
688    Truncate {
689        tables: Vec<String>,
690        restart_identity: bool,
691        cascade: bool,
692        /// v7.39 (round 647) — `TRUNCATE ONLY t`. Absorbed as a no-op
693        /// since v7.14 on the reasoning that SPG's children are separate
694        /// relations a truncate does not descend into. Same reasoning
695        /// round 621 applied to `FROM ONLY`, and it stopped being true
696        /// for the same reason: measured, `TRUNCATE <inheritance parent>`
697        /// leaves the children's rows where PG empties them, and
698        /// `TRUNCATE ONLY <partitioned parent>` is silently accepted
699        /// where PG refuses it outright.
700        only: bool,
701    },
702    /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
703    /// BTree-cold indices and merges small cold-tier segments
704    /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
705    /// 4 MiB) into a single larger segment per (table, index).
706    /// `WHERE` predicate filtering on which tables to compact is
707    /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
708    /// v6.7.3 only supports the bare form.
709    CompactColdSegments,
710    /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
711    /// parameter on the engine; v7.12.1 honours
712    /// `default_text_search_config` (consumed by `to_tsvector` /
713    /// `plainto_tsquery` family when called without an explicit
714    /// config arg). All other names are accepted as a no-op so PG
715    /// dumps with `SET client_encoding`, `SET search_path` etc.
716    /// load cleanly.
717    SetParameter {
718        name: String,
719        value: SetValue,
720        /// v7.38 (read01 P3.19) — `SET LOCAL` scopes the change to the
721        /// current transaction; the engine saves the prior value and
722        /// restores it at COMMIT / ROLLBACK. Plain `SET` (and `SET
723        /// SESSION`) leave this false and persist for the session.
724        local: bool,
725    },
726    /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
727    /// multi-assignment (mysqldump preamble uses
728    /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
729    /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
730    /// source order. Pairs whose LHS is a MySQL session/user
731    /// variable (`@VAR` / `@@VAR`) are recorded with the raw
732    /// name so the engine can ignore them; pairs whose LHS is
733    /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
734    /// go through the regular `set_session_param` path.
735    SetParameterList(Vec<(String, SetValue)>),
736    /// v7.39 (round 430) — MySQL's USER-defined variables:
737    /// `SET @x = 5, @s := CONCAT('a','b')`. Distinct from
738    /// [`Self::SetParameter`] (a `@@`-style engine/session setting) in
739    /// every way that matters: the value is an arbitrary EXPRESSION, the
740    /// name lives in its own per-session namespace, and reading an unset
741    /// one answers NULL rather than raising. `:=` and `=` are the same
742    /// assignment here.
743    ///
744    /// Before this the parser stripped every `@`, so `@x` and `@@x` were
745    /// the same node: `SET @x = 5` silently landed in the session-parameter
746    /// store where nothing could read it back, and `SELECT @x` failed with
747    /// "Unknown system variable".
748    /// v7.39 (round 554) — `SET @a = …, SETTING = …`.
749    ///
750    /// `settings` is the trailing half a mysqldump preamble writes:
751    /// `SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'`
752    /// saves a value and changes it in one statement. The parser used
753    /// to refuse the mixture outright, so no mysqldump could be
754    /// restored past its preamble.
755    SetUserVars(Vec<(String, Expr)>, Vec<(String, Expr)>),
756    /// v7.38 轴 4 — `SET [SESSION] TRANSACTION ISOLATION LEVEL …`
757    /// (plus optional READ ONLY / READ WRITE / DEFERRABLE clauses
758    /// silently accepted). PG-standard surface for picking an
759    /// isolation level. Engine tracks the value on
760    /// `Engine::current_isolation_level()`; actual MVCC / SSI
761    /// semantics implementation lands separately. PG itself maps
762    /// READ UNCOMMITTED to READ COMMITTED; SPG mirrors that —
763    /// effectively every level reads as READ COMMITTED in v7.37.8.
764    SetTransaction {
765        modes: TransactionModes,
766    },
767    /// v7.38 轴 4 — `SHOW <param>` returns a 1-column 1-row result
768    /// with the parameter's current value as TEXT. Today the only
769    /// recognised param is `transaction_isolation`; further
770    /// surfaces (`search_path`, `application_name`, …) land as the
771    /// session-parameter inventory grows.
772    ShowParameter(String),
773    /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
774    /// to its default. No-op for parameters SPG does not track.
775    ResetParameter(Option<String>),
776    /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
777    /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
778    /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
779    /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
780    /// languages parse but error at exec time with a clear
781    /// unsupported message.
782    CreateFunction(CreateFunctionStatement),
783    /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
784    /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
785    /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
786    /// triggers and column-list / WHEN clauses are out of scope
787    /// for v7.12.4.
788    CreateTrigger(CreateTriggerStatement),
789    /// v7.39 (round 139) — `CREATE RULE name AS ON event TO table [WHERE cond]
790    /// DO [ALSO|INSTEAD] { NOTHING | command }` query-rewrite rule.
791    CreateRule(CreateRuleStatement),
792    /// v7.39 (round 139) — `DROP RULE [IF EXISTS] name ON table`.
793    DropRule {
794        name: String,
795        table: String,
796        if_exists: bool,
797    },
798    /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
799    /// no-op when missing if `IF EXISTS` is set.
800    DropTrigger {
801        name: String,
802        table: String,
803        if_exists: bool,
804    },
805    /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
806    /// DROP TRIGGER but global (no table scope).
807    DropFunction {
808        name: String,
809        /// v7.39 (read01 round 62) — the argument TYPES, when the statement gave
810        /// them: `DROP FUNCTION f(int)` drops that overload only. `None` = no
811        /// argument list, which PG accepts only when the name is unambiguous.
812        args: Option<Vec<String>>,
813        if_exists: bool,
814    },
815    /// v7.17.0 — `CREATE [TEMPORARY] SEQUENCE [IF NOT EXISTS] name
816    /// [AS data_type]
817    /// [INCREMENT [BY] n]
818    /// [MINVALUE n | NO MINVALUE]
819    /// [MAXVALUE n | NO MAXVALUE]
820    /// [START [WITH] n]
821    /// [CACHE n]
822    /// [[NO] CYCLE]
823    /// [OWNED BY {table.col | NONE}]`.
824    /// Closes the round-7+ silent-no-op SEQUENCE story so pg_dump
825    /// emits + nextval/currval/setval downstream all work.
826    CreateSequence(CreateSequenceStatement),
827    /// v7.17.0 — `ALTER SEQUENCE [IF EXISTS] name <options>` with
828    /// the same option grammar as CREATE SEQUENCE, plus
829    /// `RESTART [WITH n]` and `OWNED BY ...` re-attach.
830    AlterSequence(AlterSequenceStatement),
831    /// v7.17.0 — `DROP SEQUENCE [IF EXISTS] name [, name…]
832    /// [CASCADE | RESTRICT]`. CASCADE / RESTRICT trailers parsed
833    /// silently (no FK on sequences).
834    DropSequence {
835        names: Vec<String>,
836        if_exists: bool,
837    },
838    /// v7.17.0 Phase 1.2 — `CREATE [OR REPLACE] [TEMPORARY] VIEW
839    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …>`. Closes the
840    /// silent-no-op VIEW story from the v7.17 customer-readiness
841    /// audit: pre-v7.17 SPG parsed CREATE VIEW as Statement::Empty
842    /// so any downstream `SELECT FROM v` errored with table-not-
843    /// found. The view body is stored verbatim; SELECT FROM <v>
844    /// rewrites at exec-time by prepending the view body as a
845    /// synthetic CTE.
846    CreateView(CreateViewStatement),
847    /// v7.17.0 Phase 1.2 — `DROP VIEW [IF EXISTS] name [, name…]
848    /// [CASCADE | RESTRICT]`. Removes the matching view from the
849    /// catalog; CASCADE/RESTRICT parsed silently.
850    DropView {
851        names: Vec<String>,
852        if_exists: bool,
853    },
854    /// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW [IF NOT
855    /// EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
856    /// Closes the silent-no-op MATERIALIZED VIEW story. Storage
857    /// model: the materialised result lives as a regular table
858    /// with the matching name + a parallel
859    /// `materialized_views` registry mapping name → body source
860    /// (used by REFRESH).
861    CreateMaterializedView(CreateMaterializedViewStatement),
862    /// v7.17.0 Phase 1.3 — `REFRESH MATERIALIZED VIEW name [WITH
863    /// [NO] DATA]`. Re-runs the stored body and replaces the
864    /// cached rows. `WITH NO DATA` truncates without re-running.
865    RefreshMaterializedView {
866        name: String,
867        with_data: bool,
868    },
869    /// v7.17.0 Phase 1.3 — `DROP MATERIALIZED VIEW [IF EXISTS]
870    /// name [, name…] [CASCADE | RESTRICT]`. Drops both the
871    /// backing table and the source registry entry.
872    DropMaterializedView {
873        names: Vec<String>,
874        if_exists: bool,
875    },
876    /// v7.17.0 Phase 1.4 — `CREATE TYPE name AS ENUM ('a', 'b',
877    /// …)`. Closes the silent-no-op CREATE TYPE story so PG
878    /// dumps that declare enum types load with real constraints
879    /// instead of becoming free-form TEXT. Future kinds
880    /// (composite / range / domain) extend the inner `kind`
881    /// enum.
882    CreateType(CreateTypeStatement),
883    /// v7.37 D.55 — `ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
884    /// [{BEFORE | AFTER} 'existing']`. Extends an enum's label list so
885    /// enum evolution stops being a silent no-op. `position` is
886    /// `Some((is_before, anchor))`.
887    /// v7.39 (read01 round 49) — `ALTER TYPE t RENAME VALUE 'old' TO 'new'`.
888    /// Used to be swallowed by the ALTER TYPE no-op tail, so the rename was
889    /// accepted and silently ignored.
890    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
891    /// Used to be swallowed as dump noise, so a comment was accepted and lost
892    /// (and obj_description / col_description always returned NULL).
893    /// `kind` is lowercase ("table" / "column" / "index" / …); for a column
894    /// `name` is the dotted `table.column`. `comment: None` = `IS NULL` = remove.
895    CommentOn {
896        kind: String,
897        name: String,
898        comment: Option<String>,
899    },
900    AlterTypeRenameValue {
901        type_name: String,
902        old: String,
903        new: String,
904    },
905    AlterTypeAddValue {
906        type_name: String,
907        label: String,
908        if_not_exists: bool,
909        position: Option<(bool, String)>,
910    },
911    /// v7.17.0 Phase 1.4 — `DROP TYPE [IF EXISTS] name [, name…]
912    /// [CASCADE | RESTRICT]`. Removes the matching enum/domain
913    /// from the catalog.
914    DropType {
915        names: Vec<String>,
916        if_exists: bool,
917    },
918    /// v7.17.0 Phase 1.5 — `CREATE DOMAIN name AS base_type
919    /// [DEFAULT expr] [NOT NULL | NULL] [CHECK (expr)]*`.
920    /// A DOMAIN is a named CHECK-constrained alias over a built-
921    /// in type. The CHECK + NOT NULL + DEFAULT clauses apply to
922    /// every column declared with the domain. Closes the
923    /// silent-no-op CREATE DOMAIN story so PG dumps that ship
924    /// validated identifier types (email, positive_int, …) keep
925    /// their guarantees.
926    CreateDomain(CreateDomainStatement),
927    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`. Every form was
928    /// previously swallowed by the catch-all DDL arm: the statement
929    /// reported success and did nothing, so a migration that dropped a
930    /// constraint kept rejecting the data it had just been told to
931    /// accept.
932    AlterDomain {
933        name: String,
934        action: AlterDomainAction,
935    },
936    /// v7.17.0 Phase 1.5 — `DROP DOMAIN [IF EXISTS] name
937    /// [, name…] [CASCADE | RESTRICT]`. Removes the matching
938    /// domain from the catalog.
939    DropDomain {
940        names: Vec<String>,
941        if_exists: bool,
942    },
943    /// v7.17.0 Phase 1.6 — `CREATE SCHEMA [IF NOT EXISTS]
944    /// name [AUTHORIZATION user]`. SPG is single-database;
945    /// schemas are tracked as a namespace registry so pg_dump
946    /// multi-schema declarations land cleanly and `SELECT *
947    /// FROM information_schema.schemata` returns real entries.
948    /// Schema-qualified `schema.table` references still strip
949    /// the prefix at lookup time per PG (schemas are not
950    /// isolation boundaries in v7.17 — see project-next-docket
951    /// for the v7.18+ isolation tracking).
952    CreateSchema {
953        name: String,
954        if_not_exists: bool,
955    },
956    /// v7.17.0 Phase 1.6 — `DROP SCHEMA [IF EXISTS] name
957    /// [, name…] [CASCADE | RESTRICT]`. Removes the schema
958    /// from the registry; built-in `public` / `pg_catalog` /
959    /// `information_schema` cannot be dropped.
960    DropSchema {
961        names: Vec<String>,
962        if_exists: bool,
963    },
964}
965
966/// v7.39 (round 260) — the `ALTER DOMAIN` actions SPG implements.
967#[derive(Debug, Clone, PartialEq)]
968pub enum AlterDomainAction {
969    AddConstraint { name: Option<String>, check: Expr },
970    DropConstraint { name: String, if_exists: bool },
971    SetDefault(Expr),
972    DropDefault,
973    SetNotNull,
974    DropNotNull,
975    RenameTo(String),
976}
977
978/// v7.17.0 Phase 1.5 — `CREATE DOMAIN` AST.
979#[derive(Debug, Clone, PartialEq)]
980pub struct CreateDomainStatement {
981    pub name: String,
982    /// Base type for the domain (one of the built-in
983    /// `ColumnTypeName` variants).
984    pub base_type: ColumnTypeName,
985    /// v7.39 (round 259) — `CREATE DOMAIN child AS parent …` where
986    /// `parent` is itself a DOMAIN. The parser already captured the
987    /// unknown type name; it just was not carried here, so the parent's
988    /// CHECK constraints were invisible and a value violating them was
989    /// silently accepted. `base_type` still holds the ultimate scalar
990    /// type, which is what the storage tier stores.
991    pub base_domain: Option<String>,
992    /// Optional `DEFAULT <expr>`. Resolved at engine-side
993    /// CREATE TABLE time when a column is bound to this domain.
994    pub default: Option<Expr>,
995    /// `NOT NULL` from the domain definition. Engine ORs this
996    /// with the column-level nullability so the strictest of the
997    /// two wins (i.e. the column is non-nullable if either side
998    /// says so).
999    pub not_null: bool,
1000    /// Zero-or-more `CHECK (expr)` predicates. Each one is
1001    /// enforced as part of the column's CHECK list at INSERT /
1002    /// UPDATE time, with `VALUE` substituted for the column's
1003    /// current cell value.
1004    pub checks: Vec<Expr>,
1005}
1006
1007/// v7.17.0 Phase 1.4 — `CREATE TYPE` AST.
1008#[derive(Debug, Clone, PartialEq, Eq)]
1009pub struct CreateTypeStatement {
1010    pub name: String,
1011    pub kind: TypeKind,
1012}
1013
1014/// v7.17.0 Phase 1.4 — flavour of the new type. Only ENUM is
1015/// implemented; the variant set is open so Phase 1.5 (DOMAIN)
1016/// and later (COMPOSITE, RANGE) can land without an AST shape
1017/// migration.
1018///
1019/// v7.37.x (ζ-B Phase 1 composite accept) — added Composite for
1020/// `CREATE TYPE name AS (field_name field_type, …)`. Phase 1
1021/// stores the field list in the catalog so PG dumps that emit
1022/// `CREATE TYPE … AS (…)` don't error out; using a composite type
1023/// as a column type lands in Phase 2 (Value::Composite encoding +
1024/// ROW() literal + field-access syntax).
1025#[derive(Debug, Clone, PartialEq, Eq)]
1026pub enum TypeKind {
1027    /// `AS ENUM ('a', 'b', …)`. Order is preserved (PG enum
1028    /// labels are ordered).
1029    Enum { labels: Vec<String> },
1030    /// `AS (field_name field_type, …)`. Order matters; PG
1031    /// composite literals are positional.
1032    Composite {
1033        fields: Vec<(String, ColumnTypeName)>,
1034        /// v7.39 (round 264) — parallel to `fields`: the raw type NAME
1035        /// when a field's type is not a builtin (i.e. another composite).
1036        /// The parser already captures it; without carrying it here a
1037        /// nested composite field resolved to the Text placeholder and
1038        /// the inner record never became a record.
1039        field_user_types: Vec<Option<String>>,
1040    },
1041}
1042
1043/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
1044/// a string literal, an identifier (often a config name), an
1045/// integer/float, or the bare `DEFAULT` keyword.
1046#[derive(Debug, Clone, PartialEq)]
1047pub enum SetValue {
1048    String(String),
1049    Ident(String),
1050    Number(String),
1051    Default,
1052}
1053
1054/// v7.38 轴 4 — PG-standard isolation levels. SPG accepts all four
1055/// at parse time and tracks the selected value on the engine. The
1056/// actual semantic differentiation (REPEATABLE READ snapshot,
1057/// SERIALIZABLE SSI) lands in the v7.38 isolation framework train;
1058/// today every level reads as effective READ COMMITTED (which is
1059/// also how PG treats READ UNCOMMITTED — it silently upgrades to
1060/// READ COMMITTED). Default = `ReadCommitted`.
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1062pub enum IsolationLevel {
1063    ReadUncommitted,
1064    #[default]
1065    ReadCommitted,
1066    RepeatableRead,
1067    Serializable,
1068}
1069
1070impl IsolationLevel {
1071    /// Canonical PG-style display name, as `SHOW transaction_isolation`
1072    /// would return it. v7.39 (round 770, F31 tranche 6 #154) — PG
1073    /// KEEPS the "read uncommitted" label (measured: `BEGIN ISOLATION
1074    /// LEVEL READ UNCOMMITTED; SHOW transaction_isolation` answers
1075    /// `read uncommitted`) and only BEHAVES as read committed; the old
1076    /// fold renamed the label too.
1077    /// v7.39 — the MySQL display name, which is NOT the PG one: MySQL
1078    /// hyphenates and upper-cases. Measured on MySQL 9.7.2 by setting
1079    /// each level and reading `@@transaction_isolation` back:
1080    /// `READ-UNCOMMITTED` / `READ-COMMITTED` / `REPEATABLE-READ` /
1081    /// `SERIALIZABLE` (the last has no hyphen because it is one word).
1082    ///
1083    /// This exists so the two MySQL surfaces cannot drift: both
1084    /// `SHOW VARIABLES` and `@@transaction_isolation` used to carry
1085    /// their own hard-coded literal, and the literals disagreed —
1086    /// one said `REPEATABLE-READ` while the engine ran read committed.
1087    /// v7.39 — parse what `default_transaction_isolation` holds. PG
1088    /// accepts the SQL spellings and stores them lower-cased with a
1089    /// space; anything else is not a level this understands and the
1090    /// caller keeps its own default rather than guessing.
1091    #[must_use]
1092    pub fn from_pg_name(name: &str) -> Option<Self> {
1093        match name.trim().to_ascii_lowercase().as_str() {
1094            "read uncommitted" => Some(Self::ReadUncommitted),
1095            "read committed" => Some(Self::ReadCommitted),
1096            "repeatable read" => Some(Self::RepeatableRead),
1097            "serializable" => Some(Self::Serializable),
1098            _ => None,
1099        }
1100    }
1101
1102    #[must_use]
1103    pub fn as_mysql_str(self) -> &'static str {
1104        match self {
1105            Self::ReadUncommitted => "READ-UNCOMMITTED",
1106            Self::ReadCommitted => "READ-COMMITTED",
1107            Self::RepeatableRead => "REPEATABLE-READ",
1108            Self::Serializable => "SERIALIZABLE",
1109        }
1110    }
1111
1112    pub fn as_pg_str(self) -> &'static str {
1113        match self {
1114            Self::ReadUncommitted => "read uncommitted",
1115            Self::ReadCommitted => "read committed",
1116            Self::RepeatableRead => "repeatable read",
1117            Self::Serializable => "serializable",
1118        }
1119    }
1120}
1121
1122impl core::fmt::Display for IsolationLevel {
1123    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1124        f.write_str(self.as_pg_str())
1125    }
1126}
1127
1128/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
1129/// single fixed-shape DDL; the WITH-clause options PG supports
1130/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
1131/// scope for v6.1.4 — `enabled` defaults to true and there are
1132/// no other knobs to set in v6.1.x.
1133#[derive(Debug, Clone, PartialEq, Eq)]
1134pub struct CreateSubscriptionStatement {
1135    pub name: String,
1136    /// Connection string in PG keyword=value form (e.g.
1137    /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
1138    /// `host` and `port` fields; the rest is reserved for
1139    /// future v6.1.x options.
1140    pub conn_str: String,
1141    /// One or more publications on the remote side. Order is
1142    /// preserved verbatim from the DDL; the worker requests them
1143    /// in this order. v6.1.4 records the list; v6.1.5
1144    /// publisher-side filtering enforces it.
1145    pub publications: Vec<String>,
1146}
1147
1148/// v7.17.0 — `CREATE SEQUENCE` AST node. See [`Statement::CreateSequence`].
1149#[derive(Debug, Clone, PartialEq, Eq)]
1150pub struct CreateSequenceStatement {
1151    pub name: String,
1152    pub if_not_exists: bool,
1153    pub temporary: bool,
1154    /// Optional `AS data_type`. Default in PG is BIGINT; SPG matches.
1155    pub data_type: Option<SequenceDataType>,
1156    pub options: SequenceOptions,
1157}
1158
1159/// v7.17.0 — narrow type for `AS` clause of CREATE SEQUENCE.
1160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1161pub enum SequenceDataType {
1162    SmallInt,
1163    Int,
1164    BigInt,
1165}
1166
1167/// v7.17.0 — option grammar shared by CREATE / ALTER SEQUENCE.
1168/// All fields are optional. `min_value`/`max_value` carry
1169/// `Some(SeqBound::NoBound)` for `NO MINVALUE` / `NO MAXVALUE`.
1170#[derive(Debug, Clone, Default, PartialEq, Eq)]
1171pub struct SequenceOptions {
1172    pub increment: Option<i64>,
1173    pub min_value: Option<SeqBound>,
1174    pub max_value: Option<SeqBound>,
1175    pub start: Option<i64>,
1176    /// `RESTART [WITH n]` — ALTER-only. `Some(None)` = bare
1177    /// RESTART, `Some(Some(n))` = RESTART WITH n.
1178    pub restart: Option<Option<i64>>,
1179    pub cache: Option<i64>,
1180    pub cycle: Option<bool>,
1181    pub owned_by: Option<SequenceOwnedBy>,
1182}
1183
1184/// v7.17.0 — `MINVALUE n` / `NO MINVALUE`.
1185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1186pub enum SeqBound {
1187    Value(i64),
1188    NoBound,
1189}
1190
1191/// v7.17.0 — `OWNED BY {table.col | NONE}`.
1192#[derive(Debug, Clone, PartialEq, Eq)]
1193pub enum SequenceOwnedBy {
1194    None,
1195    Column { table: String, column: String },
1196}
1197
1198/// v7.17.0 Phase 1.3 — `CREATE MATERIALIZED VIEW` AST node.
1199#[derive(Debug, Clone, PartialEq)]
1200pub struct CreateMaterializedViewStatement {
1201    pub name: String,
1202    pub if_not_exists: bool,
1203    /// Optional `(col, col, …)` rename list. Applies to the
1204    /// backing table at CREATE / REFRESH time.
1205    pub columns: Vec<String>,
1206    /// Underlying SELECT. Re-parsed at REFRESH time to rebuild
1207    /// the cached rows.
1208    pub body: SelectStatement,
1209    /// `WITH DATA` (default) = materialise the rows at CREATE
1210    /// time. `WITH NO DATA` = create an empty backing table;
1211    /// callers must REFRESH before SELECT returns rows.
1212    pub with_data: bool,
1213    /// v7.38 (read01 P6.49) — when true this node came from
1214    /// `CREATE TABLE … AS <select>` (CTAS) / `SELECT … INTO`, so the
1215    /// executor creates a plain table and does NOT register it in the
1216    /// materialized-view registry (no REFRESH semantics).
1217    pub as_plain_table: bool,
1218    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE … AS <select>`. Only
1219    /// meaningful together with `as_plain_table`; the executor puts the
1220    /// resulting table in the creating session's namespace.
1221    pub temporary: bool,
1222}
1223
1224/// v7.39 (read01 round 132) — `WITH [LOCAL | CASCADED] CHECK OPTION` on an
1225/// auto-updatable view. `Cascaded` is PG's default when the bare
1226/// `WITH CHECK OPTION` is written.
1227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1228pub enum ViewCheckOption {
1229    Local,
1230    Cascaded,
1231}
1232
1233/// v7.17.0 Phase 1.2 — `CREATE VIEW` AST node.
1234#[derive(Debug, Clone, PartialEq)]
1235pub struct CreateViewStatement {
1236    pub name: String,
1237    pub or_replace: bool,
1238    pub if_not_exists: bool,
1239    pub temporary: bool,
1240    /// Optional `(col, col, …)` rename list. When non-empty,
1241    /// these override the body's projected column names per-
1242    /// position at SELECT-from-view time.
1243    pub columns: Vec<String>,
1244    /// Underlying SELECT. Re-parsed lazily at SELECT-from-view
1245    /// time to materialise the view as a synthetic CTE.
1246    pub body: SelectStatement,
1247    /// v7.39 (round 132) — `WITH CHECK OPTION`. When set, a write through this
1248    /// view whose resulting row fails the view's WHERE is rejected (SQLSTATE
1249    /// 44000). `None` = no check option.
1250    pub check_option: Option<ViewCheckOption>,
1251}
1252
1253/// v7.17.0 — `ALTER SEQUENCE` AST node.
1254#[derive(Debug, Clone, PartialEq, Eq)]
1255pub struct AlterSequenceStatement {
1256    pub name: String,
1257    pub if_exists: bool,
1258    pub options: SequenceOptions,
1259    /// v7.39 (read01 round 49) — `ALTER SEQUENCE old RENAME TO new`. Set
1260    /// instead of `options`; the two forms are mutually exclusive in PG.
1261    pub rename_to: Option<String>,
1262}
1263
1264/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
1265/// the [`PublicationScope`] shape. v6.1.2 only accepted
1266/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
1267/// variants by flipping the parser gate (no AST migration).
1268#[derive(Debug, Clone, PartialEq, Eq)]
1269pub struct CreatePublicationStatement {
1270    pub name: String,
1271    pub scope: PublicationScope,
1272}
1273
1274/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
1275/// flips the parser gate for the `ForTables` / `AllTablesExcept`
1276/// variants — the on-disk shape, snapshot serialisation, and the
1277/// AST round-trip Display path were already in place in v6.1.2
1278/// so this is a parser-only widening.
1279#[derive(Debug, Clone, PartialEq, Eq)]
1280pub enum PublicationScope {
1281    AllTables,
1282    ForTables(Vec<String>),
1283    AllTablesExcept(Vec<String>),
1284    /// v7.39 (round 754, F31-B5) — `FOR TABLES IN SCHEMA <name>`
1285    /// (PG 15+). AST-only: the executor folds `public` to
1286    /// [`PublicationScope::AllTables`] (SPG's single-schema world)
1287    /// and refuses any other schema with PG's sentence, so the
1288    /// catalog / serializer / replication filter never see it.
1289    TablesInSchema(String),
1290}
1291
1292#[derive(Debug, Clone, PartialEq, Eq)]
1293pub struct AlterIndexStatement {
1294    pub name: String,
1295    pub target: AlterIndexTarget,
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Eq)]
1299pub enum AlterIndexTarget {
1300    /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
1301    /// rebuilds the existing graph in place without touching the
1302    /// column encoding; `Some(enc)` re-encodes every cell first.
1303    Rebuild { encoding: Option<VecEncoding> },
1304    /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
1305    /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
1306    /// uses it to make the migration idempotent (re-running on a
1307    /// DB where the rename already happened is a no-op rather
1308    /// than an error).
1309    Rename { new: String, if_exists: bool },
1310    /// v7.39 (round 710) — `SET ( option = value, … )` / `RESET ( … )`.
1311    /// Was a SYNTAX ERROR; PG resolves the INDEX first (`relation "x"
1312    /// does not exist`), so the index is validated and the storage
1313    /// parameters no-op (SPG engine-manages them, as ALTER TABLE's
1314    /// SET/RESET arms already record).
1315    StorageParams,
1316}
1317
1318/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
1319/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
1320/// can add more SET subjects without changing the dispatch shape.
1321#[derive(Debug, Clone, PartialEq)]
1322pub struct AlterTableStatement {
1323    pub name: String,
1324    /// v7.13.2 — mailrs round-6 S1. One or more subactions
1325    /// separated by commas in the source SQL. PG-semantic apply
1326    /// is sequential; engine bails on first error (no
1327    /// transactional rollback of completed subactions in v7.13).
1328    /// Single-subaction shape stays a 1-element vec.
1329    pub targets: Vec<AlterTableTarget>,
1330}
1331
1332#[derive(Debug, Clone, PartialEq)]
1333#[allow(clippy::large_enum_variant)]
1334pub enum AlterTableTarget {
1335    /// v7.39 (round 647) — `ALTER TABLE c INHERIT p` / `NO INHERIT p`.
1336    ///
1337    /// Both were accepted-and-ignored since v7.37.18, on the reasoning
1338    /// that SPG has no PG-style inheritance. Round 645 gave it one, and
1339    /// the reasoning went stale: `NO INHERIT` reported success while the
1340    /// child stayed attached, which is the worst kind of answer — the
1341    /// statement says it worked and the catalog disagrees.
1342    Inherit { parent: String, detach: bool },
1343    /// Per-table hot-tier byte budget override. The freezer
1344    /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
1345    SetHotTierBytes(u64),
1346    /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
1347    /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
1348    /// Engine validates existing rows against the new constraint
1349    /// before installing it.
1350    AddForeignKey(ForeignKeyConstraint),
1351    /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
1352    /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
1353    /// no-op when no FK with that name exists; otherwise raises.
1354    DropForeignKey { name: String, if_exists: bool },
1355    /// v7.39 (round 431) — MySQL's `ALTER TABLE t DROP {INDEX|KEY} name`,
1356    /// the counterpart of `ADD INDEX`. Lowers to the same catalog action
1357    /// as the standalone `DROP INDEX` statement.
1358    DropIndex { name: String, if_exists: bool },
1359    /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
1360    /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
1361    /// (20 migrate-*.sql hits). Engine appends the column to the
1362    /// schema and back-fills every existing row with the DEFAULT
1363    /// (or NULL when no DEFAULT and the column is nullable).
1364    AddColumn {
1365        column: ColumnDef,
1366        if_not_exists: bool,
1367    },
1368    /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
1369    /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
1370    /// existing row's column value by evaluating the optional
1371    /// USING expression (default `col::<ty>`) and re-coercing
1372    /// against the new column type.
1373    AlterColumnType {
1374        column: String,
1375        new_type: ColumnTypeName,
1376        using: Option<Expr>,
1377        /// v7.39 (round 713) — `COLLATE <name>` between the type and
1378        /// USING. PG re-collates the column, and an ABSENT clause RESETS
1379        /// the collation to the type default (measured round 713) — so
1380        /// `None` is not "leave it alone". The type parser consumed the
1381        /// clause all along and this surface dropped it on the floor:
1382        /// the statement succeeded and the ordering did not change, the
1383        /// silent-divergence shape. Folded variant + the name as written.
1384        collation: Option<(Collation, String)>,
1385    },
1386    /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
1387    /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
1388    /// every row's value at that position is removed; any index
1389    /// on the column is dropped. `if_exists` makes the drop a
1390    /// no-op when the column is missing. `cascade` removes
1391    /// dependents (FKs referencing the column, partial indexes
1392    /// whose predicate names the column); without it, the engine
1393    /// rejects when dependents exist.
1394    DropColumn {
1395        column: String,
1396        if_exists: bool,
1397        cascade: bool,
1398    },
1399    /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
1400    /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
1401    /// CONSTRAINT name CHECK (expr)` — table-level constraints
1402    /// installed post-CREATE-TABLE. pg_dump emits PKs as a
1403    /// separate ALTER TABLE statement, so this surface lets the
1404    /// dump load straight through.
1405    AddTableConstraint(TableConstraint),
1406    /// v7.39 (round 652) — `OWNER TO <role>`. SPG is single-owner, so
1407    /// there is nothing to record; what PG does that SPG did not is
1408    /// REFUSE a role that does not exist. The name has to reach the
1409    /// engine for that, because only the engine knows the roles.
1410    OwnerTo { role: String },
1411    /// v7.39 (round 652) — `CLUSTER ON <index>` and `SET WITHOUT
1412    /// CLUSTER` (the latter as `None`). SPG has no clustered storage, so
1413    /// the hint is still a no-op; naming an index that does not exist is
1414    /// not.
1415    ClusterOn { index: Option<String> },
1416    /// v7.39 (round 652) — `VALIDATE CONSTRAINT <name>`: scan the rows
1417    /// already in the table against a constraint added `NOT VALID` and,
1418    /// if they all pass, mark it validated. It used to be swallowed as a
1419    /// no-op on the theory that SPG validated at ADD time; SPG did not.
1420    ValidateConstraint { name: String },
1421    /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
1422    /// Renames the column in the schema and propagates the rename
1423    /// to every stored source string that references it as a
1424    /// (potentially-qualified) column identifier: CHECK predicates,
1425    /// partial-index predicates, runtime DEFAULT expressions, and
1426    /// triggers' `UPDATE OF` column lists. Function bodies and
1427    /// trigger bodies are NOT auto-rewritten — they're loose
1428    /// source text and may contain references SPG can't statically
1429    /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
1430    /// the column even if dependents exist; users renaming a
1431    /// column referenced by a function body update the function
1432    /// body separately.
1433    RenameColumn { old: String, new: String },
1434    /// v7.39 (read01 round 48) — `ALTER TABLE t RENAME CONSTRAINT old TO new`.
1435    /// Reachable now that the schema stores user-supplied constraint names.
1436    RenameConstraint { old: String, new: String },
1437    /// v7.22 (round-13 T2) — mark a column auto-incrementing.
1438    /// pg_dump splits SERIAL/IDENTITY columns into a plain integer
1439    /// column plus either `ALTER COLUMN c SET DEFAULT nextval(…)`
1440    /// (serial) or `ALTER COLUMN c ADD GENERATED … AS IDENTITY (…)`
1441    /// (identity); both lower to this. SPG's auto-increment is
1442    /// max+1-scan based, so the dump's `setval(…)` calls stay
1443    /// no-ops without losing the sequence position.
1444    SetColumnAutoIncrement {
1445        column: String,
1446        /// The implicit sequence pg_dump names for an identity
1447        /// column (`ADD GENERATED … ( SEQUENCE NAME s … )`) or the
1448        /// nextval target for a serial default. The engine creates
1449        /// it if absent so the dump's later `setval(s, …)` lands.
1450        seq_name: Option<String>,
1451    },
1452    /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
1453    /// table itself (mailrs round-10 A.5 carve-out — mailrs's
1454    /// migrate-042 uses it). The engine moves the table entry
1455    /// in the catalog under the new name; child catalog state
1456    /// (FKs pointing at this table, triggers watching this
1457    /// table) tracks the rename through the storage layer.
1458    RenameTable { new: String },
1459    /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
1460    /// { ALL | <name> }`. Toggles whether row-level triggers
1461    /// fire on subsequent INSERT/UPDATE/DELETE on the table.
1462    /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
1463    /// ENABLE epilogue around every table's data block so the
1464    /// rows already-computed in prod don't get re-rewritten
1465    /// (and so trigger-driven side effects like
1466    /// audit/queueing don't re-fire during a bulk reload).
1467    /// `which == TriggerSelector::All` toggles every trigger
1468    /// on the table; `Named(name)` toggles one trigger. The
1469    /// engine persists the disabled state on `TriggerDef.enabled`
1470    /// (catalog FILE_VERSION 25+) and the row-write paths skip
1471    /// the trigger when `!enabled`.
1472    SetTriggerEnabled {
1473        which: TriggerSelector,
1474        enabled: bool,
1475    },
1476    /// v7.39 (RLS) — `ALTER TABLE t { ENABLE | DISABLE | FORCE | NO FORCE }
1477    /// ROW LEVEL SECURITY`. `enabled` = Some for ENABLE/DISABLE (sets
1478    /// `relrowsecurity`); `force` = Some for FORCE/NO FORCE (sets
1479    /// `relforcerowsecurity`). Exactly one is `Some` per statement.
1480    SetRowSecurity {
1481        enabled: Option<bool>,
1482        force: Option<bool>,
1483    },
1484    /// v7.37.16 (16.3) — `ALTER TABLE parent ATTACH PARTITION child
1485    /// <bounds>`. Promotes an existing table `child` to a partition
1486    /// of `parent` using PG-style `FOR VALUES …` / `DEFAULT` bounds.
1487    /// Engine validates that `child`'s columns are layout-compatible
1488    /// with `parent` and that every row in `child` satisfies the
1489    /// bound before installing the role.
1490    AttachPartition {
1491        child: String,
1492        bounds: PartitionOfBoundsAst,
1493    },
1494    /// v7.37.16 (16.4 + 16.5) — `ALTER TABLE parent DETACH PARTITION
1495    /// child [CONCURRENTLY] [FINALIZE]`. Demotes a partition back
1496    /// to a standalone table (clears `partition_role`) and removes
1497    /// it from the parent's child set. v7.37.16.5: `CONCURRENTLY`
1498    /// is parser-accepted; engine performs the same atomic detach
1499    /// (single-engine, no replication lag — the PG semantics that
1500    /// require the two-phase split don't apply).
1501    DetachPartition {
1502        child: String,
1503        concurrently: bool,
1504        finalize: bool,
1505    },
1506    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col SET DEFAULT
1507    /// <expr>`. Engine re-parses + freezes the literal at this point,
1508    /// matching CREATE TABLE-side default semantics. Volatile shapes
1509    /// (`now()` / `nextval`) take the runtime-default path.
1510    AlterColumnSetDefault { column: String, default_expr: Expr },
1511    /// v7.37.18 (18.1) — `ALTER TABLE … ALTER COLUMN col DROP DEFAULT`.
1512    AlterColumnDropDefault { column: String },
1513    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col SET NOT NULL`.
1514    /// Engine validates that no existing row has NULL in that column
1515    /// before flipping the flag (PG semantics — partial NOT NULL
1516    /// would surface inconsistently).
1517    AlterColumnSetNotNull { column: String },
1518    /// v7.37.18 (18.2) — `ALTER TABLE … ALTER COLUMN col DROP NOT NULL`.
1519    AlterColumnDropNotNull { column: String },
1520    /// v7.39 (round 220) — `ALTER TABLE … ALTER COLUMN col RESTART
1521    /// [WITH n]` on an identity column (`None` = bare RESTART, from the
1522    /// column's start value = 1). Engine records a next-value floor over
1523    /// SPG's max+1 identity allocation.
1524    AlterColumnRestart { column: String, with: Option<i64> },
1525    /// v7.38 (read01 U10) — `ALTER TABLE … ALTER COLUMN col DROP
1526    /// EXPRESSION` turns a stored generated column into a plain column
1527    /// (its generation expression is removed; existing values are kept).
1528    AlterColumnDropExpression { column: String, if_exists: bool },
1529    /// v7.38 (read01, T28) — `ALTER COLUMN col DROP IDENTITY [IF EXISTS]`:
1530    /// de-generate an identity column into a plain column.
1531    AlterColumnDropIdentity { column: String, if_exists: bool },
1532    /// v7.38 (read01 U12) — `ALTER TABLE … ALTER COLUMN col SET
1533    /// EXPRESSION AS (expr)` (PG 17) changes a stored generated column's
1534    /// expression and recomputes every existing row.
1535    AlterColumnSetExpression { column: String, expr: Expr },
1536    /// v7.39 (round 710) — `OF <type>` / the type half of the typed-table
1537    /// binding. The BINDING stays a no-op (recorded); the TYPE must exist
1538    /// (PG: `type "x" does not exist`).
1539    OfType { type_name: String },
1540    /// v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`. The
1541    /// identity setting no-ops (SPG has no logical replication consumer);
1542    /// the INDEX must exist on this table (PG: `index "i" for table "t"
1543    /// does not exist`).
1544    ReplicaIdentityUsingIndex { index: String },
1545}
1546
1547/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
1548/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
1549/// modifiers; v7.16.1 ships the two shapes pg_dump actually
1550/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
1551/// shouldn't surface from a dump.
1552#[derive(Debug, Clone, PartialEq, Eq)]
1553pub enum TriggerSelector {
1554    /// Every trigger on the table.
1555    All,
1556    /// A specific trigger by name.
1557    Named(String),
1558}
1559
1560/// Each bool mirrors one independent PG `EXPLAIN (…)` option (ANALYZE,
1561/// SUGGEST, COSTS OFF, BUFFERS, TIMING OFF, …); they compose freely, so a
1562/// bitflags word or a nested options struct would only relocate the lint
1563/// while making the option each caller sets harder to read.
1564#[allow(clippy::struct_excessive_bools)]
1565#[derive(Debug, Clone, PartialEq)]
1566pub struct ExplainStatement {
1567    pub analyze: bool,
1568    /// v7.39 (round 225) — widened from SelectStatement so EXPLAIN
1569    /// INSERT/UPDATE/DELETE parses (PG explains DML); the engine renders
1570    /// `Insert on / Update on / Delete on` trees for them.
1571    pub inner: Box<Statement>,
1572    /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
1573    /// advisor pass: after the regular plan tree, the engine
1574    /// emits one suggestion line per column referenced in the
1575    /// query's WHERE / JOIN that has no covering index on the
1576    /// owning table.
1577    pub suggest: bool,
1578    /// v7.37.7 — `EXPLAIN (COSTS OFF) <SELECT>` strips wall-clock
1579    /// `elapsed=…us` annotations from the Total line (and any
1580    /// future cost-bearing lines). PG-standard option used by
1581    /// regression suites and diff-friendly EXPLAIN output. When
1582    /// `true`, takes precedence over the per-session
1583    /// `SPG_TEST_EXPLAIN_NO_COSTS` GUC.
1584    pub costs_off: bool,
1585    /// v7.37.22 (22.7) — `EXPLAIN (BUFFERS) <SELECT>`. PG-standard
1586    /// option that surfaces hot/cold/shared block counters. SPG's
1587    /// hot-tier scan path counts examined rows; the BUFFERS option
1588    /// makes that an explicit per-operator annotation.
1589    pub buffers: bool,
1590    /// v7.37.22 (22.7) — `EXPLAIN (TIMING [ON|OFF]) <SELECT>`. PG
1591    /// uses this to disable per-operator timing while still
1592    /// emitting actual-row counts (cheaper than ANALYZE). Default
1593    /// when EXPLAIN ANALYZE is set: TIMING ON. `false` strips the
1594    /// timing portion of the Total line. Decoupled from `costs_off`:
1595    /// PG's COSTS OFF strips estimated cost; TIMING OFF strips
1596    /// measured wall-clock.
1597    pub timing_off: bool,
1598    /// v7.37.22 (22.7) — `EXPLAIN (SETTINGS) <SELECT>`. PG appends
1599    /// modified GUC values to the plan output. SPG emits the
1600    /// session params that diverge from default after the main
1601    /// plan body.
1602    pub settings: bool,
1603    /// v7.37.22 (22.7) — `EXPLAIN (WAL) <SELECT>`. PG counts WAL
1604    /// bytes / records / FPI emitted by the query. SPG's
1605    /// write-side queries (INSERT/UPDATE/DELETE wrapped in EXPLAIN
1606    /// ANALYZE) report against the engine WAL counter delta.
1607    pub wal: bool,
1608    /// v7.39 (round 227) — `EXPLAIN (SUMMARY OFF)` suppresses the trailing
1609    /// `Planning Time:` / `Execution Time:` lines. PG defaults SUMMARY on
1610    /// for ANALYZE and off otherwise; SPG emits them for ANALYZE unless
1611    /// this is set.
1612    pub summary_off: bool,
1613    /// v7.37.23 (23.5) — `EXPLAIN (FORMAT text|json|xml|yaml)`.
1614    /// PG's standard format selector. Default is text. JSON / XML
1615    /// / YAML emit a single-row TEXT result whose body wraps the
1616    /// existing line-per-operator text in the chosen container —
1617    /// PG-compatible just enough for dashboards that parse those
1618    /// container shapes (pgAdmin's JSON path picker, etc.).
1619    pub format: ExplainFormat,
1620}
1621
1622#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1623pub enum ExplainFormat {
1624    #[default]
1625    Text,
1626    Json,
1627    Xml,
1628    Yaml,
1629}
1630
1631/// v7.39 (RLS) — the command a `CREATE POLICY` scopes to. `All` is the default.
1632#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1633pub enum PolicyCmd {
1634    All,
1635    Select,
1636    Insert,
1637    Update,
1638    Delete,
1639}
1640
1641/// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
1642/// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`.
1643#[derive(Debug, Clone, PartialEq)]
1644pub struct CreatePolicyStatement {
1645    pub name: String,
1646    pub table: String,
1647    /// `true` = PERMISSIVE (default), `false` = RESTRICTIVE.
1648    pub permissive: bool,
1649    pub cmd: PolicyCmd,
1650    /// Empty = PUBLIC.
1651    pub roles: Vec<String>,
1652    pub using: Option<Expr>,
1653    pub with_check: Option<Expr>,
1654}
1655
1656/// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
1657/// [USING (expr)] [WITH CHECK (expr)] }`. Cannot change PERMISSIVE/RESTRICTIVE
1658/// or the command (matches PG).
1659#[derive(Debug, Clone, PartialEq)]
1660pub struct AlterPolicyStatement {
1661    pub name: String,
1662    pub table: String,
1663    pub rename_to: Option<String>,
1664    pub roles: Option<Vec<String>>,
1665    pub using: Option<Expr>,
1666    pub with_check: Option<Expr>,
1667}
1668
1669/// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`.
1670#[derive(Debug, Clone, PartialEq, Eq)]
1671pub struct DropPolicyStatement {
1672    pub name: String,
1673    pub table: String,
1674    pub if_exists: bool,
1675}
1676
1677#[derive(Debug, Clone, PartialEq, Eq)]
1678pub struct CreateUserStatement {
1679    pub name: String,
1680    /// Empty when the statement carried no PASSWORD — legal for a bare
1681    /// `CREATE ROLE`, which cannot log in anyway.
1682    pub password: String,
1683    /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
1684    /// the parser; the engine validates against `Role::parse` so a
1685    /// typo lands as a runtime error with a clear message rather than
1686    /// a parse failure.
1687    pub role: String,
1688    /// v7.39 (read01 round 58) — the PG role attributes. `None` = the
1689    /// statement did not say, so the default for its spelling applies:
1690    /// `CREATE USER` is `CREATE ROLE … LOGIN`, `CREATE ROLE` is NOLOGIN;
1691    /// both default to INHERIT and NOSUPERUSER.
1692    pub login: Option<bool>,
1693    pub inherit: Option<bool>,
1694    pub superuser: Option<bool>,
1695    /// `true` when spelled `CREATE USER` (LOGIN by default).
1696    pub is_user: bool,
1697}
1698
1699/// v7.39 (round 322, V46) — PG's function volatility class. Declarative:
1700/// it tells the planner how far a call may be moved or folded. SPG records
1701/// it faithfully (`pg_proc.provolatile`, `pg_get_functiondef`) and does not
1702/// yet exploit it for constant folding.
1703#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1704pub enum FunctionVolatility {
1705    Immutable,
1706    Stable,
1707    #[default]
1708    Volatile,
1709}
1710
1711impl FunctionVolatility {
1712    /// PG's one-character `pg_proc.provolatile` code.
1713    #[must_use]
1714    pub const fn as_pg_char(self) -> &'static str {
1715        match self {
1716            Self::Immutable => "i",
1717            Self::Stable => "s",
1718            Self::Volatile => "v",
1719        }
1720    }
1721
1722    #[must_use]
1723    pub const fn as_sql(self) -> &'static str {
1724        match self {
1725            Self::Immutable => "IMMUTABLE",
1726            Self::Stable => "STABLE",
1727            Self::Volatile => "VOLATILE",
1728        }
1729    }
1730}
1731
1732/// v7.39 (round 322, V46) — PG's parallel-safety class.
1733#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1734pub enum FunctionParallel {
1735    #[default]
1736    Unsafe,
1737    Restricted,
1738    Safe,
1739}
1740
1741impl FunctionParallel {
1742    /// PG's one-character `pg_proc.proparallel` code.
1743    #[must_use]
1744    pub const fn as_pg_char(self) -> &'static str {
1745        match self {
1746            Self::Unsafe => "u",
1747            Self::Restricted => "r",
1748            Self::Safe => "s",
1749        }
1750    }
1751
1752    #[must_use]
1753    pub const fn as_sql(self) -> &'static str {
1754        match self {
1755            Self::Unsafe => "PARALLEL UNSAFE",
1756            Self::Restricted => "PARALLEL RESTRICTED",
1757            Self::Safe => "PARALLEL SAFE",
1758        }
1759    }
1760}
1761
1762/// v7.39 (round 322, V46) — the attribute clauses `CREATE FUNCTION` accepts
1763/// on either side of its body. Defaults are PG's: VOLATILE, called on null
1764/// input, SECURITY INVOKER, not leakproof, PARALLEL UNSAFE, and the
1765/// language's default cost / rows.
1766#[derive(Debug, Clone, Copy, PartialEq, Default)]
1767pub struct FunctionAttrs {
1768    pub volatility: FunctionVolatility,
1769    /// `STRICT` / `RETURNS NULL ON NULL INPUT`: a call with any NULL
1770    /// argument returns NULL without running the body.
1771    pub strict: bool,
1772    pub security_definer: bool,
1773    pub leakproof: bool,
1774    pub parallel: FunctionParallel,
1775    /// `COST n` — `None` leaves PG's per-language default.
1776    pub cost: Option<f64>,
1777    /// `ROWS n` — set-returning functions only; `None` = default.
1778    pub rows: Option<f64>,
1779}
1780
1781impl FunctionAttrs {
1782    /// The attribute words `pg_get_functiondef` puts on their own line,
1783    /// in PG's order (measured on 18.4: volatility, PARALLEL, STRICT,
1784    /// SECURITY DEFINER, LEAKPROOF, COST, ROWS). Empty when everything is
1785    /// at its default — PG then emits no such line at all.
1786    #[must_use]
1787    pub fn render_words(&self) -> alloc::vec::Vec<alloc::string::String> {
1788        let mut out = alloc::vec::Vec::new();
1789        if self.volatility != FunctionVolatility::Volatile {
1790            out.push(alloc::string::String::from(self.volatility.as_sql()));
1791        }
1792        if self.parallel != FunctionParallel::Unsafe {
1793            out.push(alloc::string::String::from(self.parallel.as_sql()));
1794        }
1795        if self.strict {
1796            out.push(alloc::string::String::from("STRICT"));
1797        }
1798        if self.security_definer {
1799            out.push(alloc::string::String::from("SECURITY DEFINER"));
1800        }
1801        if self.leakproof {
1802            out.push(alloc::string::String::from("LEAKPROOF"));
1803        }
1804        if let Some(c) = self.cost {
1805            out.push(alloc::format!("COST {}", render_attr_number(c)));
1806        }
1807        if let Some(r) = self.rows {
1808            out.push(alloc::format!("ROWS {}", render_attr_number(r)));
1809        }
1810        out
1811    }
1812}
1813
1814/// PG prints a whole-numbered cost / rows without a decimal point.
1815fn render_attr_number(v: f64) -> alloc::string::String {
1816    // no_std: `f64::fract` lives in std, so compare against the truncation.
1817    let whole = v as i64;
1818    if v.abs() < 1e15 && (whole as f64) == v {
1819        alloc::format!("{whole}")
1820    } else {
1821        alloc::format!("{v}")
1822    }
1823}
1824
1825/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
1826/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
1827/// (the row-level trigger body the CREATE TRIGGER below references).
1828/// Non-trigger user-defined functions parse but error at execution
1829/// time with a clear unsupported message; that surface lands in
1830/// v7.12.5+.
1831#[derive(Debug, Clone, PartialEq)]
1832pub struct CreateFunctionStatement {
1833    pub name: String,
1834    /// `OR REPLACE` was present; an existing function with the
1835    /// same name is overwritten instead of erroring.
1836    pub or_replace: bool,
1837    /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
1838    /// list `()` (sufficient for trigger functions). Other shapes
1839    /// parse and store the args but the executor refuses to call
1840    /// them.
1841    pub args: Vec<FunctionArg>,
1842    /// `RETURNS <type>` — `trigger` is the supported shape for
1843    /// v7.12.4; arbitrary return types parse to
1844    /// [`FunctionReturn::Other`].
1845    pub returns: FunctionReturn,
1846    /// `LANGUAGE <lang>` clause. PG accepts the clause on either
1847    /// side of `AS $$...$$`; the parser canonicalises to one slot.
1848    /// `plpgsql` and `sql` are the two interesting values.
1849    pub language: String,
1850    /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
1851    /// a structured AST; non-trigger / non-plpgsql bodies stay as
1852    /// the raw source text so the v7.12.5+ executor can pick them
1853    /// up without a parser rev.
1854    pub body: FunctionBody,
1855    /// v7.39 (round 322, V46) — `IMMUTABLE` / `STRICT` / `PARALLEL SAFE` /
1856    /// `SECURITY DEFINER` / `LEAKPROOF` / `COST` / `ROWS`. PG accepts them
1857    /// on either side of the body; before this they were a parse error, so
1858    /// PG's own `pg_dump` output would not restore.
1859    pub attrs: FunctionAttrs,
1860}
1861
1862/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
1863#[derive(Debug, Clone, PartialEq)]
1864pub struct FunctionArg {
1865    /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
1866    /// (the default); `OUT` / `INOUT` parse but the executor
1867    /// refuses them.
1868    pub mode: FunctionArgMode,
1869    /// Optional arg name. Trigger functions traditionally don't
1870    /// name their args (they read NEW/OLD instead), so `None` is
1871    /// the common case.
1872    pub name: Option<String>,
1873    /// Declared type, normalised to the SPG `DataType` mapping
1874    /// where one exists. Unknown / extension types parse as a
1875    /// raw string under [`FunctionArgType::Raw`].
1876    pub ty: FunctionArgType,
1877}
1878
1879#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1880pub enum FunctionArgMode {
1881    In,
1882    Out,
1883    InOut,
1884}
1885
1886#[derive(Debug, Clone, PartialEq)]
1887pub enum FunctionArgType {
1888    Typed(ColumnTypeName),
1889    /// Unknown / extension types — kept as the parser-side raw
1890    /// identifier so error messages can name them precisely.
1891    Raw(String),
1892}
1893
1894#[derive(Debug, Clone, PartialEq)]
1895pub enum FunctionReturn {
1896    /// `RETURNS TRIGGER` — the row-level trigger function shape.
1897    /// v7.12.4 ships exactly this for execution.
1898    Trigger,
1899    /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
1900    /// the function is unused (since v7.12.4 doesn't ship scalar
1901    /// function invocation).
1902    Void,
1903    /// `RETURNS <type>` for any concrete data type. Reserved for
1904    /// v7.12.5+'s scalar UDF surface.
1905    Type(ColumnTypeName),
1906    /// `RETURNS <ident>` for types SPG doesn't know — extension
1907    /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
1908    Other(String),
1909}
1910
1911#[derive(Debug, Clone, PartialEq)]
1912pub enum FunctionBody {
1913    /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
1914    /// trigger-function executor walks this directly without
1915    /// re-parsing.
1916    PlPgSql(PlPgSqlBlock),
1917    /// Raw source text — parser couldn't (or didn't try to)
1918    /// structure-parse the body. Used for `LANGUAGE sql`
1919    /// functions and any PL/pgSQL body that contains v7.12.5+
1920    /// features the v7.12.4 parser doesn't yet recognise. The
1921    /// executor returns an unsupported error when invoked.
1922    Raw(String),
1923}
1924
1925/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
1926/// from assignment + return to a real-PL/pgSQL surface:
1927/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
1928/// control flow, `RAISE` diagnostics, and embedded SQL
1929/// statements that execute through the regular engine path.
1930/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
1931/// which mailrs's trigger doesn't need but other PG customers
1932/// may; deferred to a future minor release.
1933#[derive(Debug, Clone, PartialEq)]
1934pub struct PlPgSqlBlock {
1935    /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
1936    /// preceding `BEGIN`. Empty when the body opens directly with
1937    /// `BEGIN`. Declarations execute in order; each may reference
1938    /// earlier-declared locals in its init expression.
1939    pub declarations: Vec<PlPgSqlDeclare>,
1940    pub statements: Vec<PlPgSqlStmt>,
1941    /// v7.37.20 (20.10) — `EXCEPTION WHEN <cond> [OR <cond>...] THEN
1942    /// <body>` handlers appended to the block. Empty when no
1943    /// EXCEPTION clause is present. When a body statement raises
1944    /// (via RAISE EXCEPTION, ASSERT falsy, or a runtime error),
1945    /// handlers are tried in order; the first matching condition
1946    /// runs its body and the block terminates cleanly. `OTHERS`
1947    /// matches any exception. Unhandled exceptions propagate.
1948    pub exception_handlers: Vec<ExceptionHandler>,
1949}
1950
1951/// v7.37.20 (20.10) — one `WHEN <cond> [OR <cond>...] THEN <body>`
1952/// arm inside an EXCEPTION block.
1953#[derive(Debug, Clone, PartialEq)]
1954pub struct ExceptionHandler {
1955    /// Condition names (`OTHERS`, `unique_violation`, etc.). Multiple
1956    /// conditions joined by `OR` share one handler body.
1957    pub conditions: Vec<String>,
1958    /// Statements to run when a matching exception is caught.
1959    pub body: Vec<PlPgSqlStmt>,
1960}
1961
1962/// v7.12.6 — single `DECLARE` entry: variable name + declared
1963/// type + optional initialiser. Variables default to SQL NULL
1964/// when no init is given (matches PG).
1965#[derive(Debug, Clone, PartialEq)]
1966pub struct PlPgSqlDeclare {
1967    pub name: String,
1968    /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
1969    /// knows it; raw text otherwise).
1970    pub ty: FunctionArgType,
1971    pub default: Option<Expr>,
1972}
1973
1974#[derive(Debug, Clone, PartialEq)]
1975pub enum PlPgSqlStmt {
1976    /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
1977    /// for clarity in error reporting (PG also forbids it) — the
1978    /// executor errors with a clear "OLD is read-only" message.
1979    Assign { target: AssignTarget, value: Expr },
1980    /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
1981    /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
1982    /// the SELECT statement with the INTO clause stripped; the
1983    /// engine runs it via `Engine::execute`, takes the first
1984    /// row's first column, and assigns to the local variable
1985    /// in the DECLARE scope. Single-column / single-row
1986    /// queries only at v7.16.2; multi-target (`INTO a, b`) is
1987    /// a v7.16.x follow-up.
1988    SelectInto {
1989        var: String,
1990        body: Box<SelectStatement>,
1991    },
1992    /// `RETURN <target>;` — trigger functions canonically return
1993    /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
1994    /// expression for forward compatibility with scalar UDFs.
1995    Return(ReturnTarget),
1996    /// v7.39 (read01 round 66) — `RETURN NEXT <expr>;`: append one row to the
1997    /// set a SETOF function is building, and KEEP GOING. Not a return.
1998    ReturnNext(Expr),
1999    /// v7.39 (read01 round 66) — `RETURN QUERY <select>;`: append every row the
2000    /// query yields, and keep going. It used to desugar to a side-effect
2001    /// statement whose result was DISCARDED — in a SETOF function that is the
2002    /// whole answer thrown away.
2003    ReturnQuery(Box<SelectStatement>),
2004    /// v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the dynamic
2005    /// twin. Its rows go to the set too; it used to run and discard them.
2006    ReturnQueryExecute { sql: Expr },
2007    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
2008    /// [ELSE body] END IF;`. Branches are tried in order; first
2009    /// truthy condition wins; the optional ELSE runs when no
2010    /// condition matched.
2011    If {
2012        branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
2013        else_branch: Vec<PlPgSqlStmt>,
2014    },
2015    /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
2016    /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
2017    /// (logging — observable side effect only) or `EXCEPTION`
2018    /// (aborts the trigger and propagates as an error). v7.12.6
2019    /// supports the basic format-string substitution PG uses
2020    /// (`%` placeholders consumed positionally).
2021    Raise {
2022        level: RaiseLevel,
2023        message: String,
2024        args: Vec<Expr>,
2025    },
2026    /// v7.12.6 — embedded SQL statement inside the trigger body
2027    /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
2028    /// NEW.col / OLD.col references inside the embedded
2029    /// statement's expression tree are substituted with the
2030    /// current trigger context before the engine re-executes the
2031    /// statement. Recursion depth into nested triggers is
2032    /// bounded by the engine's existing trigger-fire guard.
2033    EmbeddedSql(Box<Statement>),
2034    /// v7.37.20 (20.14) — `ASSERT <condition> [, <message>];`. If
2035    /// the condition evaluates falsy the trigger / DO block aborts
2036    /// with the message (defaulting to a generic shape when none
2037    /// is provided). Same propagation shape as `RAISE EXCEPTION`
2038    /// — the error reaches the caller's query path. PG's behaviour
2039    /// is identical except for a `plpgsql.check_asserts` GUC that
2040    /// can disable the check globally; SPG always evaluates.
2041    Assert {
2042        condition: Expr,
2043        message: Option<Expr>,
2044    },
2045    /// v7.37.20 (20.3) — `WHILE <condition> LOOP <body> END LOOP;`.
2046    /// Iterate the body while condition evaluates truthy. Iteration
2047    /// count is bounded by `WHILE_LOOP_BUDGET` to prevent runaway
2048    /// loops; the executor errors out when reached. EXIT / CONTINUE
2049    /// inside the body queue with 20.2.
2050    While {
2051        condition: Expr,
2052        body: Vec<PlPgSqlStmt>,
2053    },
2054    /// v7.37.20 (20.4) — `FOR <var> IN [REVERSE] <start>..<end> LOOP
2055    /// <body> END LOOP;`. Integer iteration; `var` is BigInt-valued;
2056    /// bounds inclusive on both sides. REVERSE walks backward.
2057    /// Iteration budget guards runaway.
2058    ForRange {
2059        var: String,
2060        start: Expr,
2061        end: Expr,
2062        reverse: bool,
2063        body: Vec<PlPgSqlStmt>,
2064    },
2065    /// v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`. Runs the body
2066    /// repeatedly; only `EXIT [WHEN <cond>]` breaks out. Iteration
2067    /// budget guards runaway.
2068    Loop { body: Vec<PlPgSqlStmt> },
2069    /// v7.37.20 (20.2) — `EXIT [WHEN <condition>];` inside a loop.
2070    /// Unconditional (no WHEN) or conditional (only breaks when
2071    /// condition is truthy). Bubbles up as BodyOutcome::Break which
2072    /// the enclosing loop catches. Outside a loop it's a no-op.
2073    Exit { when: Option<Expr> },
2074    /// v7.37.20 (20.2) — `CONTINUE [WHEN <condition>];` inside a
2075    /// loop. Same shape as EXIT but bubbles up as BodyOutcome::Continue
2076    /// which the enclosing loop catches, skipping the remainder of
2077    /// the body and jumping to the next iteration.
2078    Continue { when: Option<Expr> },
2079    /// v7.37.20 (20.13) — `EXECUTE <string_expr>;` runs a runtime-
2080    /// computed SQL statement. The expression is evaluated to a
2081    /// text value, the resulting string is parsed and dispatched
2082    /// through the engine like an EmbeddedSql. USING <param_list>
2083    /// for placeholder binding queues with v7.40 PL/pgSQL epic.
2084    ExecuteDynamic { sql: Expr },
2085    /// v7.37.20 (20.5) — `FOR <var> IN <select_body> LOOP <body>
2086    /// END LOOP;`. Runs the SELECT once, iterates the resulting
2087    /// rows, binds the first column of each row to `var` as a
2088    /// scalar Value, then runs the body per iteration. EXIT /
2089    /// CONTINUE / ASSERT / RAISE etc. propagate through the
2090    /// enclosing loop's BodyOutcome discipline the same way
2091    /// FOR range and WHILE do. Full record-binding (var as
2092    /// composite carrying all columns) queues with v7.40 record
2093    /// type infrastructure.
2094    ForQuery {
2095        var: String,
2096        query: Box<SelectStatement>,
2097        body: Vec<PlPgSqlStmt>,
2098    },
2099    /// v7.37.20 (20.6) — `FOR <var> IN EXECUTE <string_expr> LOOP
2100    /// <body> END LOOP;`. Same shape as ForQuery but the SELECT is
2101    /// computed at runtime from a text expression, parsed on the
2102    /// fly, then iterated. Enables dynamic queries where the
2103    /// projection / FROM / WHERE clauses depend on runtime values.
2104    ForExecute {
2105        var: String,
2106        sql_expr: Expr,
2107        body: Vec<PlPgSqlStmt>,
2108    },
2109}
2110
2111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2112pub enum RaiseLevel {
2113    /// `RAISE NOTICE` — diagnostic message, observable in the
2114    /// server log. Does not affect the trigger's outcome.
2115    Notice,
2116    /// `RAISE WARNING` — like NOTICE, slightly louder severity.
2117    Warning,
2118    /// `RAISE INFO` — like NOTICE, slightly quieter.
2119    Info,
2120    /// `RAISE LOG` — like NOTICE, lower priority.
2121    Log,
2122    /// `RAISE DEBUG` — like NOTICE, lowest priority.
2123    Debug,
2124    /// `RAISE EXCEPTION` — aborts the trigger function with the
2125    /// given message, propagating up to the caller as a query-
2126    /// level error.
2127    Exception,
2128}
2129
2130#[derive(Debug, Clone, PartialEq)]
2131pub enum AssignTarget {
2132    NewColumn(String),
2133    OldColumn(String),
2134    /// Reserved for v7.12.5 DECLARE'd local variables.
2135    Local(String),
2136}
2137
2138#[derive(Debug, Clone, PartialEq)]
2139pub enum ReturnTarget {
2140    /// `RETURN NEW;` — for BEFORE triggers, this is the row that
2141    /// actually gets written (possibly with NEW.col mutations
2142    /// applied). For AFTER triggers, the return value is ignored.
2143    New,
2144    /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
2145    /// the delete proceed; for BEFORE UPDATE / INSERT it's
2146    /// equivalent to dropping the write.
2147    Old,
2148    /// `RETURN NULL;` — for BEFORE triggers, skips the write
2149    /// entirely. For AFTER, the return value is ignored.
2150    Null,
2151    /// `RETURN <expr>;` — non-row return shape; reserved for the
2152    /// scalar UDF surface in v7.12.5+. Executor errors when used
2153    /// inside a trigger function.
2154    Expr(Expr),
2155}
2156
2157/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
2158/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
2159/// but the executor refuses them. `WHEN (cond)` clauses are out
2160/// of scope; the trigger function can short-circuit on a leading
2161/// IF inside its body once v7.12.5 lands IF.
2162#[derive(Debug, Clone, PartialEq)]
2163pub struct CreateTriggerStatement {
2164    pub name: String,
2165    pub or_replace: bool,
2166    pub timing: TriggerTiming,
2167    /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
2168    /// three entries in order.
2169    pub events: Vec<TriggerEvent>,
2170    pub table: String,
2171    /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
2172    /// only `Row`; `Statement` parses but the executor refuses.
2173    pub for_each: TriggerForEach,
2174    /// Name of the function to invoke. The function must exist at
2175    /// CREATE TRIGGER time — PG18-measured (round 753): PG refuses a
2176    /// forward reference (`function no_such_fn() does not exist`), so
2177    /// requiring it IS the PG behaviour (the old note claimed the
2178    /// opposite).
2179    pub function: String,
2180    /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
2181    /// (mailrs round-5 G7). Non-empty only when the events list
2182    /// contains UPDATE and the user wrote the column-list filter.
2183    /// PG fires the trigger only when at least one of these
2184    /// columns appears in the SET clause; SPG conservatively
2185    /// fires on any UPDATE matching the listed columns or
2186    /// rewriting them at the row level. Empty vec = no filter
2187    /// (fire on every UPDATE).
2188    pub update_columns: Vec<String>,
2189    /// v7.39 (round 138) — `WHEN ( condition )` row-level filter: the row
2190    /// trigger fires only when the condition (over NEW / OLD) is true. `None`
2191    /// = no WHEN (fire unconditionally). Not allowed on INSTEAD OF triggers.
2192    pub when_condition: Option<Expr>,
2193}
2194
2195/// v7.39 (round 139) — `CREATE RULE` query-rewrite rule AST node.
2196#[derive(Debug, Clone, PartialEq)]
2197pub struct CreateRuleStatement {
2198    pub name: String,
2199    pub or_replace: bool,
2200    /// Event keyword, uppercased: `INSERT` / `UPDATE` / `DELETE` / `SELECT`.
2201    pub event: String,
2202    pub table: String,
2203    /// `true` = `DO INSTEAD` (replace the operation), `false` = `DO ALSO`
2204    /// (run alongside; PG's default when neither keyword is written).
2205    pub instead: bool,
2206    /// Optional `WHERE ( condition )` — the rule applies only when it holds.
2207    pub when_condition: Option<Expr>,
2208    /// The `DO` commands (over NEW / OLD). Empty = `NOTHING`.
2209    pub commands: Vec<Statement>,
2210}
2211
2212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2213pub enum TriggerTiming {
2214    /// Fires before the row is written; the trigger function's
2215    /// return value (NEW or NULL) decides the row content and
2216    /// whether the write proceeds at all.
2217    Before,
2218    /// Fires after the row is written; the return value is
2219    /// ignored.
2220    After,
2221    /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
2222    /// v7.12.4 (SPG has no updatable-view surface).
2223    InsteadOf,
2224}
2225
2226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2227pub enum TriggerEvent {
2228    Insert,
2229    Update,
2230    Delete,
2231    /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
2232    /// so the trigger never fires.
2233    Truncate,
2234}
2235
2236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2237pub enum TriggerForEach {
2238    Row,
2239    Statement,
2240}
2241
2242/// v7.39 (round 537) — a `CREATE INDEX` key column's ordering clause.
2243///
2244/// SPG's index does not scan in a direction, but `indexdef` reproduces
2245/// the DDL and dropping this made `(a DESC NULLS LAST)` read back as
2246/// `(a)`. `nulls_first` is `None` when the statement did not say, in
2247/// which case PG's default applies — LAST for ascending, FIRST for
2248/// descending, and neither is rendered.
2249#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2250pub struct IndexColumnOrder {
2251    pub descending: bool,
2252    pub nulls_first: Option<bool>,
2253}
2254
2255#[derive(Debug, Clone, PartialEq)]
2256pub struct CreateIndexStatement {
2257    pub name: String,
2258    /// `CREATE INDEX CONCURRENTLY`. SPG builds indexes synchronously
2259    /// either way, so this changes nothing about how the index is made
2260    /// — it is carried because PG refuses the CONCURRENTLY form inside
2261    /// a transaction block and accepts the plain one, and the engine
2262    /// cannot tell them apart without it.
2263    pub concurrently: bool,
2264    /// v7.39 (round 537) — the leading key column's ordering clause,
2265    /// which is the column SPG indexes.
2266    pub key_order: IndexColumnOrder,
2267    /// v7.39 (round 538) — an explicit `COLLATE` on that key, as
2268    /// written. SPG orders text by bytes, so honouring it changes
2269    /// nothing; PG prints it, because an explicitly named collation and
2270    /// the one a column inherits are different objects.
2271    pub key_collation: Option<String>,
2272    pub table: String,
2273    pub column: String,
2274    /// v7.39 (read01 round 52) — `CREATE UNIQUE INDEX … NULLS NOT DISTINCT`
2275    /// (PG 15+). Default (`false`) is the SQL-standard NULLS DISTINCT, where
2276    /// any NULL in the key exempts the row from the uniqueness check.
2277    pub nulls_not_distinct: bool,
2278    /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
2279    /// graph for vector kNN); unspecified is the default B-tree index.
2280    pub method: IndexMethod,
2281    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2282    /// index name already exists, instead of raising `DuplicateIndex`.
2283    pub if_not_exists: bool,
2284    /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
2285    /// non-key columns the planner should treat as "covered" by
2286    /// this index when checking whether a query can run as an
2287    /// index-only scan. Empty when no `INCLUDE` clause was given.
2288    pub included_columns: Vec<String>,
2289    /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
2290    /// for which `<expr>` evaluates truthy enter the index;
2291    /// queries whose `WHERE` clause's canonical Display form
2292    /// matches this expression's Display form can be served by the
2293    /// partial index. Stored as a parsed `Expr` so the engine
2294    /// re-uses the existing evaluation path; storage persists the
2295    /// Display form on the catalog snapshot.
2296    pub partial_predicate: Option<Expr>,
2297    /// v6.8.2 — expression-based index. When `Some(expr)`, the
2298    /// index key is the result of `expr` evaluated on each row
2299    /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
2300    /// field still names the *primary* column the expression
2301    /// touches so existing planner shortcuts that resolve a
2302    /// column position stay valid. `None` = plain
2303    /// column-reference index (the legacy shape).
2304    pub expression: Option<Expr>,
2305    /// v7.9.14 — extra column names after the leading column in a
2306    /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
2307    /// planner today still only uses the leading column for index
2308    /// seeks; the extras are tracked verbatim so the same DDL
2309    /// round-trips through WAL replay + catalog snapshot, and so
2310    /// the engine can emit a clear warning at INDEX CREATE time
2311    /// that only the leading column is currently honoured.
2312    /// Composite BTree index keys land in v7.10.
2313    pub extra_columns: Vec<String>,
2314    /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
2315    /// enforces uniqueness on the indexed key (combined with the
2316    /// `partial_predicate` filter — only rows where the predicate
2317    /// evaluates truthy enter the uniqueness check). Standard SQL
2318    /// and PG's canonical way to express conditional uniqueness.
2319    /// mailrs K1.
2320    pub is_unique: bool,
2321    /// v7.15.0 — operator class on the leading column, when the
2322    /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
2323    /// Lower-cased. Most opclasses are still informational; the
2324    /// engine routes on `gin_trgm_ops` specifically to build a
2325    /// trigram-shingle GIN over a TEXT column, and otherwise
2326    /// keeps the current "accepted and discarded" behaviour for
2327    /// pg_dump compatibility.
2328    pub opclass: Option<String>,
2329    /// r1038 — the access method as WRITTEN, lower-cased; `None` when
2330    /// there was no `USING` clause.
2331    ///
2332    /// `method` cannot answer this: `gist` / `spgist` / `hash` all become
2333    /// `IndexMethod::BTree` so PG schemas naming an AM SPG has no
2334    /// implementation for still load. That degradation is deliberate, but
2335    /// it loses the name — and the operator-class check needs it, both to
2336    /// look the class up under the AM the user actually named and to say
2337    /// which AM it was missing from, the way PG's message does.
2338    pub method_name: Option<String>,
2339}
2340
2341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2342pub enum IndexMethod {
2343    /// Default — B-tree over `IndexKey`. Used for equality / range
2344    /// lookups on scalar columns.
2345    BTree,
2346    /// `USING hnsw` — NSW graph for kNN over a vector column.
2347    Hnsw,
2348    /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
2349    /// metadata that records (min_key, max_key) for each page in a
2350    /// cold-tier segment, on the indexed column. The optimizer
2351    /// can use these summaries to skip pages whose range does NOT
2352    /// overlap a query's WHERE predicate. BRIN indexes carry no
2353    /// in-memory data — the summaries live in the segment v2
2354    /// envelope's sidecar. Created via the standard
2355    /// `CREATE INDEX … USING brin (col)` syntax.
2356    Brin,
2357    /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
2358    /// column. Posting lists map `lexeme word` → row locators; the
2359    /// planner uses them to narrow `WHERE col @@ tsquery` to the
2360    /// candidate rows whose vectors contain a matching term, then
2361    /// re-evaluates the full `@@` semantics on each candidate.
2362    /// Replaces the v7.9.26b `USING gin` → BTree fallback that
2363    /// silently degraded to a full scan at query time.
2364    Gin,
2365}
2366
2367/// v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING} <opt> ]*`
2368/// inside a CREATE TABLE column list.
2369///
2370/// The source table's shape can only be read from the catalog, so the
2371/// parser records the clause and the engine expands it. `at` is how many
2372/// explicit columns preceded it: PG keeps the written order, so
2373/// `CREATE TABLE k (x int, LIKE t)` puts `x` first.
2374#[derive(Debug, Clone, PartialEq)]
2375pub struct LikeSpec {
2376    pub source: String,
2377    pub at: usize,
2378    pub options: LikeOptions,
2379}
2380
2381/// Which properties `LIKE` carries over. A bare `LIKE` copies names,
2382/// types and NOT NULL and nothing else — measured on PG18, where a
2383/// copied generated column becomes a plain one and a copied identity
2384/// column loses its identity.
2385#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2386pub struct LikeOptions {
2387    pub defaults: bool,
2388    pub constraints: bool,
2389    pub identity: bool,
2390    pub generated: bool,
2391    pub indexes: bool,
2392    pub comments: bool,
2393}
2394
2395#[derive(Debug, Clone, PartialEq)]
2396pub struct CreateTableStatement {
2397    /// v7.39 (round 436) — `CREATE TEMPORARY TABLE`. The table lives in the
2398    /// creating session's own namespace: it shadows a permanent table of the
2399    /// same name, other sessions never see it, and it is dropped when the
2400    /// session ends. A `bool` here lands in the struct's existing padding.
2401    pub temporary: bool,
2402    pub name: String,
2403    /// v7.39 — the `ENGINE=` a MySQL dump names. Consumed and discarded
2404    /// before, so `ENGINE=NONSUCH` built a table where MySQL 9.7.2
2405    /// answers `ERROR 1286`, and `sql_mode` claimed
2406    /// `NO_ENGINE_SUBSTITUTION` while doing it.
2407    pub engine: Option<String>,
2408    pub columns: Vec<ColumnDef>,
2409    /// v7.39 (round 531) — the `LIKE` clauses in the column list, in
2410    /// the order written. Empty for a table that has none.
2411    pub like_specs: Vec<LikeSpec>,
2412    /// v7.39 (round 645) — `CREATE TABLE c (…) INHERITS (p1, p2)`.
2413    /// Empty for a table that inherits from nothing. Order matters:
2414    /// the child takes each parent's columns in this order before its
2415    /// own, and a parent's position here is its `pg_inherits.inhseqno`.
2416    pub inherits: Vec<String>,
2417    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
2418    /// table name already exists, instead of raising `DuplicateTable`.
2419    pub if_not_exists: bool,
2420    /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
2421    /// constraints. Column-level `REFERENCES` (single-column inline
2422    /// form) is normalised into this vec at parse time so the engine
2423    /// sees one uniform list.
2424    pub foreign_keys: Vec<ForeignKeyConstraint>,
2425    /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
2426    /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
2427    /// Engine resolves each into a BTree index named after the
2428    /// constraint's leading column at CREATE TABLE time; INSERT
2429    /// path enforces composite uniqueness via row scan on the
2430    /// leading column index.
2431    pub table_constraints: Vec<TableConstraint>,
2432    /// v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY <strategy>
2433    /// (key_col)` declarative partition-parent suffix. `Some` ⇒
2434    /// the engine creates a parent table whose own rows stay
2435    /// empty and routes INSERT/SELECT through children. Mutually
2436    /// exclusive with `partition_of` (parser enforces).
2437    pub partition_by: Option<PartitionBySpec>,
2438    /// v7.37.6-B — `PARTITION OF <parent> { FOR VALUES FROM (a)
2439    /// TO (b) | DEFAULT }` child-table declaration. `Some` ⇒
2440    /// the table inherits its column list from `parent` (the
2441    /// parser rejects an explicit column list when this is set);
2442    /// engine routes child rows back to the parent at INSERT.
2443    pub partition_of: Option<PartitionOfSpec>,
2444}
2445
2446/// v7.37.6-B — `PARTITION BY <kind> (key_columns…)` parent suffix.
2447/// v7.37.6-B only RANGE is recognised; the enum keeps space for
2448/// future LIST / HASH without breaking the public AST shape.
2449#[derive(Debug, Clone, PartialEq)]
2450pub struct PartitionBySpec {
2451    pub kind: PartitionKindAst,
2452    /// One or more ident references into the parent's column list.
2453    /// v7.37.6-B contracts a single TIMESTAMPTZ key; multi-key
2454    /// RANGE is a phase-2 extension. Parser allows ≥1 to keep the
2455    /// shape PG-compatible.
2456    pub key_columns: Vec<String>,
2457}
2458
2459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2460pub enum PartitionKindAst {
2461    Range,
2462    /// v7.37.16 (16.1) — `PARTITION BY LIST (key)`. Child uses
2463    /// `FOR VALUES IN (lit, lit, …)`.
2464    List,
2465    /// v7.37.16 (16.2) — `PARTITION BY HASH (key)`. Child uses
2466    /// `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2467    Hash,
2468}
2469
2470/// v7.37.6-B — `PARTITION OF <parent> <bounds>` child suffix.
2471/// Bounds is either a half-open range (`FOR VALUES FROM (a) TO (b)`)
2472/// or the catch-all `DEFAULT` partition.
2473#[derive(Debug, Clone, PartialEq)]
2474pub struct PartitionOfSpec {
2475    pub parent_name: String,
2476    pub bounds: PartitionOfBoundsAst,
2477}
2478
2479#[derive(Debug, Clone, PartialEq)]
2480pub enum PartitionOfBoundsAst {
2481    /// `FOR VALUES FROM (lower) TO (upper)`. `Expr` is ~144 bytes
2482    /// (lits include vector bodies), so we box both bounds to keep
2483    /// the variant size in line with `Default` for clippy and to
2484    /// minimise per-statement footprint when the partition shape
2485    /// isn't in use.
2486    Range {
2487        lower: Box<Expr>,
2488        upper: Box<Expr>,
2489    },
2490    /// v7.37.16 (16.1) — `FOR VALUES IN (lit [, lit, …])`. Each
2491    /// expr resolves to a typed literal at child-create time.
2492    List {
2493        values: Vec<Expr>,
2494    },
2495    /// v7.37.16 (16.2) — `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2496    /// PG enforces `0 ≤ r < m`; m must be positive.
2497    Hash {
2498        modulus: u32,
2499        remainder: u32,
2500    },
2501    Default,
2502}
2503
2504/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
2505/// column list. Either a composite PRIMARY KEY or a UNIQUE
2506/// (single- or multi-column).
2507#[derive(Debug, Clone, PartialEq)]
2508pub enum TableConstraint {
2509    /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
2510    /// referenced column. Engine builds a BTree index named
2511    /// `<table>_pkey` and enforces composite uniqueness on INSERT.
2512    PrimaryKey {
2513        name: Option<String>,
2514        columns: Vec<String>,
2515        /// v7.39 (round 711) — `[NOT] DEFERRABLE [INITIALLY DEFERRED]`.
2516        /// Round 621 consumed the clauses; these carry them.
2517        deferrable: bool,
2518        initially_deferred: bool,
2519    },
2520    /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
2521    /// named `<table>_<leading_col>_key` (single-column) or
2522    /// `<table>_<leading_col>_<…>_key` (composite) and enforces
2523    /// uniqueness on INSERT.
2524    Unique {
2525        name: Option<String>,
2526        columns: Vec<String>,
2527        /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
2528        /// G10). PG 15+ flips the NULL handling so any number of
2529        /// NULL rows collide on the constraint. Default is
2530        /// `false` (NULLS DISTINCT, standard SQL behaviour).
2531        nulls_not_distinct: bool,
2532        /// v7.39 (round 711) — see PrimaryKey.
2533        deferrable: bool,
2534        initially_deferred: bool,
2535    },
2536    /// v7.13.0 — `CHECK (<expr>)` table-level constraint
2537    /// (mailrs round-5 G3). Column-level inline CHECKs fold into
2538    /// this same variant at parse time. Engine evaluates the
2539    /// predicate against each INSERT/UPDATE candidate row; a
2540    /// false / NULL result rejects the mutation.
2541    /// v7.39 (round 652) — `not_valid` carries the `NOT VALID` suffix.
2542    /// PG adds such a constraint without scanning the existing rows: new
2543    /// rows are checked, the ones already there are grandfathered in, and
2544    /// `pg_constraint.convalidated` reads `f` until `VALIDATE CONSTRAINT`
2545    /// scans and flips it. pg_dump emits the suffix for exactly those, so
2546    /// validating them on restore would refuse a dump PG itself produced.
2547    Check {
2548        name: Option<String>,
2549        expr: Expr,
2550        not_valid: bool,
2551    },
2552    /// v7.39 (round 210) — `EXCLUDE [USING <method>] (<col> WITH <op>
2553    /// [, …])`: no two rows may satisfy `(r.c1 op1 s.c1) AND …` for
2554    /// every element (the booking/scheduling non-overlap constraint,
2555    /// `EXCLUDE USING gist (during WITH &&)`). `method` is the index
2556    /// AM name (gist/spgist/btree — informational in Phase 0; the O(n)
2557    /// enforcement doesn't build the index yet). Each element pairs a
2558    /// column name with an operator spelling (`&&`, `=`, `@>`, …).
2559    Exclude {
2560        name: Option<String>,
2561        method: Option<String>,
2562        elements: Vec<(String, String)>,
2563    },
2564    /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
2565    /// non-unique secondary-index declaration inline in CREATE
2566    /// TABLE. Engine builds a BTree index on the leading column
2567    /// (composite columns parse but only the leading column is
2568    /// honoured at v7.15 — matches the existing
2569    /// `CreateIndexStatement::extra_columns` semantics). Useful
2570    /// for `mysql/blog`-style schemas that lean on routine
2571    /// secondary indexes for ORM lookups.
2572    Index {
2573        name: Option<String>,
2574        columns: Vec<String>,
2575    },
2576    /// v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY/INDEX [name]
2577    /// (cols)` inline declaration. Pre-v7.17 the parser
2578    /// silently dropped these so MyISAM-imported FULLTEXT
2579    /// indexes vanished; v7.17 routes them through the
2580    /// existing tsvector-GIN engine path so MATCH AGAINST
2581    /// queries get a real inverted index instead of falling
2582    /// back to a full scan. Multi-column FULLTEXT KEYs build
2583    /// one GIN per column at v7.17 (per-column posting lists);
2584    /// the leading column drives query planning.
2585    FulltextIndex {
2586        name: Option<String>,
2587        columns: Vec<String>,
2588    },
2589}
2590
2591#[derive(Debug, Clone, PartialEq)]
2592#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
2593pub struct ColumnDef {
2594    pub name: String,
2595    pub ty: ColumnTypeName,
2596    pub nullable: bool,
2597    /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
2598    /// evaluates this once (with an empty row) and caches the resulting
2599    /// `Value` on the column schema.
2600    pub default: Option<Expr>,
2601    /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
2602    /// per such column and fills the slot when INSERT leaves it
2603    /// unbound (omitted from a column-list INSERT or explicitly NULL).
2604    pub auto_increment: bool,
2605    /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
2606    /// migration follow-up F1. Implies `NOT NULL`. Engine creates
2607    /// an implicit BTree index named `<table>_pkey` over this
2608    /// column at CREATE TABLE time, satisfying the parent-side
2609    /// index requirement for any FOREIGN KEY pointing at it.
2610    pub is_primary_key: bool,
2611    /// v7.13.0 — inline `UNIQUE` column constraint
2612    /// (mailrs round-5 G2). The CREATE TABLE handler folds this
2613    /// into a single-column `TableConstraint::Unique` so the
2614    /// engine path stays uniform with table-level UNIQUE.
2615    pub is_unique: bool,
2616    /// v7.38 (read01 P4.19) — `UNIQUE NULLS NOT DISTINCT` (PG 15+) on the
2617    /// inline column constraint: treat NULL keys as equal so only one NULL
2618    /// is allowed. Ignored unless `is_unique`. Folded into the table-level
2619    /// `TableConstraint::Unique { nulls_not_distinct }`.
2620    pub unique_nulls_not_distinct: bool,
2621    /// v7.39 (round 711) — `DEFERRABLE [INITIALLY DEFERRED]` written on the
2622    /// inline PK/UNIQUE column constraint. Consumed since round 621; carried
2623    /// since this round so the fold into the table-level constraint keeps it.
2624    pub constraint_deferrable: bool,
2625    pub constraint_initially_deferred: bool,
2626    /// v7.13.0 — inline `CHECK (<expr>)` column constraint
2627    /// (mailrs round-5 G3). Stored alongside the column so the
2628    /// CREATE TABLE handler can fold these into table-level
2629    /// CHECK constraints. Multiple inline CHECKs on the same
2630    /// column are concatenated with AND at the table level.
2631    pub check: Option<Expr>,
2632    /// v7.17.0 Phase 1.4 — user-defined type reference. When the
2633    /// parser sees an unknown column-type ident (anything not in
2634    /// the built-in `parse_column_type_name` table), it sets
2635    /// `ty = ColumnTypeName::Text` and records the original name
2636    /// here. The engine resolves at CREATE TABLE time: if a
2637    /// catalog enum/domain with this name exists, the column is
2638    /// bound to it (label-checked on INSERT for enums; CHECK-
2639    /// constrained for domains); otherwise the CREATE TABLE
2640    /// errors with "unknown type".
2641    pub user_type_ref: Option<String>,
2642    /// v7.17.0 Phase 2.1 — MySQL-style `ON UPDATE
2643    /// CURRENT_TIMESTAMP` column attribute. When set, an
2644    /// UPDATE that does NOT explicitly bind this column
2645    /// overrides the new value with `now()` (engine clock).
2646    /// Pre-v7.17 SPG silently accepted the syntax and never
2647    /// fired the override — `updated_at` columns from mysqldump
2648    /// stayed pinned at their initial DEFAULT forever, an
2649    /// audit Tier-S silent-failure. Generalised as a stored
2650    /// expression source so future shapes (`ON UPDATE
2651    /// CURRENT_TIMESTAMP(6)`, `ON UPDATE LOCALTIMESTAMP`) reuse
2652    /// the same field; v7.17 only accepts CURRENT_TIMESTAMP.
2653    pub on_update_runtime: Option<Expr>,
2654    /// v7.17.0 Phase 2.5 — text collation derived from the
2655    /// post-fix `COLLATE <name>` clause (and / or the table-level
2656    /// `COLLATE=<name>` for MySQL dumps that don't repeat it
2657    /// per column). Pre-2.5 SPG accepted the clause and
2658    /// discarded the name, leaving every column byte-compared
2659    /// — a Tier-S silent failure when the customer expected
2660    /// `_ci` / `case_insensitive` semantics. Parser normalises
2661    /// the raw collation name into the variants in `Collation`.
2662    /// Default `Binary` preserves the legacy compare path.
2663    pub collation: Collation,
2664    /// v7.39 (round 370, M4 P4a) — whether `collation` came from an
2665    /// explicit `COLLATE <name>` clause rather than the default. Under the
2666    /// MySQL dialect a text column with NO explicit clause takes the
2667    /// folding default collation, while an explicit `COLLATE utf8mb4_bin`
2668    /// stays byte-wise — and both resolve to `Collation::Binary`, so this
2669    /// flag is the only thing that tells them apart.
2670    pub collation_explicit: bool,
2671    /// v7.39 (round 676) — the collation name AS WRITTEN, because
2672    /// `collation` above cannot carry it: `Collation` is a two-variant
2673    /// MySQL enum and `from_collation_name` folds `C`, `POSIX`, `en_US` and
2674    /// `default` all into `Binary`. `pg_attribute.attcollation` needs to
2675    /// tell them apart.
2676    pub collation_name: Option<String>,
2677    /// v7.17.0 Phase 4.4 — MySQL `UNSIGNED` modifier flag. Pre-
2678    /// 4.4 SPG accepted and discarded the keyword, leaving
2679    /// negative values silently accepted on a column the
2680    /// customer declared `INT UNSIGNED NOT NULL`. Now: the engine
2681    /// rejects negative INSERT / UPDATE values on UNSIGNED int
2682    /// columns. SPG widening to `u64`-shaped storage is out of
2683    /// v7.17 scope; the upper bound remains the signed-type max
2684    /// (i64::MAX for BIGINT UNSIGNED), which still strictly
2685    /// exceeds what every mailrs / Rails app actually uses.
2686    pub is_unsigned: bool,
2687    /// v7.17.0 Phase 3.P0-36 — MySQL inline `ENUM('a','b','c')`
2688    /// value list captured at parse time. When `Some`, the parser
2689    /// recognised `ENUM(...)` in the type slot; the engine
2690    /// validates INSERT cells against this list at
2691    /// column_def_to_schema time and persists the variants on
2692    /// `ColumnSchema.inline_enum_variants`. None for all
2693    /// non-ENUM columns.
2694    pub inline_enum_variants: Option<Vec<String>>,
2695    /// v7.17.0 Phase 3.P0-37 — MySQL inline `SET('a','b','c')`
2696    /// value list. Distinct from ENUM (subset semantics rather
2697    /// than pick-one). None for all non-SET columns.
2698    pub inline_set_variants: Option<Vec<String>>,
2699    /// v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS (<expr>)
2700    /// STORED` computed-column source. When `Some`, the engine
2701    /// stores the Display-form of the parsed expression on
2702    /// `ColumnSchema.generated_stored_expr` at CREATE TABLE time
2703    /// and re-evaluates the expression against every INSERT /
2704    /// UPDATE candidate row, overwriting whatever the caller
2705    /// supplied for this column. Boxed to keep `ColumnDef` from
2706    /// blowing past the `large_enum_variant` clippy ceiling
2707    /// (`Expr` widens with vector literals).
2708    pub generated_stored_expr: Option<Box<Expr>>,
2709    /// v7.38 (read01) — `GENERATED ALWAYS AS IDENTITY` (as opposed to
2710    /// `GENERATED BY DEFAULT AS IDENTITY`). Both flavours set
2711    /// `auto_increment`; this additionally marks the ALWAYS one, whose
2712    /// explicit INSERT value PG rejects ("cannot insert a non-DEFAULT
2713    /// value into column …") unless the INSERT carries `OVERRIDING SYSTEM
2714    /// VALUE`. Only meaningful when the column is also an identity column.
2715    pub identity_always: bool,
2716    /// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2717    /// integer width (TINYINT / MEDIUMINT), captured before the type
2718    /// collapses to SmallInt / Int. The engine copies it to
2719    /// `ColumnSchema.mysql_int_width` at CREATE TABLE time so the write
2720    /// path can enforce the real range. None for every other column and
2721    /// under the PG dialect.
2722    pub mysql_int_width: Option<MysqlIntWidth>,
2723    /// v7.39 (round 424, type-fidelity epic) — the declared MySQL
2724    /// fractional-seconds precision of a temporal column (`DATETIME(3)` is
2725    /// `Some(3)`; a bare `DATETIME` / `TIME` / `TIMESTAMP` is `Some(0)`,
2726    /// MySQL's default). The engine copies it to `ColumnSchema.mysql_fsp` at
2727    /// CREATE TABLE time so the write path can truncate and the render path
2728    /// can pad. None under the PG dialect, where temporal columns keep full
2729    /// microseconds.
2730    pub mysql_fsp: Option<u8>,
2731}
2732
2733/// v7.17.0 Phase 2.5 — text collation classification surfaced
2734/// from the SQL parser. Mirrors `spg_storage::Collation`; the
2735/// engine bridges between the two at CREATE TABLE time.
2736///
2737/// Recognised collation-name patterns (case-insensitive):
2738///   * `case_insensitive`, `*_ci`, `*_ai_ci`, `nocase`         → CaseInsensitive
2739///   * Everything else (`C`, `POSIX`, `default`,
2740///     `pg_catalog.default`, `*_cs`, `*_bin`, unknown names)   → Binary
2741#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2742pub enum Collation {
2743    Binary,
2744    CaseInsensitive,
2745}
2746
2747/// v7.39 (round 386, type-fidelity epic P1) — the declared MySQL narrow
2748/// integer width for a column whose `ColumnTypeName` is too wide to carry
2749/// it: `TINYINT` collapses to `SmallInt`, `MEDIUMINT` to `Int`. Mirrors
2750/// `spg_storage::MysqlIntWidth`; the engine bridges the two at CREATE
2751/// TABLE time. Only recorded under the MySQL dialect.
2752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2753pub enum MysqlIntWidth {
2754    Tiny,
2755    Small,
2756    Medium,
2757    Int,
2758    /// v7.39 (round 471, epic P4b) — `BIGINT UNSIGNED`.
2759    Big,
2760}
2761
2762#[allow(clippy::derivable_impls)]
2763impl Default for Collation {
2764    fn default() -> Self {
2765        Self::Binary
2766    }
2767}
2768
2769impl Collation {
2770    /// Classify a `COLLATE <name>` ident into one of the supported
2771    /// variants. Empty / unknown names fall back to `Binary` —
2772    /// matches the pre-2.5 silent-accept behaviour for snapshots
2773    /// that load through but don't actually depend on the
2774    /// collation semantics.
2775    #[must_use]
2776    pub fn from_collation_name(name: &str) -> Self {
2777        let lc = name.trim().to_ascii_lowercase();
2778        // Strip any quotes / schema-qualifier the parser left on
2779        // (e.g. `pg_catalog.default`).
2780        let bare = lc
2781            .trim_matches(|c: char| c == '"' || c == '\'')
2782            .rsplit('.')
2783            .next()
2784            .unwrap_or("");
2785        if bare.is_empty() {
2786            return Self::Binary;
2787        }
2788        if bare == "case_insensitive" || bare == "nocase" {
2789            return Self::CaseInsensitive;
2790        }
2791        // MySQL `_ci` suffix (covers `utf8mb4_general_ci`,
2792        // `utf8mb4_unicode_ci`, `utf8mb4_0900_ai_ci`, …).
2793        if bare.ends_with("_ci") {
2794            return Self::CaseInsensitive;
2795        }
2796        Self::Binary
2797    }
2798}
2799
2800/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
2801/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
2802/// parse into this shape — the column-level form has a single-entry
2803/// `columns` / `parent_columns`.
2804#[derive(Debug, Clone, PartialEq)]
2805pub struct ForeignKeyConstraint {
2806    /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
2807    /// today but parses + stores it so a future ALTER TABLE DROP
2808    /// CONSTRAINT can target by name (v7.6.8).
2809    pub name: Option<String>,
2810    /// Local columns participating in the FK (≥ 1).
2811    pub columns: Vec<String>,
2812    /// Referenced parent table.
2813    pub parent_table: String,
2814    /// Referenced parent columns. Must have the same arity as
2815    /// `columns`; engine validates parent has a PK / UNIQUE index
2816    /// on exactly this column set (v7.6.1).
2817    pub parent_columns: Vec<String>,
2818    /// `ON DELETE` action. Defaults to `Restrict` if absent.
2819    pub on_delete: FkAction,
2820    /// `ON UPDATE` action. Defaults to `Restrict` if absent.
2821    pub on_update: FkAction,
2822    /// v7.38 (read01, T29) — `MATCH {SIMPLE | FULL}`. Defaults to `Simple`.
2823    pub match_type: MatchType,
2824    /// v7.39 (round 288) — `[NOT] DEFERRABLE`. Parsed since v7.17 and
2825    /// dropped on the floor, so a constraint declared DEFERRABLE was
2826    /// enforced immediately and a circular-FK migration could not load.
2827    pub deferrable: bool,
2828    /// `INITIALLY DEFERRED` — the check moves to COMMIT unless
2829    /// `SET CONSTRAINTS … IMMEDIATE` pulls it forward.
2830    pub initially_deferred: bool,
2831}
2832
2833/// v7.38 (read01, T29) — FK `MATCH` type. SIMPLE (default) skips the check when
2834/// ANY referencing column is NULL; FULL requires all-or-none NULL (a mixed-NULL
2835/// key errors). PARTIAL is parse-rejected (PG does not implement it either).
2836#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2837pub enum MatchType {
2838    #[default]
2839    Simple,
2840    Full,
2841}
2842
2843/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
2844#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2845pub enum FkAction {
2846    /// Reject the parent mutation if any child row references it.
2847    /// SQL spec default; SPG default when no clause is given.
2848    Restrict,
2849    /// Recursively propagate the parent's delete / update to the
2850    /// child rows. Same TX.
2851    Cascade,
2852    /// Set the child FK column(s) to NULL. Requires the FK columns
2853    /// to be NULL-able.
2854    SetNull,
2855    /// Set the child FK column(s) to their declared DEFAULT.
2856    /// Requires the child column(s) to have DEFAULT.
2857    SetDefault,
2858    /// SQL spec `NO ACTION` (deferred check). SPG treats this as
2859    /// `Restrict` because the single-writer model has no deferred
2860    /// constraint window; the keyword is accepted for compatibility.
2861    NoAction,
2862}
2863
2864/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
2865/// optional `USING <encoding>` clause; omitting it keeps the
2866/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
2867/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
2868/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
2869/// binary16 (2× compression, ~3 decimal digits of precision).
2870#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2871pub enum VecEncoding {
2872    /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
2873    /// uncompressed `vector` type wire / storage layout.
2874    #[default]
2875    F32,
2876    /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
2877    /// `spg_storage::quantize::Sq8Vector` for the math + recall
2878    /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
2879    /// dim ≥ 32).
2880    Sq8,
2881    /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
2882    /// per-element. DDL keyword `HALF` (pgvector convention).
2883    /// Bit-exact dequantise to f32 at the storage layer; no
2884    /// rerank pass needed for kNN search.
2885    F16,
2886}
2887
2888impl fmt::Display for VecEncoding {
2889    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2890        match self {
2891            Self::F32 => f.write_str("F32"),
2892            Self::Sq8 => f.write_str("SQ8"),
2893            // pgvector convention: DDL keyword is `HALF`, not `F16`.
2894            Self::F16 => f.write_str("HALF"),
2895        }
2896    }
2897}
2898
2899/// SQL-level type names. The mapping to the storage runtime's `DataType`
2900/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
2901#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2902pub enum ColumnTypeName {
2903    /// v7.39 (round 291) — PG's `name`, the identifier type its
2904    /// catalogs use. `CREATE TABLE t (a name)` is legal SQL that SPG
2905    /// answered `type "name" does not exist` to.
2906    Name,
2907    /// v7.39 (round 640) — PG's transaction-id types. `xid` is the
2908    /// 32-bit wrapping counter the row header carries; `xid8` is the
2909    /// 64-bit monotonic one. `CREATE TABLE t (a xid)` is legal SQL that
2910    /// SPG answered `type "xid" does not exist` to.
2911    Xid,
2912    Xid8,
2913    /// v7.39 (round 667) — `OID`. `XID` was already a column type here
2914    /// and `OID` was not, so `CREATE TABLE t(o OID)` answered
2915    /// `type "oid" does not exist` while `t(x XID)` built fine.
2916    Oid,
2917    SmallInt,
2918    Int,
2919    BigInt,
2920    Float,
2921    /// v7.39 (round 269) — `REAL` / `FLOAT4` / `FLOAT(1..24)`: 32-bit
2922    /// IEEE. It used to map to [`Self::Float`] on the theory that a
2923    /// wider float is harmless, but the width is observable: a `real`
2924    /// column holding 0.1 stored the f64 0.1, so `r = 0.1::real`
2925    /// answered false where PG answers true.
2926    Real,
2927    Text,
2928    /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
2929    Varchar(u32),
2930    /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
2931    Char(u32),
2932    Bool,
2933    /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
2934    /// `USING <encoding>` clause; omitting it surfaces as
2935    /// `encoding = VecEncoding::F32` (the pre-v6 default).
2936    Vector {
2937        dim: u32,
2938        encoding: VecEncoding,
2939    },
2940    /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
2941    /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
2942    /// v7.39 (round 271) — scale widened to u16 alongside the value's.
2943    /// v7.39 (round 272) — precision too: PG's runs to 1000.
2944    /// v7.39 (round 273) — the DECLARED scale is signed (-1000..=1000);
2945    /// a negative one rounds to tens / hundreds. A VALUE's display scale
2946    /// stays unsigned.
2947    Numeric(u16, i16),
2948    /// `DATE` — calendar day, no time-of-day component.
2949    Date,
2950    /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
2951    /// precision.
2952    Timestamp,
2953    /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
2954    /// stores all timestamps as UTC microseconds-since-epoch and
2955    /// does not carry per-row offset (PG's internal representation
2956    /// is the same — TZ is a display convention). The distinction
2957    /// from `TIMESTAMP` exists for the PG-wire layer to advertise
2958    /// OID 1184 so sqlx-style clients decode into
2959    /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
2960    Timestamptz,
2961    /// v4.9 `JSON` — text-backed JSON document. No parse-time
2962    /// validation; the engine round-trips the literal verbatim.
2963    /// PG OID 114 on the wire.
2964    Json,
2965    /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
2966    /// PG OID 3802 on the wire so sqlx-style binary-typed clients
2967    /// decode without a custom type registration.
2968    Jsonb,
2969    /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
2970    /// Literal forms (decoded by the engine at coercion time):
2971    ///   - PG hex form: `'\xDEADBEEF'`
2972    ///   - Escape form: `'foo\\000bar'` (backslash octal triples)
2973    Bytes,
2974    /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
2975    /// OID 1009. Literal forms accepted by the parser:
2976    ///   - `ARRAY['a', 'b', NULL]`
2977    ///   - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
2978    ///     form at coerce time)
2979    TextArray,
2980    /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
2981    /// 1007. Same literal forms as TEXT[] (substituting integer
2982    /// elements).
2983    IntArray,
2984    /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
2985    /// OID 1016.
2986    BigIntArray,
2987    /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
2988    /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
2989    /// external form). G-CRIT-3.
2990    TsVector,
2991    /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
2992    /// wire OID 3615.
2993    TsQuery,
2994    /// v7.17.0 `UUID` — 128-bit identifier. PG wire OID 2950.
2995    /// Literal input accepts canonical hyphenated, unhyphenated,
2996    /// uppercase, and `{...}`-braced forms; display normalises to
2997    /// canonical lowercase 8-4-4-4-12. The drop-in PG surface for
2998    /// Django / Rails / Hibernate `id UUID PRIMARY KEY DEFAULT
2999    /// gen_random_uuid()`.
3000    Uuid,
3001    /// v7.17.0 Phase 3.P0-32 `TIME` (without time zone) — i64
3002    /// microseconds since 00:00:00. PG wire OID 1083. Literal
3003    /// input is `'HH:MM:SS'` with an optional `.fraction` suffix
3004    /// (6-digit microsecond precision). Display normalises to
3005    /// the canonical `HH:MM:SS[.ffffff]`.
3006    Time,
3007    /// v7.17.0 Phase 3.P0-33 MySQL `YEAR` — u16 in range
3008    /// 1901..=2155 plus the zero-year sentinel 0. No dedicated
3009    /// PG OID; advertised as INT4 on the wire. Display always
3010    /// 4 digits zero-padded.
3011    Year,
3012    /// v7.17.0 Phase 3.P0-34 PG `TIME WITH TIME ZONE` (TIMETZ) —
3013    /// i64 us since 00:00:00 (local) + i32 offset_secs from UTC.
3014    /// Wire OID 1266. Literal input is `'HH:MM:SS[.ffffff]±HH[:MM]'`.
3015    /// Offset range: ±14 hours.
3016    TimeTz,
3017    /// v7.17.0 Phase 3.P0-35 PG `MONEY` — i64 cents
3018    /// (locale-independent storage). Wire OID 790. Literal input
3019    /// accepts `$N.NN`, `$N,NNN.NN`, bare integer (treated as
3020    /// major units), optional leading `-`. Display: en_US locale.
3021    Money,
3022    /// v7.17.0 Phase 3.P0-38 PG range types. Pair stores the
3023    /// element kind tag (Int4 / Int8 / Num / Ts / TsTz / Date)
3024    /// — the engine bridges to `DataType::Range(RangeKind)`.
3025    Range(RangeKindAst),
3026    /// v7.17.0 Phase 3.P0-39 PG `hstore` extension type — flat
3027    /// `text => text` map with NULL value support.
3028    Hstore,
3029    /// v7.17.0 Phase 3.P0-40 — 2D arrays for INT / TEXT / BIGINT.
3030    IntArray2D,
3031    BigIntArray2D,
3032    TextArray2D,
3033    /// v7.39 (read01 round 75) — `bool[][]`.
3034    BoolArray2D,
3035    /// v7.37.5 β-P2 — `INTERVAL` as a column type. Storage is the
3036    /// three-field {months, days, micros} struct (PG-byte-equal),
3037    /// catalog tag 34, FILE_VERSION 48+. Wire OID 1186. Prior to
3038    /// β-P2 `INTERVAL` was runtime-only — literal in expression
3039    /// position but rejected at CREATE TABLE.
3040    Interval,
3041    /// v7.37.5 β-P4 — `INTERVAL[]` — single-dimension array of
3042    /// INTERVAL. Wire OID 1187 (`_interval`). Catalog tag 35.
3043    /// PG external form quotes each non-NULL element because
3044    /// interval text contains spaces / colons
3045    /// (`{"1 day","24:00:00",NULL}`).
3046    IntervalArray,
3047    /// v7.37.5 γ — full PG array-of-scalar family. Each variant
3048    /// mirrors a scalar `ColumnTypeName` that already existed.
3049    BoolArray,
3050    SmallIntArray,
3051    FloatArray,
3052    NumericArray,
3053    DateArray,
3054    TimestampArray,
3055    TimestamptzArray,
3056    UuidArray,
3057    JsonArray,
3058    JsonbArray,
3059    BytesArray,
3060    VarcharArray,
3061    CharArray,
3062    /// v7.37.5 δ — PG 14+ multirange types. Same wrapper pattern
3063    /// as `Range(RangeKindAst)` — one column type variant covers
3064    /// all six builtin multiranges, kind pins the element type.
3065    /// Wire OIDs in pgwire.
3066    Multirange(RangeKindAst),
3067    /// v7.37.5 ε — PG geometry scalar family. Each maps one-to-
3068    /// one to a PG type: point/lseg/path/box/polygon/line/circle.
3069    /// Wire OIDs in pgwire.
3070    Point,
3071    Lseg,
3072    Path,
3073    PgBox,
3074    Polygon,
3075    Line,
3076    Circle,
3077    /// v7.37.5 ζ-A — PG network / bit / xml / "char" / money[].
3078    Inet,
3079    Cidr,
3080    Macaddr,
3081    Macaddr8,
3082    /// v7.39 (round 281) — `BIT(n)`; `0` = no typmod (PG: `bit(1)`).
3083    Bit(u32),
3084    /// v7.39 (round 281) — `BIT VARYING(n)`; `0` = unbounded.
3085    BitVarying(u32),
3086    Xml,
3087    Char1,
3088    MoneyArray,
3089}
3090
3091/// v7.17.0 Phase 3.P0-38 — PG range element kind. Mirrors
3092/// `spg_storage::RangeKind`; we keep it spg-sql-local so the AST
3093/// crate doesn't depend on storage. Bridged at engine boundary.
3094#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3095pub enum RangeKindAst {
3096    Int4,
3097    Int8,
3098    Num,
3099    Ts,
3100    TsTz,
3101    Date,
3102}
3103
3104impl fmt::Display for ColumnTypeName {
3105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3106        match self {
3107            Self::SmallInt => f.write_str("SMALLINT"),
3108            Self::Int => f.write_str("INT"),
3109            Self::BigInt => f.write_str("BIGINT"),
3110            Self::Float => f.write_str("FLOAT"),
3111            Self::Real => f.write_str("REAL"),
3112            Self::Text => f.write_str("TEXT"),
3113            Self::Name => f.write_str("name"),
3114            Self::Xid => f.write_str("xid"),
3115            Self::Xid8 => f.write_str("xid8"),
3116            Self::Oid => f.write_str("oid"),
3117            Self::Varchar(n) => write!(f, "VARCHAR({n})"),
3118            Self::Char(n) => write!(f, "CHAR({n})"),
3119            Self::Bool => f.write_str("BOOL"),
3120            Self::Vector { dim, encoding } => match encoding {
3121                VecEncoding::F32 => write!(f, "VECTOR({dim})"),
3122                VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
3123                VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
3124            },
3125            Self::Json => f.write_str("JSON"),
3126            Self::Jsonb => f.write_str("JSONB"),
3127            Self::Bytes => f.write_str("BYTEA"),
3128            Self::TextArray => f.write_str("TEXT[]"),
3129            Self::IntArray => f.write_str("INT[]"),
3130            Self::BigIntArray => f.write_str("BIGINT[]"),
3131            Self::TsVector => f.write_str("TSVECTOR"),
3132            Self::TsQuery => f.write_str("TSQUERY"),
3133            Self::Uuid => f.write_str("UUID"),
3134            Self::Numeric(p, s) => {
3135                if *s == 0 {
3136                    write!(f, "NUMERIC({p})")
3137                } else {
3138                    write!(f, "NUMERIC({p}, {s})")
3139                }
3140            }
3141            Self::Date => f.write_str("DATE"),
3142            Self::Timestamp => f.write_str("TIMESTAMP"),
3143            Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
3144            Self::Time => f.write_str("TIME"),
3145            Self::Year => f.write_str("YEAR"),
3146            Self::TimeTz => f.write_str("TIMETZ"),
3147            Self::Money => f.write_str("MONEY"),
3148            Self::Range(k) => f.write_str(match k {
3149                RangeKindAst::Int4 => "INT4RANGE",
3150                RangeKindAst::Int8 => "INT8RANGE",
3151                RangeKindAst::Num => "NUMRANGE",
3152                RangeKindAst::Ts => "TSRANGE",
3153                RangeKindAst::TsTz => "TSTZRANGE",
3154                RangeKindAst::Date => "DATERANGE",
3155            }),
3156            Self::Hstore => f.write_str("HSTORE"),
3157            Self::Interval => f.write_str("INTERVAL"),
3158            Self::IntervalArray => f.write_str("INTERVAL[]"),
3159            Self::BoolArray => f.write_str("BOOL[]"),
3160            Self::SmallIntArray => f.write_str("SMALLINT[]"),
3161            Self::FloatArray => f.write_str("FLOAT[]"),
3162            Self::NumericArray => f.write_str("NUMERIC[]"),
3163            Self::DateArray => f.write_str("DATE[]"),
3164            Self::TimestampArray => f.write_str("TIMESTAMP[]"),
3165            Self::TimestamptzArray => f.write_str("TIMESTAMPTZ[]"),
3166            Self::UuidArray => f.write_str("UUID[]"),
3167            Self::JsonArray => f.write_str("JSON[]"),
3168            Self::JsonbArray => f.write_str("JSONB[]"),
3169            Self::BytesArray => f.write_str("BYTEA[]"),
3170            Self::VarcharArray => f.write_str("VARCHAR[]"),
3171            Self::CharArray => f.write_str("CHAR[]"),
3172            Self::Multirange(k) => f.write_str(match k {
3173                RangeKindAst::Int4 => "INT4MULTIRANGE",
3174                RangeKindAst::Int8 => "INT8MULTIRANGE",
3175                RangeKindAst::Num => "NUMMULTIRANGE",
3176                RangeKindAst::Ts => "TSMULTIRANGE",
3177                RangeKindAst::TsTz => "TSTZMULTIRANGE",
3178                RangeKindAst::Date => "DATEMULTIRANGE",
3179            }),
3180            Self::Point => f.write_str("POINT"),
3181            Self::Lseg => f.write_str("LSEG"),
3182            Self::Path => f.write_str("PATH"),
3183            Self::PgBox => f.write_str("BOX"),
3184            Self::Polygon => f.write_str("POLYGON"),
3185            Self::Line => f.write_str("LINE"),
3186            Self::Circle => f.write_str("CIRCLE"),
3187            Self::Inet => f.write_str("INET"),
3188            Self::Cidr => f.write_str("CIDR"),
3189            Self::Macaddr => f.write_str("MACADDR"),
3190            Self::Macaddr8 => f.write_str("MACADDR8"),
3191            Self::Bit(0) => f.write_str("BIT"),
3192            Self::Bit(n) => write!(f, "BIT({n})"),
3193            Self::BitVarying(0) => f.write_str("VARBIT"),
3194            Self::BitVarying(n) => write!(f, "VARBIT({n})"),
3195            Self::Xml => f.write_str("XML"),
3196            Self::Char1 => f.write_str("\"char\""),
3197            Self::MoneyArray => f.write_str("MONEY[]"),
3198            Self::IntArray2D => f.write_str("INT[][]"),
3199            Self::BigIntArray2D => f.write_str("BIGINT[][]"),
3200            Self::TextArray2D => f.write_str("TEXT[][]"),
3201            Self::BoolArray2D => f.write_str("BOOL[][]"),
3202        }
3203    }
3204}
3205
3206/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
3207/// engine evaluates `expr` per matched row in the table's row order
3208/// and rewrites cells in place. Indexed columns are dropped + re-
3209/// inserted into the affected B-tree on each row change.
3210/// v7.39 (round 413) — the boxed payload for MySQL's `ORDER BY [LIMIT]`
3211/// tail on a DML statement. Boxed off the statement struct so the PG-only
3212/// common path stays at its pre-r413 size (see the round-305 nesting-stack
3213/// lesson). v7.39 (round 431+1) — DELETE carries the identical clause with
3214/// the identical meaning, so both share this one payload rather than each
3215/// growing its own.
3216#[derive(Debug, Clone, PartialEq)]
3217pub struct DmlOrderLimit {
3218    pub order_by: Vec<OrderBy>,
3219    pub limit: Option<u32>,
3220}
3221
3222/// v7.39 (round 533) — what `UPDATE … FROM src WHERE cond` was lowered
3223/// FROM, kept so the engine can finish the job.
3224///
3225/// The parser rewrites the statement onto correlated subqueries, and it
3226/// can only classify a QUALIFIED leaf: deciding whether an unqualified
3227/// name belongs to the target or to a source needs their column lists,
3228/// which parse time does not have. Carrying the clause lets the engine
3229/// — which has the catalog — resolve the rest.
3230#[derive(Debug, Clone, PartialEq)]
3231pub struct UpdateFromSources {
3232    pub from: FromClause,
3233    pub sub_where: Option<Expr>,
3234}
3235
3236#[derive(Debug, Clone, PartialEq)]
3237pub struct UpdateStatement {
3238    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3239    /// level UPDATE. Empty for a plain UPDATE.
3240    pub ctes: Vec<Cte>,
3241    pub table: String,
3242    /// v7.39 (round 646) — `UPDATE ONLY t` / `DELETE FROM ONLY t`: apply
3243    /// to `t`'s own rows and not to anything that descends from it.
3244    ///
3245    /// Round 644 taught the FROM clause the keyword and left DML behind
3246    /// because it needed a field here, and this struct carries a warning
3247    /// that round 413 measured widening it in place overflowing the
3248    /// parser's nesting stack. That warning was about `from_sources`, a
3249    /// struct wide enough to need boxing; a `bool` lands in the padding
3250    /// already present — same as `CreateTableStatement::temporary`.
3251    ///
3252    /// It also earns its keep beyond the spelling: the inheritance
3253    /// fan-out needs a way to say "the parent's own rows" as a
3254    /// statement, or running one on the parent recurses forever.
3255    pub only: bool,
3256    /// v7.39 (round 241) — `UPDATE t [AS] alias SET …`: the name the
3257    /// statement's expressions refer to the target row by. PG allows the
3258    /// bare spelling here (unlike INSERT, which requires AS).
3259    pub alias: Option<String>,
3260    pub assignments: Vec<(String, Expr)>,
3261    /// v7.39 (round 533) — boxed: round 413 measured that widening this
3262    /// struct in place overflows the parser's nesting stack.
3263    pub from_sources: Option<alloc::boxed::Box<UpdateFromSources>>,
3264    pub where_: Option<Expr>,
3265    /// v7.39 (round 413) — MySQL's `UPDATE … [ORDER BY … [LIMIT n]]`:
3266    /// mutate the first `limit` rows in the given order. PG has no such
3267    /// clause; the parser accepts it only under the MySQL dialect. Boxed
3268    /// so a PG UPDATE (the common case) grows this struct by ONE pointer,
3269    /// not `Vec<OrderBy> + Option<u32>` — a naked add tipped the parser's
3270    /// 512 KiB nesting stack under a full workspace test (round 305 kin).
3271    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3272    /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
3273    /// clause (legacy CommandComplete path). Some = engine
3274    /// evaluates the projection over each mutated row and
3275    /// streams the result as a Rows QueryResult.
3276    pub returning: Option<Vec<SelectItem>>,
3277}
3278
3279/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
3280/// from the active catalog and prunes them from every index.
3281#[derive(Debug, Clone, PartialEq)]
3282pub struct DeleteStatement {
3283    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3284    /// level DELETE. Empty for a plain DELETE.
3285    pub ctes: Vec<Cte>,
3286    pub table: String,
3287    /// v7.39 (round 646) — `DELETE FROM ONLY t`, the sibling of `UpdateStatement::only`: apply
3288    /// to `t`'s own rows and not to anything that descends from it.
3289    ///
3290    /// Round 644 taught the FROM clause the keyword and left DML behind
3291    /// because it needed a field here, and this struct carries a warning
3292    /// that round 413 measured widening it in place overflowing the
3293    /// parser's nesting stack. That warning was about `from_sources`, a
3294    /// struct wide enough to need boxing; a `bool` lands in the padding
3295    /// already present — same as `CreateTableStatement::temporary`.
3296    ///
3297    /// It also earns its keep beyond the spelling: the inheritance
3298    /// fan-out needs a way to say "the parent's own rows" as a
3299    /// statement, or running one on the parent recurses forever.
3300    pub only: bool,
3301    /// v7.39 (round 241) — `DELETE FROM t [AS] alias USING …`: the name
3302    /// the WHERE / RETURNING expressions refer to the target row by.
3303    pub alias: Option<String>,
3304    pub where_: Option<Expr>,
3305    /// v7.39 (round 432) — MySQL's `DELETE … [ORDER BY … [LIMIT n]]`, the
3306    /// batched-cleanup idiom. Same clause and same meaning as the UPDATE
3307    /// form (round 413), so it shares that payload — and it is boxed for
3308    /// the same reason: a naked `Vec<OrderBy> + Option<u32>` on a DML
3309    /// statement tipped the parser's 512 KiB nesting stack.
3310    pub order_limit: Option<alloc::boxed::Box<DmlOrderLimit>>,
3311    /// v7.9.4 — `RETURNING <projection>`.
3312    pub returning: Option<Vec<SelectItem>>,
3313}
3314
3315/// v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE statement.
3316/// One WHEN clause fires per source row depending on whether the
3317/// `on` condition matched any target row(s); the executor walks
3318/// `clauses` in declaration order and fires the first whose
3319/// `matched` kind and optional `condition` are both satisfied.
3320#[derive(Debug, Clone, PartialEq)]
3321pub struct MergeStatement {
3322    /// v7.39 (read01 round 149) — leading `WITH <cte> [, …]` (PG 15 allows
3323    /// a WITH clause on MERGE; `WITH RECURSIVE` is rejected at parse, as
3324    /// in PG). Each CTE materialises before the merge runs and its alias
3325    /// resolves as a source relation.
3326    pub ctes: Vec<Cte>,
3327    pub target: String,
3328    pub target_alias: Option<String>,
3329    pub source: String,
3330    pub source_alias: Option<String>,
3331    /// v7.37 D.44 — `USING (SELECT …) alias` subquery source. When present,
3332    /// the engine materialises this SELECT for the source rows and `source`
3333    /// is empty; the alias (required by PG for a subquery source) is in
3334    /// `source_alias`. `None` = plain `USING <table>` (source names a table).
3335    pub source_select: Option<Box<SelectStatement>>,
3336    /// v7.39 (round 768, F31-D5) — `USING (VALUES …) s(id, v)`: the
3337    /// positional column-alias list after the source alias. Empty when
3338    /// the statement carries none; the engine renames the materialised
3339    /// source columns positionally (PG's rule).
3340    pub source_column_aliases: Vec<String>,
3341    pub on: Expr,
3342    pub clauses: Vec<MergeWhenClause>,
3343    /// v7.39 (read01 round 130) — PG17+ `MERGE … RETURNING <projection>`.
3344    /// The projection may use `merge_action()`, `OLD.*`/`NEW.*`, and the
3345    /// target/source aliases. `None` = no RETURNING (the common form).
3346    pub returning: Option<Vec<SelectItem>>,
3347}
3348
3349#[derive(Debug, Clone, PartialEq)]
3350pub struct MergeWhenClause {
3351    pub matched: MergeMatched,
3352    /// Optional `AND <expr>` filter — when present, the clause
3353    /// only fires for the source rows whose match-pair satisfies
3354    /// the predicate.
3355    pub condition: Option<Expr>,
3356    pub action: MergeAction,
3357}
3358
3359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3360pub enum MergeMatched {
3361    Matched,
3362    /// `WHEN NOT MATCHED [BY TARGET]` — a source row with no matching
3363    /// target row (the classic insert branch).
3364    NotMatched,
3365    /// v7.39 (round 146, PG17) — `WHEN NOT MATCHED BY SOURCE`: a TARGET
3366    /// row no source row matches. Actions are UPDATE / DELETE / DO
3367    /// NOTHING only (INSERT is a syntax error, as in PG).
3368    NotMatchedBySource,
3369}
3370
3371#[derive(Debug, Clone, PartialEq)]
3372pub enum MergeAction {
3373    /// `INSERT (cols) VALUES (vals)`. SPG v7.17 requires the
3374    /// explicit column list (the bare `INSERT VALUES (vals)`
3375    /// shape lands later).
3376    Insert {
3377        columns: Vec<String>,
3378        values: Vec<Expr>,
3379    },
3380    /// `UPDATE SET col = expr [, …]` — applied to every matched
3381    /// target row for the firing source row.
3382    Update { assignments: Vec<(String, Expr)> },
3383    /// `DELETE` — drop every matched target row.
3384    Delete,
3385    /// `DO NOTHING` — explicit no-op (the SQL standard accepts
3386    /// the clause and SPG mirrors so a customer-side MERGE that
3387    /// uses it for branch-control doesn't error).
3388    DoNothing,
3389}
3390
3391#[derive(Debug, Clone, PartialEq)]
3392pub struct InsertStatement {
3393    /// v7.37.43-T4.4 — leading `WITH cte AS (…)` clauses on a top-
3394    /// level INSERT (writable CTE outer body). Empty for a plain
3395    /// INSERT. PG semantics: each CTE materialises before the
3396    /// outer INSERT runs, sharing the same transaction.
3397    pub ctes: Vec<Cte>,
3398    pub table: String,
3399    /// v7.39 (round 240) — `INSERT INTO t AS alias`: the alias the ON
3400    /// CONFLICT DO UPDATE expressions (and RETURNING) refer to the target
3401    /// row by. PG requires the AS keyword in this position.
3402    pub alias: Option<String>,
3403    /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
3404    /// `None`, every tuple is positional and must match the table arity.
3405    /// When `Some`, the engine maps each tuple slot to the named column and
3406    /// fills the rest with NULL (must be nullable).
3407    pub columns: Option<Vec<String>>,
3408    /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
3409    /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
3410    /// `select_source` is `Some` (the engine builds rows from the
3411    /// inner SELECT result set instead).
3412    pub rows: Vec<Vec<Expr>>,
3413    /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3414    /// round-5 G4). When present, `rows` is empty and the engine
3415    /// materialises the SELECT result, coerces each output tuple to
3416    /// the target column types, and inserts as a single batch.
3417    pub select_source: Option<Box<SelectStatement>>,
3418    /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
3419    /// upsert clause. None = legacy INSERT (conflict raises a
3420    /// DuplicateKey error). mailrs migration blocker #2.
3421    pub on_conflict: Option<OnConflictClause>,
3422    /// v7.9.4 — `RETURNING <projection>`.
3423    pub returning: Option<Vec<SelectItem>>,
3424    /// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` clause
3425    /// between the column list and VALUES. Governs how explicitly-supplied
3426    /// values interact with `GENERATED … AS IDENTITY` columns:
3427    ///   * `None` — default. A `GENERATED ALWAYS` identity column rejects
3428    ///     an explicit non-DEFAULT value; a `BY DEFAULT` one accepts it.
3429    ///   * `System` — override the ALWAYS restriction: the explicit value
3430    ///     is used verbatim, as for a `BY DEFAULT` column.
3431    ///   * `User` — ignore any explicit value on a `BY DEFAULT` identity
3432    ///     column and generate from the sequence instead (no effect on
3433    ///     non-identity columns).
3434    pub overriding: Overriding,
3435    /// v7.39 (round 434) — the statement was spelled `INSERT IGNORE`.
3436    /// Round 406 lowered that to `ON CONFLICT DO NOTHING`, which covers the
3437    /// key-conflict half of MySQL's IGNORE. The other half is that IGNORE
3438    /// also downgrades per-VALUE errors to coercions (out-of-range clamps,
3439    /// over-long strings truncate, a non-numeric string becomes 0, a NULL
3440    /// into a NOT NULL column becomes the type's default), and the engine
3441    /// cannot recover that intent from the conflict clause alone. A plain
3442    /// `bool` lands in this struct's existing padding, so the AST does not
3443    /// grow — measured, per the round-305 / 413 nesting-stack lesson.
3444    pub mysql_ignore: bool,
3445}
3446
3447/// v7.38 (read01) — `OVERRIDING { SYSTEM | USER } VALUE` on an INSERT.
3448#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3449pub enum Overriding {
3450    /// No `OVERRIDING` clause.
3451    #[default]
3452    None,
3453    /// `OVERRIDING SYSTEM VALUE`.
3454    System,
3455    /// `OVERRIDING USER VALUE`.
3456    User,
3457}
3458
3459/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
3460#[derive(Debug, Clone, PartialEq)]
3461pub struct OnConflictClause {
3462    /// Local columns that identify the conflict (must match a
3463    /// UNIQUE / PRIMARY KEY index on the target table). Empty
3464    /// list means the user wrote `ON CONFLICT DO …` without a
3465    /// target — the engine arbitrates on every unique constraint
3466    /// (round 240).
3467    pub target_columns: Vec<String>,
3468    /// v7.39 (round 240) — the index predicate after the target list
3469    /// (`ON CONFLICT (col) WHERE pred DO …`). PG uses it to infer a
3470    /// PARTIAL unique index; SPG's conflict arbiters are full indexes,
3471    /// which satisfy any predicate, so it is parsed and carried but not
3472    /// consulted (recorded residual: partial-unique-index arbiters).
3473    pub index_where: Option<Expr>,
3474    /// v7.37.17 (17.6 siblings) — `ON CONFLICT ON CONSTRAINT
3475    /// <name>`: the pg_dump conflict-target form. The engine
3476    /// resolves the name to the constraint's columns.
3477    pub constraint_name: Option<String>,
3478    /// v7.39 (round 240) — true when this clause was LOWERED from MySQL's
3479    /// `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`, whose bare-target DO
3480    /// UPDATE is legal (MySQL watches every unique key); PG's own bare
3481    /// `ON CONFLICT DO UPDATE` is refused (42601).
3482    pub mysql_lowered: bool,
3483    /// The action on conflict.
3484    pub action: OnConflictAction,
3485}
3486
3487/// v7.9.7 — action on conflict.
3488#[derive(Debug, Clone, PartialEq)]
3489pub enum OnConflictAction {
3490    /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
3491    /// silently skips conflicting ones.
3492    Nothing,
3493    /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
3494    /// may reference `EXCLUDED.col` to read the incoming row's
3495    /// value (engine wires `EXCLUDED` as a virtual table).
3496    Update {
3497        assignments: Vec<(String, Expr)>,
3498        where_: Option<Expr>,
3499    },
3500}
3501
3502/// v7.39 (round 293, E3 Phase 1) — a row-locking clause.
3503///
3504/// `spg-sql` cannot depend on `spg-engine`, so the strengths and
3505/// policies are spelled again here and mapped at the engine boundary.
3506/// v7.39 — the modes `BEGIN` / `START TRANSACTION` / `SET TRANSACTION`
3507/// / `SET SESSION CHARACTERISTICS` accept. `read_only` used to be parsed
3508/// and dropped on the floor, so `BEGIN READ ONLY` opened an ordinary
3509/// read-write transaction and every write in it was accepted.
3510///
3511/// `None` on either field means the statement did not name that mode, so
3512/// the session default applies — which is not the same as naming the
3513/// default explicitly.
3514#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3515pub struct TransactionModes {
3516    pub isolation: Option<IsolationLevel>,
3517    /// `Some(true)` = READ ONLY, `Some(false)` = READ WRITE.
3518    pub read_only: Option<bool>,
3519}
3520
3521/// v7.39 — what a read-only transaction refuses, and what PG calls it.
3522///
3523/// SPG did not enforce read-only transactions at all: `BEGIN READ ONLY;
3524/// INSERT …` answered `INSERT 0 1` and committed, and
3525/// `default_transaction_read_only = on` changed nothing. Both GUCs were
3526/// in the inventory, so a session could set one, read it back, and be
3527/// told it held a guarantee nothing was enforcing. Applications open
3528/// read-only transactions as a SAFETY measure — a reporting connection,
3529/// a read-only leg in a pool, a "this path must not write" discipline —
3530/// so accepting the writes is the worst possible answer.
3531///
3532/// `Some(tag)` means refuse with PG's message, `cannot execute {tag} in
3533/// a read-only transaction` (SQLSTATE 25006). Every tag below was read
3534/// back from PostgreSQL 18.6 by running the statement inside
3535/// `BEGIN READ ONLY`, one statement per transaction so no error could be
3536/// attributed to the wrong line.
3537///
3538/// Several answers were not what one would guess, which is why they were
3539/// measured rather than reasoned:
3540///
3541///   * `CREATE TEMP TABLE` is REFUSED, tagged `CREATE TABLE`.
3542///   * `NOTIFY`, `LISTEN` and `REINDEX` are ALLOWED.
3543///   * `PREPARE` of an INSERT is ALLOWED — only the EXECUTE writes.
3544///   * `UPDATE … WHERE false`, which changes nothing, is still REFUSED:
3545///     the verb decides, not the row count.
3546///   * `GRANT`, `COMMENT ON` and `SELECT … FOR SHARE` are all REFUSED.
3547///
3548/// The match is exhaustive on purpose. A new statement cannot be added
3549/// without deciding here whether it writes, which is the failure this
3550/// repository keeps meeting: one member of a family gets handled and its
3551/// siblings quietly do not.
3552impl Statement {
3553    #[must_use]
3554    pub fn read_only_violation_tag(&self) -> Option<&'static str> {
3555        match self {
3556            // ---- writes rows -------------------------------------------
3557            Self::Insert { .. } => Some("INSERT"),
3558            Self::Update { .. } => Some("UPDATE"),
3559            Self::Delete { .. } => Some("DELETE"),
3560            Self::Merge { .. } => Some("MERGE"),
3561            Self::Truncate { .. } => Some("TRUNCATE TABLE"),
3562            Self::CopyFromFile { .. } => Some("COPY FROM"),
3563
3564            // A SELECT that takes row locks writes lock state, and PG
3565            // names the strength it was asked for.
3566            Self::Select(sel) => sel.locking.as_ref().map(|l| match l.strength {
3567                LockStrength::Update => "SELECT FOR UPDATE",
3568                LockStrength::NoKeyUpdate => "SELECT FOR NO KEY UPDATE",
3569                LockStrength::Share => "SELECT FOR SHARE",
3570                LockStrength::KeyShare => "SELECT FOR KEY SHARE",
3571            }),
3572
3573            // ---- changes the catalog -----------------------------------
3574            Self::CreateTable { .. } => Some("CREATE TABLE"),
3575            Self::DropTable { .. } => Some("DROP TABLE"),
3576            Self::AlterTable { .. } => Some("ALTER TABLE"),
3577            Self::CreateIndex { .. } => Some("CREATE INDEX"),
3578            Self::DropIndex { .. } => Some("DROP INDEX"),
3579            Self::AlterIndex { .. } => Some("ALTER INDEX"),
3580            Self::CreateView { .. } => Some("CREATE VIEW"),
3581            Self::DropView { .. } => Some("DROP VIEW"),
3582            Self::CreateMaterializedView { .. } => Some("CREATE MATERIALIZED VIEW"),
3583            Self::RefreshMaterializedView { .. } => Some("REFRESH MATERIALIZED VIEW"),
3584            Self::DropMaterializedView { .. } => Some("DROP MATERIALIZED VIEW"),
3585            Self::CreateSequence { .. } => Some("CREATE SEQUENCE"),
3586            Self::AlterSequence { .. } => Some("ALTER SEQUENCE"),
3587            Self::DropSequence { .. } => Some("DROP SEQUENCE"),
3588            Self::CreateType { .. } => Some("CREATE TYPE"),
3589            Self::DropType { .. } => Some("DROP TYPE"),
3590            Self::AlterTypeAddValue { .. } | Self::AlterTypeRenameValue { .. } => {
3591                Some("ALTER TYPE")
3592            }
3593            Self::CreateDomain { .. } => Some("CREATE DOMAIN"),
3594            Self::AlterDomain { .. } => Some("ALTER DOMAIN"),
3595            Self::DropDomain { .. } => Some("DROP DOMAIN"),
3596            Self::CreateSchema { .. } => Some("CREATE SCHEMA"),
3597            Self::DropSchema { .. } => Some("DROP SCHEMA"),
3598            Self::CreateFunction { .. } => Some("CREATE FUNCTION"),
3599            Self::DropFunction { .. } => Some("DROP FUNCTION"),
3600            Self::CreateTrigger { .. } => Some("CREATE TRIGGER"),
3601            Self::DropTrigger { .. } => Some("DROP TRIGGER"),
3602            Self::CreateRule { .. } => Some("CREATE RULE"),
3603            Self::DropRule { .. } => Some("DROP RULE"),
3604            Self::CreateExtension { .. } => Some("CREATE EXTENSION"),
3605            Self::CreateStatistics { .. } => Some("CREATE STATISTICS"),
3606            Self::DropStatistics { .. } => Some("DROP STATISTICS"),
3607            Self::DropAggregate { .. } => Some("DROP AGGREGATE"),
3608            Self::CommentOn { .. } => Some("COMMENT"),
3609            Self::DropDatabase { .. } => Some("DROP DATABASE"),
3610            Self::CreatePublication { .. } => Some("CREATE PUBLICATION"),
3611            Self::DropPublication { .. } => Some("DROP PUBLICATION"),
3612            Self::CreateSubscription { .. } => Some("CREATE SUBSCRIPTION"),
3613            Self::DropSubscription { .. } => Some("DROP SUBSCRIPTION"),
3614
3615            // ---- changes roles / permissions ---------------------------
3616            Self::CreateUser { .. } => Some("CREATE ROLE"),
3617            Self::DropUser { .. } => Some("DROP ROLE"),
3618            Self::AlterRolePassword { .. } | Self::SetDbRoleSetting { .. } => Some("ALTER ROLE"),
3619            Self::Grant { .. } => Some("GRANT"),
3620            Self::Revoke { .. } => Some("REVOKE"),
3621            Self::CreatePolicy { .. } => Some("CREATE POLICY"),
3622            Self::AlterPolicy { .. } => Some("ALTER POLICY"),
3623            Self::DropPolicy { .. } => Some("DROP POLICY"),
3624            Self::AlterSystem { .. } => Some("ALTER SYSTEM"),
3625
3626            // ---- SPG's own writers -------------------------------------
3627            // Rewrites cold-tier segments on disk. PG has no equivalent to
3628            // ask, so the test is what it does, not what it is called.
3629            Self::CompactColdSegments => Some("COMPACT COLD SEGMENTS"),
3630
3631            // ---- allowed -----------------------------------------------
3632            // Reads, transaction control, session state, cursors, and the
3633            // maintenance statements PG itself permits. `REINDEX` really is
3634            // allowed in a read-only transaction (measured), which is why
3635            // `Maintain` is here.
3636            //
3637            // `Prepare`, `Execute`, `Call` and `DoBlock` are allowed at
3638            // this level for the reason PG allows them: the write inside
3639            // is refused when it runs, by this same check. Measured:
3640            // `PREPARE p AS INSERT …` succeeds; `DO $$ … INSERT … $$`
3641            // fails with `cannot execute INSERT`.
3642            Self::Explain { .. }
3643            | Self::CopyTo { .. }
3644            | Self::CopyToFile { .. }
3645            | Self::Analyze { .. }
3646            | Self::Maintain { .. }
3647            | Self::Vacuum { .. }
3648            | Self::Begin { .. }
3649            | Self::Commit
3650            | Self::Rollback
3651            | Self::Savepoint { .. }
3652            | Self::RollbackToSavepoint { .. }
3653            | Self::ReleaseSavepoint { .. }
3654            | Self::PrepareTransaction { .. }
3655            | Self::SetTransaction { .. }
3656            | Self::SetConstraints { .. }
3657            | Self::SetParameter { .. }
3658            | Self::SetParameterList { .. }
3659            | Self::SetUserVars { .. }
3660            | Self::SetRole { .. }
3661            | Self::ResetParameter { .. }
3662            | Self::ShowParameter { .. }
3663            | Self::Discard { .. }
3664            | Self::Prepare { .. }
3665            | Self::Execute { .. }
3666            | Self::Deallocate { .. }
3667            | Self::Call { .. }
3668            | Self::DoBlock { .. }
3669            | Self::DeclareCursor { .. }
3670            | Self::FetchCursor { .. }
3671            | Self::MoveCursor { .. }
3672            | Self::CloseCursor { .. }
3673            | Self::Listen { .. }
3674            | Self::Notify { .. }
3675            | Self::Unlisten { .. }
3676            | Self::Kill { .. }
3677            | Self::WaitForWalPosition { .. }
3678            | Self::ValidateOnly { .. }
3679            | Self::NoOpPreventedInTransaction { .. }
3680            | Self::Empty
3681            | Self::ShowTables
3682            | Self::ShowDatabases
3683            | Self::ShowCreateTable { .. }
3684            | Self::ShowIndexes { .. }
3685            | Self::ShowStatus
3686            | Self::ShowVariables
3687            | Self::ShowVariablesLike { .. }
3688            | Self::ShowProcesslist
3689            | Self::ShowColumns { .. }
3690            | Self::ShowUsers
3691            | Self::ShowPublications
3692            | Self::ShowSubscriptions => None,
3693        }
3694    }
3695}
3696
3697#[derive(Debug, Clone, PartialEq, Eq)]
3698pub struct LockingClause {
3699    pub strength: LockStrength,
3700    /// `FOR UPDATE OF t1, t2` — empty means every relation in the FROM.
3701    pub of_tables: Vec<String>,
3702    pub policy: LockWait,
3703}
3704
3705/// PG's four tuple-lock strengths, weakest first.
3706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3707pub enum LockStrength {
3708    KeyShare,
3709    Share,
3710    NoKeyUpdate,
3711    Update,
3712}
3713
3714/// What to do when the row is already locked.
3715#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3716pub enum LockWait {
3717    /// Block until it is free — PG's default.
3718    #[default]
3719    Wait,
3720    /// `NOWAIT` — fail the statement with 55P03.
3721    NoWait,
3722    /// `SKIP LOCKED` — leave the row out of the result.
3723    SkipLocked,
3724}
3725
3726#[derive(Debug, Clone, PartialEq, Default)]
3727pub struct SelectStatement {
3728    /// v7.39 (round 293, E3 Phase 1) — `FOR UPDATE` and friends. The
3729    /// clause was parsed and DISCARDED since v7.17, so SPG accepted the
3730    /// whole syntax and locked nothing: two workers running the classic
3731    /// `SKIP LOCKED` queue take both took the same row.
3732    /// v7.39 (round 305) — boxed. A locking clause appears on a
3733    /// vanishing fraction of SELECTs, but an inline `Option<LockingClause>`
3734    /// cost every `SelectStatement` 32 bytes, and this struct sits in
3735    /// recursive evaluation frames where the engine already runs close to
3736    /// its stack budget (a 512 KB depth guard is the canary).
3737    pub locking: Option<alloc::boxed::Box<LockingClause>>,
3738    /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
3739    /// expressions, materialised once at query start before the
3740    /// body SELECT runs. Empty for a regular SELECT. Non-recursive
3741    /// only — no `WITH RECURSIVE` for v4.x.
3742    pub ctes: Vec<Cte>,
3743    pub distinct: bool,
3744    /// v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3745    /// keep the first row (per ORDER BY) of each group the
3746    /// expressions define. Empty = no DISTINCT ON.
3747    pub distinct_on: Vec<Expr>,
3748    pub items: Vec<SelectItem>,
3749    pub from: Option<FromClause>,
3750    pub where_: Option<Expr>,
3751    pub group_by: Option<Vec<Expr>>,
3752    /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
3753    /// expands `group_by` to every non-aggregate SELECT-list item
3754    /// before the executor runs. Mutually exclusive with an
3755    /// explicit `group_by` list (the parser sets exactly one).
3756    pub group_by_all: bool,
3757    /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
3758    /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
3759    /// aggregate executor resolves them through the same synthetic
3760    /// schema used for the SELECT items.
3761    pub having: Option<Expr>,
3762    /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
3763    /// itself a `SelectStatement` with `order_by = None` and `limit =
3764    /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
3765    /// top of the chain).
3766    pub unions: Vec<(UnionKind, SelectStatement)>,
3767    /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
3768    /// Keys are matched left-to-right: first key decides, ties break
3769    /// to the second, etc.
3770    pub order_by: Vec<OrderBy>,
3771    /// `LIMIT <n>` — bound on row output. `n` is an integer
3772    /// literal **or** (v7.9.24) a placeholder `$N` resolved
3773    /// against the prepared-statement Bind values. mailrs
3774    /// migration follow-up H2.
3775    pub limit: Option<LimitExpr>,
3776    /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
3777    /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
3778    pub offset: Option<LimitExpr>,
3779    /// v7.17.0 Phase 3.P0-49 — `FETCH FIRST <n> ROWS WITH TIES`
3780    /// (SQL:2008). When true and an ORDER BY is present, the
3781    /// executor extends past the LIMIT-truncated tail to include
3782    /// every row whose ORDER BY key equals the last-kept row's
3783    /// key. Requires an ORDER BY; the executor errors otherwise
3784    /// (matching PG's `WITH TIES` rule). The parser was already
3785    /// accepting `WITH TIES` since Phase 5.1; this field captures
3786    /// the choice so the executor can act on it.
3787    pub limit_with_ties: bool,
3788    /// v7.39 (round 705) — the key expressions of WINDOW-clause definitions
3789    /// that NOTHING referenced. PG analyses every definition whether
3790    /// referenced or not, so `WINDOW w AS (ORDER BY nosuch)` fails there
3791    /// and silently succeeded here — the referenced ones get their columns
3792    /// resolved through the WindowFunction nodes they were inlined into,
3793    /// and the unreferenced ones used to be dropped at parse, unexamined.
3794    /// The engine resolves these with a LIMIT-0 probe of the same FROM.
3795    ///
3796    /// Not part of `Display`: an unreferenced definition has no effect on
3797    /// the result, so a deparsed body (a stored view) omits it.
3798    pub window_check_exprs: Vec<Expr>,
3799}
3800
3801impl Expr {
3802    /// v7.39 (round 305, V23) — hand every `SelectStatement` nested
3803    /// directly inside this expression to `f`. `f` receives each nested
3804    /// statement once; descending further (into that statement's own
3805    /// clauses) is the caller's job, which keeps this walk finite and
3806    /// lets the caller order the recursion.
3807    ///
3808    /// The match is deliberately **wildcard-free**: a new `Expr` variant
3809    /// does not compile until it says whether it can carry a subquery.
3810    /// The row-count resolution pass is built on this, and a shape it
3811    /// silently failed to visit would leave a `LimitExpr::Expr` behind —
3812    /// which every row-count reader would take as "no limit", i.e. the
3813    /// whole table. Compile-time exhaustiveness is what rules that out.
3814    /// Iterative on purpose. Expression trees here get deep (long
3815    /// boolean chains, big IN lists), and this walk is on the path of
3816    /// every statement; recursing would add a frame per node to a stack
3817    /// budget the engine already runs close to — a depth guard that runs
3818    /// on a deliberately small stack caught exactly that. Depth costs
3819    /// heap here instead.
3820    pub fn for_each_subquery_mut<E>(
3821        &mut self,
3822        f: &mut impl FnMut(&mut SelectStatement) -> Result<(), E>,
3823    ) -> Result<(), E> {
3824        let mut stack: Vec<&mut Self> = alloc::vec![self];
3825        while let Some(e) = stack.pop() {
3826            match e {
3827                Self::Literal(_) | Self::Column(_) | Self::Placeholder(_) => {}
3828                Self::NamedArg { expr, .. }
3829                | Self::Variadic(expr)
3830                | Self::Unary { expr, .. }
3831                | Self::Cast { expr, .. }
3832                | Self::FieldAccess { base: expr, .. }
3833                | Self::IsNull { expr, .. }
3834                | Self::BoolTest { expr, .. }
3835                | Self::Extract { source: expr, .. } => stack.push(expr),
3836                Self::Binary { lhs, rhs, .. } => {
3837                    stack.push(lhs);
3838                    stack.push(rhs);
3839                }
3840                Self::Like { expr, pattern, .. } => {
3841                    stack.push(expr);
3842                    stack.push(pattern);
3843                }
3844                Self::ArraySubscript { target, index } => {
3845                    stack.push(target);
3846                    stack.push(index);
3847                }
3848                Self::ArraySlice { target, lo, hi } => {
3849                    stack.push(target);
3850                    stack.extend(lo.iter_mut().chain(hi.iter_mut()).map(|b| &mut **b));
3851                }
3852                Self::AnyAll { expr, array, .. } => {
3853                    stack.push(expr);
3854                    stack.push(array);
3855                }
3856                Self::FunctionCall { args, .. } | Self::Array(args) => {
3857                    stack.extend(args.iter_mut());
3858                }
3859                Self::AggregateOrdered {
3860                    call,
3861                    order_by,
3862                    filter,
3863                    ..
3864                } => {
3865                    stack.push(call);
3866                    stack.extend(order_by.iter_mut().map(|o| &mut o.expr));
3867                    stack.extend(filter.iter_mut().map(|b| &mut **b));
3868                }
3869                Self::WindowFunction {
3870                    args,
3871                    partition_by,
3872                    order_by,
3873                    filter,
3874                    ..
3875                } => {
3876                    // `frame` bounds hold folded numbers / interval
3877                    // parts, never expressions — nothing to visit there.
3878                    stack.extend(args.iter_mut().chain(partition_by.iter_mut()));
3879                    stack.extend(order_by.iter_mut().map(|(e, _, _)| e));
3880                    stack.extend(filter.iter_mut().map(|b| &mut **b));
3881                }
3882                Self::InList { expr, list, .. } => {
3883                    stack.push(expr);
3884                    stack.extend(list.iter_mut());
3885                }
3886                Self::Case {
3887                    operand,
3888                    branches,
3889                    else_branch,
3890                } => {
3891                    stack.extend(
3892                        operand
3893                            .iter_mut()
3894                            .chain(else_branch.iter_mut())
3895                            .map(|b| &mut **b),
3896                    );
3897                    for (when, then) in branches.iter_mut() {
3898                        stack.push(when);
3899                        stack.push(then);
3900                    }
3901                }
3902                Self::ScalarSubquery(s) | Self::Exists { subquery: s, .. } => f(s)?,
3903                Self::InSubquery { expr, subquery, .. } => {
3904                    stack.push(expr);
3905                    f(subquery)?;
3906                }
3907                Self::RowInSubquery { row, subquery, .. }
3908                | Self::RowCmpSubquery { row, subquery, .. } => {
3909                    stack.extend(row.iter_mut());
3910                    f(subquery)?;
3911                }
3912            }
3913        }
3914        Ok(())
3915    }
3916}
3917
3918/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
3919/// time or a placeholder `$N` resolved during extended-query
3920/// Bind. mailrs migration follow-up H2.
3921///
3922/// v7.39 (round 305) — no longer `Copy`/`Eq`: the `Expr` variant boxes
3923/// an arbitrary row-count expression. Losing `Copy` is deliberate — it
3924/// made the compiler point at every site that used to duplicate a
3925/// row-count out of the AST, which is exactly the set that must not
3926/// bypass the resolution pre-pass.
3927#[derive(Debug, Clone, PartialEq)]
3928pub enum LimitExpr {
3929    /// `LIMIT 10` — value known at parse time.
3930    Literal(u32),
3931    /// `LIMIT $N` — the 1-based parameter index, resolved against
3932    /// the bind values when the prepared statement executes.
3933    Placeholder(u16),
3934    /// v7.39 (round 305, V23) — `LIMIT (SELECT 4)` / `LIMIT
3935    /// greatest(2,3)`: a row-count expression that isn't constant, so
3936    /// it can't be folded at parse time. Evaluated once, before
3937    /// dispatch, by the engine's `resolve_limit_exprs` pre-pass, which
3938    /// rewrites it to `Literal` (or to `None` for a NULL result, PG's
3939    /// "no limit"). **No execution path may see this variant** —
3940    /// `as_literal` would report `None`, which every row-count reader
3941    /// takes to mean "unlimited", i.e. the whole table.
3942    Expr(alloc::boxed::Box<Expr>),
3943}
3944
3945impl fmt::Display for LimitExpr {
3946    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3947        match self {
3948            Self::Literal(n) => write!(f, "{n}"),
3949            Self::Placeholder(n) => write!(f, "${n}"),
3950            // Parenthesised so the round-trip text re-parses as one
3951            // row-count expression (`LIMIT (SELECT 4)`), which is also
3952            // the only spelling `FETCH FIRST` accepts.
3953            Self::Expr(e) => write!(f, "({e})"),
3954        }
3955    }
3956}
3957
3958impl LimitExpr {
3959    /// Convenience for the simple-query path where no placeholders
3960    /// can possibly exist. Returns the literal value or `None` if
3961    /// this is a placeholder (caller must surface as Unsupported).
3962    ///
3963    /// v7.39 (round 305) — `None` is read by every row-count consumer as
3964    /// "no limit". An unresolved [`LimitExpr::Expr`] reaching here would
3965    /// therefore silently return the whole table, so the engine's
3966    /// `resolve_limit_exprs` pre-pass rewrites the variant away before
3967    /// dispatch. The assertion makes a missed nesting site fail loudly
3968    /// in every test build rather than quietly widening a result set.
3969    #[must_use]
3970    pub fn as_literal(&self) -> Option<u32> {
3971        match self {
3972            Self::Literal(n) => Some(*n),
3973            Self::Placeholder(_) => None,
3974            Self::Expr(_) => {
3975                debug_assert!(
3976                    false,
3977                    "LimitExpr::Expr reached execution — resolve_limit_exprs \
3978                     missed a nesting site; treating it as `no limit` would \
3979                     return every row"
3980                );
3981                None
3982            }
3983        }
3984    }
3985}
3986
3987/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
3988/// the engine's `substitute_placeholders` pass these are
3989/// always Literal; in the simple-query path a Placeholder
3990/// shape returns None (executor surfaces as
3991/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
3992impl SelectStatement {
3993    #[must_use]
3994    pub fn limit_literal(&self) -> Option<u32> {
3995        self.limit.as_ref().and_then(LimitExpr::as_literal)
3996    }
3997    #[must_use]
3998    pub fn offset_literal(&self) -> Option<u32> {
3999        self.offset.as_ref().and_then(LimitExpr::as_literal)
4000    }
4001}
4002
4003#[derive(Debug, Clone, PartialEq)]
4004pub struct Cte {
4005    pub name: String,
4006    /// v7.37.43-T4.4 — body is either a SELECT (read-only CTE, the
4007    /// classical case) or a data-modifying statement
4008    /// (INSERT / UPDATE / DELETE … RETURNING …) per PG writable
4009    /// CTE semantics. The modifying body's RETURNING projection
4010    /// becomes the materialised CTE table the outer query can
4011    /// reference; the modifying statement runs once before the
4012    /// outer query, within the same transaction.
4013    pub body: CteBody,
4014    /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
4015    /// RECURSIVE keyword. Applies to every CTE in the clause per
4016    /// PG semantics. A non-recursive body in a RECURSIVE WITH is
4017    /// allowed; the engine just runs it once.
4018    pub recursive: bool,
4019    /// v4.22: optional `WITH name(a, b, c)` column-name list. When
4020    /// non-empty, these override the body's output column names
4021    /// position-by-position; the engine errors out if the count
4022    /// doesn't match the body's projection width.
4023    pub column_overrides: Vec<String>,
4024    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY cols
4025    /// SET seqcol` on a recursive CTE. Desugared at parse time into an
4026    /// extra ordering column on the body (see `rewrite_search_and_cycle`).
4027    pub search: Option<SearchClause>,
4028    /// v7.38 (read01 U16) — `CYCLE cols SET markcol [TO v DEFAULT w]
4029    /// USING pathcol` cycle detection, desugared at parse time.
4030    pub cycle: Option<CycleClause>,
4031}
4032
4033/// v7.38 (read01 U16) — parsed `SEARCH … FIRST BY … SET …` clause.
4034#[derive(Debug, Clone, PartialEq)]
4035pub struct SearchClause {
4036    /// `true` = DEPTH FIRST, `false` = BREADTH FIRST.
4037    pub depth_first: bool,
4038    /// The CTE output columns the search orders by.
4039    pub by_columns: Vec<String>,
4040    /// The new column holding the ordering key (a row-array for depth,
4041    /// a `(depth, keys…)` row for breadth).
4042    pub set_column: String,
4043}
4044
4045/// v7.38 (read01 U16) — parsed `CYCLE … SET … [TO … DEFAULT …] USING …`.
4046#[derive(Debug, Clone, PartialEq)]
4047pub struct CycleClause {
4048    /// Columns whose repetition along a path marks a cycle.
4049    pub columns: Vec<String>,
4050    /// The new boolean-ish column set to `mark_value` on a cycle.
4051    pub mark_column: String,
4052    /// Value written to `mark_column` when a cycle is detected (default
4053    /// `true`); `default_value` otherwise. PG allows any type; SPG carries
4054    /// them as literals.
4055    pub mark_value: Option<Literal>,
4056    pub default_value: Option<Literal>,
4057    /// The new column accumulating the visited-row path array.
4058    pub path_column: String,
4059}
4060
4061/// v7.37.43-T4.4 — CTE body. Read-only (Select) or data-modifying
4062/// (Insert / Update / Delete with optional RETURNING). The
4063/// data-modifying variants must carry a RETURNING projection for the
4064/// outer query to reference the CTE alias by; an empty RETURNING is
4065/// only valid if no outer reference materialises (rare — typically
4066/// caught at planning).
4067#[allow(clippy::large_enum_variant)] // CteBody::Select dominates; Boxing would touch every match site
4068#[derive(Debug, Clone, PartialEq)]
4069pub enum CteBody {
4070    Select(SelectStatement),
4071    Insert(Box<InsertStatement>),
4072    Update(Box<UpdateStatement>),
4073    Delete(Box<DeleteStatement>),
4074    /// v7.39 (read01 round 149) — PG 17 allows MERGE as a
4075    /// data-modifying CTE body (`WITH m AS (MERGE … RETURNING …)`).
4076    Merge(Box<MergeStatement>),
4077}
4078
4079impl CteBody {
4080    /// Convenience accessor used by classical (read-only) CTE
4081    /// callsites that still expect a SELECT body. Returns None for
4082    /// data-modifying CTEs; callers must explicitly route those
4083    /// through `exec_with_ctes`'s modifying branch.
4084    #[must_use]
4085    pub fn as_select(&self) -> Option<&SelectStatement> {
4086        match self {
4087            Self::Select(s) => Some(s),
4088            _ => None,
4089        }
4090    }
4091
4092    #[must_use]
4093    pub fn as_select_mut(&mut self) -> Option<&mut SelectStatement> {
4094        match self {
4095            Self::Select(s) => Some(s),
4096            _ => None,
4097        }
4098    }
4099
4100    #[must_use]
4101    pub fn is_modifying(&self) -> bool {
4102        !matches!(self, Self::Select(_))
4103    }
4104}
4105
4106#[derive(Debug, Clone, PartialEq)]
4107pub struct OrderBy {
4108    pub expr: Expr,
4109    /// `false` = ASC (default), `true` = DESC.
4110    pub desc: bool,
4111    /// v7.24 (mailrs round-16 A) — explicit `NULLS FIRST` /
4112    /// `NULLS LAST`. `None` = PG default (NULLS LAST for ASC,
4113    /// NULLS FIRST for DESC); the engine resolves the effective
4114    /// value via `nulls_first.unwrap_or(desc)`.
4115    pub nulls_first: Option<bool>,
4116    /// v7.39 (round 691) — an explicit `COLLATE` written on this key.
4117    /// It lives here rather than in the expression for the same reason
4118    /// `desc` does: at an ORDER BY key a collation is ordering
4119    /// information, and nothing downstream of the sort needs it. A new
4120    /// `Expr` variant would instead put a new arm on `eval_expr`, which
4121    /// this repo has measured to overflow the debug stack.
4122    ///
4123    /// `None` means none was written, and the key falls back to whatever
4124    /// its COLUMN declares — which is every key that existed before this.
4125    pub collation: Option<String>,
4126}
4127
4128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4129pub enum UnionKind {
4130    /// `UNION` — dedupes the combined set.
4131    Distinct,
4132    /// `UNION ALL` — concatenates without dedup.
4133    All,
4134    /// v7.37.17 (17.6 siblings) — `INTERSECT`: distinct rows
4135    /// present on both sides.
4136    Intersect,
4137    /// `INTERSECT ALL` — multiset intersection (min per-row count).
4138    IntersectAll,
4139    /// `EXCEPT` — distinct left rows absent from the right.
4140    Except,
4141    /// `EXCEPT ALL` — multiset subtraction.
4142    ExceptAll,
4143}
4144
4145#[derive(Debug, Clone, PartialEq)]
4146pub enum SelectItem {
4147    Wildcard,
4148    /// v7.39 (read01 round 128) — qualified wildcard `qualifier.*`: every column
4149    /// of the table / alias `qualifier` (or, in a RETURNING list, the `OLD` /
4150    /// `NEW` pseudo-relation).
4151    QualifiedWildcard(String),
4152    Expr {
4153        expr: Expr,
4154        alias: Option<String>,
4155    },
4156}
4157
4158#[derive(Debug, Clone, PartialEq)]
4159pub struct TableRef {
4160    pub name: String,
4161    pub alias: Option<String>,
4162    /// v7.39 (round 644) — `FROM ONLY t`: do not descend into `t`'s
4163    /// children.
4164    ///
4165    /// The keyword used to be absorbed at parse time, on the reasoning
4166    /// that SPG's inheritance children are separate relations a plain
4167    /// scan does not descend into — so ONLY already described what the
4168    /// scan did. That stopped being true when a partition parent
4169    /// started unioning its children: measured, `SELECT count(*) FROM
4170    /// ONLY <partitioned parent>` answered 2 where PG answers 0.
4171    pub only: bool,
4172    /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
4173    /// When `Some(id)`, the scan restricts to rows that live in
4174    /// segment `<id>` only — useful for forensic inspection of a
4175    /// specific freezer-emitted segment without exposing the hot
4176    /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
4177    /// is STABILITY carve-out for v6.10 — needs the freezer to
4178    /// stamp each segment with a wall-clock at creation time.
4179    pub as_of_segment: Option<u32>,
4180    /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
4181    /// source. When `Some`, `name` is the alias (defaulting to
4182    /// `"unnest"` when no `AS` is given) and the engine builds a
4183    /// synthetic single-column table by evaluating the expression
4184    /// once at SELECT entry. Each TEXT[] element becomes one row;
4185    /// NULL elements become NULL cells. v7.11 supported
4186    /// uncorrelated UNNEST only as the FROM primary; v7.13.2
4187    /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
4188    /// position (cross-join with regular tables).
4189    pub unnest_expr: Option<Box<Expr>>,
4190    /// v7.13.2 — mailrs round-6 S5. PG-standard
4191    /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
4192    /// when non-empty, the first entry overrides the projected
4193    /// column name for the unnested column. Empty = fall back to
4194    /// the table alias (pre-v7.13.2 behaviour).
4195    pub unnest_column_aliases: Vec<String>,
4196    /// `WITH ORDINALITY` on an unnest-channel SRF — when true, the
4197    /// row-stream gains a trailing BIGINT column counting rows
4198    /// from 1 in element order. PG names it `ordinality`; a second
4199    /// entry in the column-alias list renames it.
4200    pub with_ordinality: bool,
4201    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4202    /// [, step])` set-returning source. When `Some`, the engine
4203    /// materialises a single-column virtual table by stepping
4204    /// `start` to `stop` inclusive. Args are the literal arg list
4205    /// (2 for default-step, 3 for explicit-step). Supports:
4206    ///   * SmallInt / Int / BigInt with integer step (default = 1)
4207    ///   * Timestamp with INTERVAL step (PG date-range pattern)
4208    /// Mutually exclusive with `unnest_expr` — both populate the
4209    /// same downstream dispatch slot. `name` defaults to
4210    /// `"generate_series"` when no alias is provided.
4211    pub generate_series_args: Option<Vec<Expr>>,
4212    /// v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
4213    /// table. When `Some`, the TableRef is a parenthesised SELECT
4214    /// that may reference columns from the preceding FROM items
4215    /// (correlated derived table). The executor materialises the
4216    /// subquery per left-row, substituting outer-column references
4217    /// against the current join row's values before running the
4218    /// inner SELECT, then cross-joins the result back.
4219    /// Mutually exclusive with `name` / `unnest_expr` /
4220    /// `generate_series_args`.
4221    pub lateral_subquery: Option<Box<SelectStatement>>,
4222    /// v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
4223    /// function as a FROM item. PG semantics: for each key/value
4224    /// pair in the JSONB object argument, emit one (key TEXT,
4225    /// value TEXT) row. When prefixed by `LATERAL` and joined via
4226    /// `CROSS JOIN LATERAL`, the argument may reference columns
4227    /// from a preceding FROM item, in which case the executor
4228    /// evaluates `<expr>` per outer row.
4229    /// Mutually exclusive with `unnest_expr` / `generate_series_args`
4230    /// / `lateral_subquery`. The optional `LATERAL` keyword does not
4231    /// require a separate flag — the executor evaluates per-row
4232    /// whenever the join sits in a JoinKind context.
4233    ///
4234    /// v7.37.17 (17.6 siblings) — the tuple's first slot carries the
4235    /// lowercase SRF name (`jsonb_each` / `jsonb_each_text` /
4236    /// `json_each` / `json_each_text`) so the executor picks the
4237    /// value-column rendering (JSON text vs unwrapped text).
4238    pub jsonb_each_text_arg: Option<(String, Box<Expr>)>,
4239    /// v7.39 (read01 partitionfuncs.c) — generic FROM-position table
4240    /// function channel: `(lowercase fn name, args)`. Carries
4241    /// `pg_partition_tree` / `pg_partition_ancestors`; the executor
4242    /// dispatches by name.
4243    pub table_fn_call: Option<Box<(String, Vec<Expr>)>>,
4244    /// v7.39 (read01 round 78) — this FROM item is a call to a function that
4245    /// returns a BASE type, so the item's row type IS that scalar: a whole-row
4246    /// reference to it yields the value, not a one-field composite
4247    /// (`SELECT j FROM jsonb_array_elements('[1]') AS j` → `1`, PG). The
4248    /// desugared shape is indistinguishable from a hand-written
4249    /// `FROM (SELECT unnest(…)) s`, which is a subquery and does NOT collapse —
4250    /// only the parser knows which one it built, so it says so here.
4251    pub scalar_fn_item: bool,
4252    /// v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))`: N table functions
4253    /// zipped in LOCKSTEP, the shorter padded with NULLs (the same rule the
4254    /// target-list SRFs follow — see round 67). The array-returning family keeps
4255    /// its own lowering; this channel carries the ones that have no array form
4256    /// (`generate_series`, a user `RETURNS SETOF` function).
4257    pub rows_from: Option<Vec<(String, Vec<Expr>)>>,
4258    /// v7.39 (round 205, JSON_TABLE epic) — a `JSON_TABLE(doc, '$path'
4259    /// COLUMNS (...))` FROM item. The doc expr may reference left-side
4260    /// tables (implicit LATERAL, like every SRF channel). Executed by
4261    /// walking the row path over the parsed doc, then each column's
4262    /// path per row-item; NESTED expands as a per-parent outer join.
4263    pub json_table: Option<Box<JsonTable>>,
4264}
4265
4266/// v7.39 (round 205) — a `JSON_TABLE` FROM item.
4267#[derive(Debug, Clone, PartialEq)]
4268pub struct JsonTable {
4269    /// The document expression (jsonb/json/text). May reference outer
4270    /// columns → implicit LATERAL.
4271    pub doc: Box<Expr>,
4272    /// The row-pattern jsonpath (the 2nd JSON_TABLE argument); each
4273    /// match is one row's context item.
4274    pub row_path: String,
4275    /// The COLUMNS list (regular columns, FOR ORDINALITY, NESTED).
4276    pub columns: Vec<JsonTableColumn>,
4277    /// `PASSING <expr> AS <name>` variables, folded into jsonpath `$name`.
4278    pub passing: Vec<(String, Expr)>,
4279}
4280
4281/// v7.39 (round 205) — one entry in a JSON_TABLE (or NESTED) COLUMNS list.
4282#[derive(Debug, Clone, PartialEq)]
4283pub enum JsonTableColumn {
4284    /// `<name> FOR ORDINALITY` — 1-based counter within this level.
4285    Ordinality { name: String },
4286    /// `<name> <type> [FORMAT JSON] PATH '<p>' [WITH WRAPPER]
4287    /// [{DEFAULT <e>|ERROR|NULL} ON EMPTY] [... ON ERROR]`, or
4288    /// `<name> <type> EXISTS [PATH '<p>']`.
4289    Regular {
4290        name: String,
4291        ty: ColumnTypeName,
4292        /// The column jsonpath; defaults to `$.<name>` when `PATH` omitted.
4293        path: String,
4294        /// `EXISTS [PATH …]` — the column is a boolean "did the path match".
4295        exists: bool,
4296        /// `FORMAT JSON` — return the raw jsonb value (not a coerced scalar).
4297        format_json: bool,
4298        /// `WITH [UNCONDITIONAL] WRAPPER` — wrap the result in a json array.
4299        wrapper: bool,
4300        /// Behaviour when the path matches nothing (default NULL).
4301        on_empty: JsonTableOnBehavior,
4302        /// Behaviour when coercion fails (default NULL).
4303        on_error: JsonTableOnBehavior,
4304    },
4305    /// `NESTED PATH '<p>' COLUMNS (...)` — a child level joined per parent
4306    /// row like a LEFT JOIN (a parent with no nested match still emits one
4307    /// row, nested cols NULL).
4308    Nested {
4309        path: String,
4310        columns: Vec<JsonTableColumn>,
4311    },
4312}
4313
4314/// v7.39 (round 205) — a JSON_TABLE column's ON EMPTY / ON ERROR clause.
4315#[derive(Debug, Clone, PartialEq)]
4316pub enum JsonTableOnBehavior {
4317    /// Default: the column value is NULL.
4318    Null,
4319    /// `ERROR ON {EMPTY|ERROR}` — raise PG's error.
4320    Error,
4321    /// `DEFAULT <expr> ON {EMPTY|ERROR}` — the given value.
4322    Default(Box<Expr>),
4323}
4324
4325/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
4326/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
4327/// joins evaluate left-associatively in nested-loop order.
4328#[derive(Debug, Clone, PartialEq)]
4329pub struct FromClause {
4330    pub primary: TableRef,
4331    pub joins: Vec<FromJoin>,
4332}
4333
4334#[derive(Debug, Clone, PartialEq)]
4335pub struct FromJoin {
4336    pub kind: JoinKind,
4337    pub table: TableRef,
4338    /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
4339    pub on: Option<Expr>,
4340    /// v7.37.16 — `JOIN … USING (c1, c2, …)`. When `Some`, records the
4341    /// USING column list so the executor can perform PG's column-merge
4342    /// (the join columns collapse to a single unqualified output column,
4343    /// `t1.c` for INNER/LEFT, `t2.c` for RIGHT, `COALESCE(t1.c,t2.c)` for
4344    /// FULL, and appear first in `SELECT *`). The parser ALSO desugars
4345    /// USING into an equivalent `on` predicate so the join filter/count
4346    /// path works unchanged; `using_cols` drives only the output-shape
4347    /// rewrite. Empty/`None` for `ON` and CROSS joins.
4348    pub using_cols: Option<Vec<String>>,
4349    /// v7.37.16 — `NATURAL [INNER|LEFT|RIGHT|FULL] JOIN`. The common
4350    /// column names are not known until the table schemas are available
4351    /// (parse time is schema-less), so the parser only sets this flag and
4352    /// leaves `on`/`using_cols` empty; the engine resolves the common
4353    /// columns at execution time, synthesises the `on` predicate + the
4354    /// USING column-merge, and clears the flag. If there are no common
4355    /// columns PG treats it as a CROSS join.
4356    pub natural: bool,
4357}
4358
4359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4360pub enum JoinKind {
4361    Inner,
4362    Left,
4363    Cross,
4364    /// v7.37.16 — `RIGHT [OUTER] JOIN`: keep every right (peer) row,
4365    /// NULL-filling the left (drive) columns on unmatched right rows.
4366    /// The executor runs the LEFT algorithm's mirror: it tracks which
4367    /// peer rows matched and emits the unmatched ones with a NULL-left
4368    /// tuple after the probe loop. Output column order is unchanged
4369    /// (left-table cols then right-table cols).
4370    Right,
4371    /// v7.37.16 — `FULL [OUTER] JOIN`: keep every row from both sides
4372    /// (LEFT-unmatched → NULL right, RIGHT-unmatched → NULL left).
4373    FullOuter,
4374    /// v7.39 (round 725) — SEMI join: each drive row is kept AT MOST
4375    /// once, paired with the first peer row that satisfies the ON. Not
4376    /// reachable from SQL — the EXISTS pull-up emits it, which is what
4377    /// frees positive EXISTS from the round-721 uniqueness gate (an
4378    /// INNER join would multiply the outer rows; a semi join cannot).
4379    Semi,
4380}
4381
4382#[derive(Debug, Clone, PartialEq)]
4383pub enum Expr {
4384    Literal(Literal),
4385    Column(ColumnName),
4386    /// v7.39 (read01 round 77) — a NAMED call argument (`f(x := 1)`, or the
4387    /// older `f(x => 1)` spelling). Which slot the name fills depends on the
4388    /// callee's declared parameter names, and a user function's live in the
4389    /// catalog — which the parser cannot see. So the name rides along in the
4390    /// tree and the evaluator, which has the catalog, does the reordering.
4391    /// Appears only inside a `FunctionCall`'s argument list.
4392    NamedArg {
4393        name: String,
4394        expr: Box<Expr>,
4395    },
4396    /// v7.39 (read01 round 100) — `VARIADIC <array>` as the last argument of a
4397    /// variadic function call (`concat_ws(',', VARIADIC ARRAY[…])`). The inner
4398    /// expression evaluates to an array whose elements the evaluator splices
4399    /// into the call as individual trailing arguments. Appears only inside a
4400    /// `FunctionCall`'s argument list.
4401    Variadic(Box<Expr>),
4402    /// v6.1.1 — `$N` parameter placeholder for the extended query
4403    /// protocol. The number is 1-based per PostgreSQL convention.
4404    /// Evaluation looks up `params[N-1]` from the prepared-statement
4405    /// bind buffer; out-of-range indices raise a runtime error
4406    /// (same shape as a column-not-found miss).
4407    Placeholder(u16),
4408    Binary {
4409        lhs: Box<Expr>,
4410        op: BinOp,
4411        rhs: Box<Expr>,
4412    },
4413    Unary {
4414        op: UnOp,
4415        expr: Box<Expr>,
4416    },
4417    /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
4418    /// TEXT, BOOL targets; engine coerces at evaluation time.
4419    Cast {
4420        expr: Box<Expr>,
4421        target: CastTarget,
4422    },
4423    /// v7.38 (read01, T9) — composite field access `(expr).field`. `base`
4424    /// evaluates to a composite/record value (an explicit `ROW(...)`, a
4425    /// whole-row reference, or a composite-returning function); `field` names
4426    /// the member (`f1`..`fN` positional for an anonymous ROW, or the base
4427    /// column names for a whole-row). Only the parenthesised form reaches
4428    /// here — a bare `a.b` is parsed as a qualified column reference.
4429    FieldAccess {
4430        base: Box<Expr>,
4431        field: String,
4432    },
4433    /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
4434    IsNull {
4435        expr: Box<Expr>,
4436        negated: bool,
4437    },
4438    /// v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`, the
4439    /// three-valued boolean tests. `value` is `Some(true)` for TRUE,
4440    /// `Some(false)` for FALSE and `None` for UNKNOWN.
4441    ///
4442    /// These used to be lowered to `CASE` / `IS NULL` right in the parser.
4443    /// The semantics were right, but the AST then had no way to say what
4444    /// the user wrote, so every renderer printed the lowering:
4445    /// `CHECK ((a > 1) IS TRUE)` came back as
4446    /// `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`, and a
4447    /// dumped view lost the form too.
4448    BoolTest {
4449        expr: Box<Expr>,
4450        value: Option<bool>,
4451        negated: bool,
4452    },
4453    /// Function call `name(args...)`. v1.4 supports a small built-in set
4454    /// (length, upper, lower, abs, coalesce); unknown names error at eval
4455    /// time so the parser stays open for v1.5 aggregates.
4456    FunctionCall {
4457        name: String,
4458        args: Vec<Expr>,
4459    },
4460    /// v7.24 (mailrs round-16 A) — an aggregate call with an
4461    /// internal ordering: `array_agg(x ORDER BY y DESC NULLS LAST)`.
4462    /// Wraps the plain [`Expr::FunctionCall`] so every existing
4463    /// FunctionCall consumer stays untouched; only the aggregate
4464    /// executor (and the expression walkers) know the wrapper.
4465    /// Non-aggregate evaluation contexts reject it at eval time.
4466    AggregateOrdered {
4467        call: Box<Expr>,
4468        order_by: Vec<OrderBy>,
4469        /// v7.25 (round-17) — `COUNT(DISTINCT x)` /
4470        /// `string_agg(DISTINCT s, ',')`. The wrapper carries every
4471        /// aggregate modifier so plain FunctionCall stays untouched.
4472        distinct: bool,
4473        /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
4474        /// Only the rows where `cond` is true contribute to this
4475        /// aggregate (SQL:2003 T612 / PG 9.4). Carried as a first-class
4476        /// modifier — NOT desugared to `agg(CASE WHEN cond THEN arg
4477        /// END)`, which is faithful for NULL-ignoring aggregates but
4478        /// WRONG for `array_agg` (it would collect a NULL per excluded
4479        /// row). The executor instead skips excluded rows before
4480        /// accumulation, which is correct for every aggregate.
4481        filter: Option<Box<Expr>>,
4482    },
4483    /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
4484    /// wildcards are `%` (any run) and `_` (one char), backslash escapes
4485    /// the next char (so `\%` matches a literal `%`).
4486    Like {
4487        expr: Box<Expr>,
4488        pattern: Box<Expr>,
4489        negated: bool,
4490        /// v7.25 (mailrs round-17) — `ILIKE`: case-insensitive
4491        /// match. PG folds both operands.
4492        case_insensitive: bool,
4493    },
4494    /// v4.12 window function call: `name(args) OVER (PARTITION BY
4495    /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
4496    /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
4497    /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
4498    /// unordered windows and "from start of partition through
4499    /// current row" for ordered windows — no explicit ROWS /
4500    /// RANGE clause in v4.12 MVP.
4501    WindowFunction {
4502        name: String,
4503        args: Vec<Expr>,
4504        partition_by: Vec<Expr>,
4505        /// v7.24.1 — third slot: explicit NULLS FIRST/LAST
4506        /// (None = PG default, same contract as [`OrderBy`]).
4507        order_by: Vec<(
4508            Expr,
4509            bool,         /* desc */
4510            Option<bool>, /* nulls_first */
4511        )>,
4512        /// v4.20 explicit frame. `None` means "use the default":
4513        /// whole-partition when unordered, running aggregate from
4514        /// partition start through current row when ordered.
4515        frame: Option<WindowFrame>,
4516        /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
4517        /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
4518        /// `Respect` (PG / ANSI default — NULLs participate). Other
4519        /// window functions ignore this flag.
4520        null_treatment: NullTreatment,
4521        /// v7.37 D.40 — `agg(...) FILTER (WHERE cond) OVER (...)`. `None`
4522        /// = no FILTER. Only aggregate window functions honor it; the
4523        /// predicate restricts which peer rows contribute within the frame.
4524        filter: Option<Box<Expr>>,
4525    },
4526    /// v4.10 scalar subquery — `(SELECT ...)` used in expression
4527    /// position. Must return exactly one row × one column at eval
4528    /// time; the engine errors out otherwise. Uncorrelated only —
4529    /// the inner SELECT cannot reference outer columns.
4530    ScalarSubquery(Box<SelectStatement>),
4531    /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
4532    /// projection is ignored; only row-count matters.
4533    Exists {
4534        subquery: Box<SelectStatement>,
4535        negated: bool,
4536    },
4537    /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
4538    /// project exactly one column; membership is tested by Eq
4539    /// against each row's value (NULL handling follows ANSI:
4540    /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
4541    InSubquery {
4542        expr: Box<Expr>,
4543        subquery: Box<SelectStatement>,
4544        negated: bool,
4545    },
4546    /// `(a, b, …) [NOT] IN (SELECT x, y, …)` — a row constructor tested
4547    /// against a multi-column subquery. Row comparisons against a *list*
4548    /// decompose to OR-of-AND at parse time, but the subquery form can't
4549    /// (its rows are only known at runtime), so this survives as its own
4550    /// node evaluated with PG's row-comparison three-valued logic.
4551    RowInSubquery {
4552        row: Vec<Expr>,
4553        subquery: Box<SelectStatement>,
4554        negated: bool,
4555    },
4556    /// `(a, b, …) <op> (SELECT x, y, …)` — a row constructor compared to a
4557    /// single-row subquery (`=`, `<>`, `<`, `<=`, `>`, `>=`). Like
4558    /// RowInSubquery, the literal-RHS form decomposes at parse time but the
4559    /// subquery form can't, so it survives as its own node. The subquery
4560    /// must yield at most one row (zero → NULL, PG scalar-subquery rule).
4561    RowCmpSubquery {
4562        row: Vec<Expr>,
4563        op: BinOp,
4564        subquery: Box<SelectStatement>,
4565    },
4566    /// v7.30.2 (mailrs round-25) — `expr [NOT] IN (a, b, …)` as a FLAT
4567    /// list. Both the parser's literal-list path and the engine's
4568    /// IN-subquery materialisation used to desugar into a left-deep
4569    /// OR-Eq chain, so expression depth scaled with the element count
4570    /// — a 24k-row subquery result overflowed the 2 MiB worker stack
4571    /// (recursive eval AND recursive Box drop) and aborted embedding
4572    /// host processes. The flat node keeps depth constant: eval is an
4573    /// iterative scan with PG three-valued logic, drop is a Vec drop.
4574    InList {
4575        expr: Box<Expr>,
4576        list: Vec<Expr>,
4577        negated: bool,
4578    },
4579    /// `EXTRACT(<field> FROM <source>)` — pull an integer component
4580    /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
4581    /// because the `FROM` keyword is what separates the two halves,
4582    /// not a comma.
4583    Extract {
4584        field: ExtractField,
4585        source: Box<Expr>,
4586    },
4587    /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
4588    /// element is evaluated independently; NULLs are allowed.
4589    /// v7.10 supports only single-dimension TEXT[] semantically;
4590    /// non-text elements coerce at engine evaluation time when
4591    /// the surrounding context (column type / cast) makes the
4592    /// target clear.
4593    Array(Vec<Expr>),
4594    /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
4595    /// engine returns NULL for out-of-range indices.
4596    ArraySubscript {
4597        target: Box<Expr>,
4598        index: Box<Expr>,
4599    },
4600    /// Array slice `arr[lo:hi]` — PG 1-based, both ends
4601    /// inclusive; a missing bound extends to that end of the
4602    /// array and out-of-range bounds clamp. Returns an array of
4603    /// the same element type.
4604    ArraySlice {
4605        target: Box<Expr>,
4606        lo: Option<Box<Expr>>,
4607        hi: Option<Box<Expr>>,
4608    },
4609    /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
4610    /// operator is the comparison binary op (Eq / Ne / Lt / …);
4611    /// the engine desugars: `ANY` returns true if any element
4612    /// satisfies; `ALL` returns true only if every element does.
4613    /// NULL handling follows PG's three-valued logic.
4614    AnyAll {
4615        expr: Box<Expr>,
4616        op: BinOp,
4617        array: Box<Expr>,
4618        /// `true` = ANY, `false` = ALL.
4619        is_any: bool,
4620    },
4621    /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
4622    /// (searched form, `operand` is None) and
4623    /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
4624    /// `operand` is the lead expression compared against each
4625    /// branch's match). Each `(when_expr, then_expr)` branch
4626    /// stays as written; engine short-circuits on the first match.
4627    /// `else_branch` is `None` when no ELSE; evaluates to NULL.
4628    /// mailrs round-5 G9.
4629    Case {
4630        operand: Option<Box<Expr>>,
4631        branches: Vec<(Expr, Expr)>,
4632        else_branch: Option<Box<Expr>>,
4633    },
4634}
4635
4636/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
4637/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
4638/// in the offset walk. `Ignore` causes the function to skip NULL
4639/// values in the argument expression, returning the next non-NULL.
4640#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4641pub enum NullTreatment {
4642    #[default]
4643    Respect,
4644    Ignore,
4645}
4646
4647/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
4648/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
4649/// where end implicitly = CURRENT ROW.
4650#[derive(Debug, Clone, PartialEq, Eq)]
4651pub struct WindowFrame {
4652    pub kind: FrameKind,
4653    pub start: FrameBound,
4654    pub end: Option<FrameBound>,
4655    /// v7.37 (scout round 12) — `EXCLUDE {CURRENT ROW | GROUP |
4656    /// TIES | NO OTHERS}` frame exclusion. NO OTHERS is the default
4657    /// no-op; CURRENT ROW drops the current row from the frame.
4658    pub exclude: FrameExclusion,
4659}
4660
4661#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4662pub enum FrameExclusion {
4663    /// Default — exclude nothing.
4664    #[default]
4665    NoOthers,
4666    /// Drop the current row from the frame.
4667    CurrentRow,
4668    /// Drop the current row's whole peer group.
4669    Group,
4670    /// Drop the current row's peers but keep the current row.
4671    Ties,
4672}
4673
4674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4675pub enum FrameKind {
4676    Rows,
4677    Range,
4678    /// v7.37.19 (19.11) — PG 11+ `GROUPS BETWEEN N PRECEDING AND M
4679    /// FOLLOWING` peer-group frame mode. With UNBOUNDED / CURRENT ROW
4680    /// bounds (no explicit integer offsets) GROUPS behaves identically
4681    /// to RANGE — both consult the peer-group of the current row.
4682    /// Integer offsets are not yet supported; the executor rejects
4683    /// them at run time.
4684    Groups,
4685}
4686
4687#[derive(Debug, Clone, PartialEq, Eq)]
4688pub enum FrameBound {
4689    UnboundedPreceding,
4690    OffsetPreceding(u64),
4691    CurrentRow,
4692    OffsetFollowing(u64),
4693    UnboundedFollowing,
4694    /// `RANGE BETWEEN <interval> PRECEDING …` — value-based offset over a
4695    /// DATE / TIMESTAMP ORDER BY column (PG time-series windows). The
4696    /// interval is folded to its (months, days, micros) components at
4697    /// parse time.
4698    IntervalPreceding {
4699        months: i32,
4700        days: i32,
4701        micros: i64,
4702    },
4703    IntervalFollowing {
4704        months: i32,
4705        days: i32,
4706        micros: i64,
4707    },
4708}
4709
4710impl fmt::Display for FrameBound {
4711    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4712        match self {
4713            Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
4714            Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
4715            Self::CurrentRow => f.write_str("CURRENT ROW"),
4716            Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
4717            Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
4718            Self::IntervalPreceding { .. } => f.write_str("INTERVAL PRECEDING"),
4719            Self::IntervalFollowing { .. } => f.write_str("INTERVAL FOLLOWING"),
4720        }
4721    }
4722}
4723
4724#[derive(Debug, Clone, PartialEq, Eq)]
4725pub enum ExtractField {
4726    Year,
4727    Month,
4728    Day,
4729    Hour,
4730    Minute,
4731    Second,
4732    Microsecond,
4733    /// Seconds since 1970-01-01 00:00:00 UTC (PG returns numeric;
4734    /// SPG keeps the integer convention — truncated seconds).
4735    Epoch,
4736    /// Day of week, 0 = Sunday … 6 = Saturday.
4737    Dow,
4738    /// ISO day of week, 1 = Monday … 7 = Sunday.
4739    Isodow,
4740    /// Day of year, 1-366.
4741    Doy,
4742    /// ISO 8601 week number, 1-53.
4743    Week,
4744    /// ISO 8601 week-numbering year (pairs with `Week`).
4745    Isoyear,
4746    /// Quarter, 1-4.
4747    Quarter,
4748    /// Year divided by 10 (floor).
4749    Decade,
4750    /// Century — 2001-2100 is century 21.
4751    Century,
4752    /// Millennium — 2001-3000 is millennium 3.
4753    Millennium,
4754    /// Julian day number (truncated for timestamps).
4755    Julian,
4756    /// Seconds and fraction in milliseconds (ss·1000 + frac).
4757    Millisecond,
4758    /// UTC offset in seconds — SPG sessions run UTC, so 0.
4759    Timezone,
4760    /// Hour component of the UTC offset — 0.
4761    TimezoneHour,
4762    /// Minute component of the UTC offset — 0.
4763    TimezoneMinute,
4764    /// v7.39 (round 253) — a field name the parser does not know. PG
4765    /// resolves EXTRACT fields at RUNTIME and reports them with the
4766    /// source type (`unit "nosuch" not recognized for type timestamp
4767    /// without time zone`, 22023), so the parser carries the raw name
4768    /// instead of rejecting.
4769    Other(String),
4770}
4771
4772impl fmt::Display for ExtractField {
4773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4774        f.write_str(match self {
4775            Self::Year => "YEAR",
4776            Self::Month => "MONTH",
4777            Self::Day => "DAY",
4778            Self::Hour => "HOUR",
4779            Self::Minute => "MINUTE",
4780            Self::Second => "SECOND",
4781            Self::Microsecond => "MICROSECOND",
4782            Self::Epoch => "EPOCH",
4783            Self::Dow => "DOW",
4784            Self::Isodow => "ISODOW",
4785            Self::Doy => "DOY",
4786            Self::Week => "WEEK",
4787            Self::Isoyear => "ISOYEAR",
4788            Self::Quarter => "QUARTER",
4789            Self::Decade => "DECADE",
4790            Self::Century => "CENTURY",
4791            Self::Millennium => "MILLENNIUM",
4792            Self::Julian => "JULIAN",
4793            Self::Millisecond => "MILLISECOND",
4794            Self::Timezone => "TIMEZONE",
4795            Self::TimezoneHour => "TIMEZONE_HOUR",
4796            Self::TimezoneMinute => "TIMEZONE_MINUTE",
4797            Self::Other(name) => return f.write_str(name),
4798        })
4799    }
4800}
4801
4802#[derive(Debug, Clone, PartialEq, Eq)]
4803pub enum CastTarget {
4804    Int,
4805    BigInt,
4806    Float,
4807    Text,
4808    Bool,
4809    Vector,
4810    Date,
4811    Timestamp,
4812    /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
4813    /// H3a. Engine reuses the existing runtime-interval / timestamp
4814    /// paths (parse the text input, return the matching Value).
4815    Interval,
4816    Timestamptz,
4817    /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
4818    /// types (v7.9.0); the cast just routes Text→Json with the
4819    /// requested OID for the wire layer.
4820    Json,
4821    Jsonb,
4822    /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
4823    /// compatibility; engine surfaces as Unsupported with a
4824    /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
4825    RegType,
4826    RegClass,
4827    /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
4828    /// the PG external array form `{a,b,NULL}`.
4829    TextArray,
4830    /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
4831    /// `{1,2,3}` or widens a `TextArray` whose elements are
4832    /// integer-shaped.
4833    IntArray,
4834    BigIntArray,
4835    /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
4836    /// external form text representation. Used by pg_dump output
4837    /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
4838    TsVector,
4839    TsQuery,
4840    /// v7.17.0 — `::uuid`. Decodes the LHS Text via
4841    /// `spg_storage::parse_uuid_str` (accepts canonical hyphenated,
4842    /// unhyphenated, uppercase, and brace-wrapped forms); malformed
4843    /// input is a SQL error.
4844    Uuid,
4845    /// v7.18 — `::bytea`. Decodes the LHS Text via PG's hex form
4846    /// (`'\xdeadbeef'`) or escape form (`'\x05\x00'`); Bytes
4847    /// inputs pass through unchanged. Closes the mailrs D-pre #3
4848    /// reverse-acceptance gap — anywhere a PG schema writes
4849    /// `expr::bytea`, SPG now matches.
4850    Bytea,
4851    /// v7.37.5 ship triage — generic cast target for the long tail
4852    /// of PG type names the parser meets in `expr::TYPE` shapes that
4853    /// don't deserve their own enum variant. The engine routes these
4854    /// through `column_type_to_data_type` + the existing typed
4855    /// `coerce_value` dispatch, so adding a new PG type to SPG
4856    /// implicitly adds its cast-target form too — no parser change
4857    /// per type. The string carries the lowercase PG type ident
4858    /// (e.g. `"point"`, `"int4multirange"`); the engine errors with
4859    /// a clear message when the type isn't known.
4860    Named(String),
4861}
4862
4863impl fmt::Display for CastTarget {
4864    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4865        f.write_str(match self {
4866            Self::Int => "int",
4867            Self::BigInt => "bigint",
4868            Self::Float => "float",
4869            Self::Text => "text",
4870            Self::Bool => "bool",
4871            Self::Vector => "vector",
4872            Self::Interval => "interval",
4873            Self::Timestamptz => "timestamptz",
4874            Self::Json => "json",
4875            Self::Jsonb => "jsonb",
4876            Self::RegType => "regtype",
4877            Self::RegClass => "regclass",
4878            Self::Date => "date",
4879            Self::Timestamp => "timestamp",
4880            Self::TextArray => "TEXT[]",
4881            Self::IntArray => "INT[]",
4882            Self::BigIntArray => "BIGINT[]",
4883            Self::TsVector => "tsvector",
4884            Self::TsQuery => "tsquery",
4885            Self::Uuid => "uuid",
4886            Self::Bytea => "bytea",
4887            // v7.37.5 — `Self::Named` carries its own canonical name.
4888            Self::Named(name) => return f.write_str(name),
4889        })
4890    }
4891}
4892
4893#[derive(Debug, Clone, PartialEq)]
4894pub enum Literal {
4895    Integer(i64),
4896    Float(f64),
4897    /// Exact decimal literal — a bare `12.34`-style token, kept as
4898    /// `unscaled / 10^scale` so no precision or trailing-zero scale is lost
4899    /// before it becomes a `Value::Numeric`. PG parses such literals as
4900    /// `numeric`, not `double precision`. (Scientific/huge literals stay
4901    /// `Float`.)
4902    Numeric {
4903        unscaled: i128,
4904        /// v7.39 (round 271) — widened to u16. At u8 a literal with more
4905        /// than 255 decimal places could not be represented, and the
4906        /// conversion's `.expect("lexer-validated decimal")` aborted the
4907        /// query with an internal error on SQL PG accepts.
4908        scale: u16,
4909    },
4910    /// v7.38 (read01, T3.C3) — an exact decimal literal whose mantissa overflows
4911    /// i128 (kept as its source digit string, `[-]digits[.digits]`). Becomes a
4912    /// `Value::NumericBig` at eval; previously such literals fell back to double.
4913    NumericBig(String),
4914    String(String),
4915    /// v7.38.8 — a temporal constant that has already been decoded.
4916    ///
4917    /// Without these the only way to carry one through the AST was as
4918    /// text, and a predicate comparing a `timestamp` column against a
4919    /// literal then coerced that text back into a timestamp ONCE PER
4920    /// ROW — 32 ns of the 52 a comparison cost, measured on a customer
4921    /// profile. `constfold` produced text for the same reason: its exit
4922    /// had nothing else to hand back.
4923    ///
4924    /// `text` keeps the spelling so `Display` round-trips byte for byte,
4925    /// the way `Interval` already does and for the same reason: this
4926    /// node is printed in EXPLAIN, in dumps and in error messages, and
4927    /// none of those should change because the value stopped being
4928    /// carried as a string. The enum already holds a `String` and an
4929    /// `i128`, so neither variant widens it.
4930    Timestamp {
4931        micros: i64,
4932        text: String,
4933    },
4934    /// Days since the epoch `Value::Date` counts from. See
4935    /// [`Literal::Timestamp`].
4936    Date {
4937        days: i32,
4938        text: String,
4939    },
4940    Bool(bool),
4941    Null,
4942    /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
4943    Vector(Vec<f32>),
4944    /// TEXT[] value carried through the prepared-bind path
4945    /// (`= ANY($1)` has no column context to re-parse a `{a,b}`
4946    /// text form, so the array rides the AST natively).
4947    TextArray(Vec<Option<String>>),
4948    /// INT[] value carried through the prepared-bind path.
4949    IntArray(Vec<Option<i32>>),
4950    /// BIGINT[] value carried through the prepared-bind path.
4951    BigIntArray(Vec<Option<i64>>),
4952    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
4953    /// Three independent dimensions: `months` (variable-length;
4954    /// year/month), `days` (fixed 86400 seconds at non-DST, but
4955    /// preserved as its own dimension so `'1 day'` ≠ `'24 hours'`
4956    /// stays distinguishable), and `micros` (sub-day; can carry).
4957    /// `text` keeps the original spelling so Display round-trips
4958    /// byte-for-byte. v7.37.5 β added the `days` field for PG parity.
4959    Interval {
4960        months: i32,
4961        days: i32,
4962        micros: i64,
4963        text: String,
4964    },
4965}
4966
4967#[derive(Debug, Clone, PartialEq, Eq)]
4968pub struct ColumnName {
4969    pub qualifier: Option<String>,
4970    pub name: String,
4971}
4972
4973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4974pub enum BinOp {
4975    Or,
4976    And,
4977    Eq,
4978    NotEq,
4979    /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
4980    /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
4981    /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
4982    /// non-NULL behaviour matches `<>` / `=` exactly. Common in
4983    /// PG-style JOIN ON predicates and pg_dump output.
4984    IsDistinctFrom,
4985    IsNotDistinctFrom,
4986    /// v7.39 (round 353, M9) — MySQL's `DIV`: integer division that
4987    /// truncates TOWARD ZERO (`-7 DIV 2` is -3, measured on MariaDB 11)
4988    /// and answers NULL on a zero divisor. MySQL-dialect only; `/` there
4989    /// is a real division (round 351).
4990    IntDiv,
4991    Lt,
4992    LtEq,
4993    Gt,
4994    GtEq,
4995    Add,
4996    Sub,
4997    Mul,
4998    Div,
4999    /// v7.37.7 C.1.7 — PG / SQL standard integer modulo. Same
5000    /// precedence as Mul/Div; result type follows left operand.
5001    Mod,
5002    /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
5003    /// operands of equal dimension; engine returns `Value::Float(d)`.
5004    L2Distance,
5005    /// v7.39 (read01 geo_ops.c) — `?||` geometric "is parallel".
5006    GeomParallel,
5007    /// v7.39 (read01 rangetypes.c) — range `&<` / `&>`.
5008    OverLeft,
5009    OverRight,
5010    /// v7.39 (read01 geo_ops.c) — `?-|` geometric "is perpendicular".
5011    GeomPerp,
5012    /// v7.39 (read01 geo_ops.c) — `~=` geometric "same as".
5013    GeomSameAs,
5014    /// v7.39 (read01 geo_ops.c) — `##` closest point on the right-hand
5015    /// object to the left-hand one.
5016    ClosestPoint,
5017    /// v7.39 (read01 geo_ops.c) — `?-` points horizontally aligned.
5018    GeomHoriz,
5019    /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
5020    /// more similar" remains true (matches pgvector's published convention).
5021    InnerProduct,
5022    /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
5023    CosineDistance,
5024    /// SQL string concatenation `||`. NULL propagates.
5025    Concat,
5026    /// Bitwise OR `|` on integers.
5027    BitOr,
5028    /// Bitwise AND `&` on integers.
5029    BitAnd,
5030    /// Bitwise XOR `#` on integers and equal-length bit strings.
5031    BitXor,
5032    /// v7.39 (round 407) — MySQL's logical `XOR` operator. Reads both
5033    /// sides as truth values and returns their exclusive-or (`1 XOR 0`
5034    /// is 1, `1 XOR 1` is 0); NULL on either side yields NULL. Only the
5035    /// MySQL dialect produces it; PG has no logical XOR. Its precedence
5036    /// sits between OR (loosest) and AND.
5037    LogicalXor,
5038    /// v4.14 `json -> key` — element access by string key (object)
5039    /// or integer index (array). Returns a JSON value.
5040    JsonGet,
5041    /// v4.14 `json ->> key` — same access, returns the result as
5042    /// TEXT (unwraps a top-level JSON string; renders other scalars
5043    /// as their canonical text).
5044    JsonGetText,
5045    /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
5046    /// text array literal like `'{a,0,b}'`. Returns JSON.
5047    JsonGetPath,
5048    /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
5049    JsonGetPathText,
5050    /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
5051    /// when every key/value in `sub_json` is structurally present in
5052    /// the left side. Matches PG semantics (top-level + recursive).
5053    JsonContains,
5054    /// `@?` — jsonb path existence (jsonb_path_exists).
5055    JsonPathExists,
5056    /// v7.37.6-A `json <@ sub_json` — contained-by. Returns BOOL;
5057    /// `a <@ b` is defined as `b @> a` (same semantics, swapped
5058    /// sides). Eval dispatch reuses `JsonContains` with swapped args.
5059    JsonContainedBy,
5060    /// v7.37.6-A `json ? key` — key-exists. RHS is TEXT;
5061    /// returns BOOL. For an object, true if `key` is an existing
5062    /// member name; for an array, true if any element is the string
5063    /// `key` (PG semantics).
5064    JsonKeyExists,
5065    /// v7.37.6-A `json ?| keys` — any-key-exists. RHS is TEXT[];
5066    /// returns BOOL.
5067    JsonKeysAny,
5068    /// v7.37.6-A `json ?& keys` — all-keys-exist. RHS is TEXT[];
5069    /// returns BOOL.
5070    JsonKeysAll,
5071    /// `jsonb #- path_text[]` — delete the value at a nested path.
5072    /// RHS is a PG text-array literal like `'{a,b}'`; returns JSONB.
5073    JsonDeletePath,
5074    /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
5075    /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
5076    /// tsvector` and engine eval normalises either ordering.
5077    TsMatch,
5078    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contained-in
5079    /// `<<`. LHS network is strictly inside RHS network (no equality).
5080    InetContainedBy,
5081    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contained-in-or-equal
5082    /// `<<=`. LHS network ⊆ RHS network.
5083    InetContainedByEq,
5084    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR strict contains `>>`.
5085    /// LHS network strictly contains RHS network.
5086    InetContains,
5087    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR contains-or-equal `>>=`.
5088    /// LHS network ⊇ RHS network.
5089    InetContainsEq,
5090    /// v7.17.0 Phase 3.P0-47 — PG INET / CIDR network overlap `&&`.
5091    /// True iff either network contains any address of the other.
5092    InetOverlap,
5093    /// v7.39 (round 508) — `?#`, "do these intersect": box/box, line/box,
5094    /// line/line, lseg/box, lseg/line, lseg/lseg, path/path.
5095    Intersects,
5096    /// v7.39 (round 508) — `<^` / `>^`, strictly below / strictly above
5097    /// (point, box).
5098    IsBelow,
5099    IsAbove,
5100    /// v7.39 (round 508) — the `text_pattern_ops` comparisons `~<~`, `~<=~`,
5101    /// `~>~`, `~>=~`: BYTE order, ignoring collation. `'A' ~<~ 'a'` is true
5102    /// where `'A' < 'a'` is false under a non-C collation, which is the
5103    /// whole reason the operator family exists — it is what makes a LIKE
5104    /// prefix index-usable. pg_dump writes these into index definitions.
5105    PatternLt,
5106    PatternLtEq,
5107    PatternGt,
5108    PatternGtEq,
5109}
5110
5111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5112pub enum UnOp {
5113    Not,
5114    Neg,
5115    /// Bitwise NOT `~` on integers.
5116    BitNot,
5117    /// v7.39 (round 507) — unary `+`. SPG had no such operator at all:
5118    /// `SELECT +1` parsed only because the lexer reads `+1` as one signed
5119    /// literal, so `+ 1`, `+a`, `+(1)` and `1 + +1` were all syntax errors
5120    /// while PG18 and MariaDB accept every one of them.
5121    ///
5122    /// It is not a no-op to drop at parse time — PG refuses it on
5123    /// non-numeric operands ("operator does not exist: + boolean"), so the
5124    /// operand's type has to be seen at eval.
5125    Plus,
5126}
5127
5128// --- Display impls (round-trip-safe) --------------------------------------
5129
5130impl Statement {
5131    /// v7.18 — classify whether the statement is read-only at
5132    /// engine level. Used by `spg-sqlx`'s `SpgConnection` to
5133    /// route SELECT-shaped traffic through the fan-out
5134    /// `AsyncReadHandle` (no writer-lock contention) while
5135    /// keeping DML / DDL / TX-control on the single-writer path.
5136    ///
5137    /// The classification matches what
5138    /// `Engine::execute_readonly_with_cancel` accepts: anything
5139    /// that does NOT mutate catalog, statistics, session state,
5140    /// or transaction state. WaitForWalPosition is included
5141    /// (engine returns `Unsupported`, but the classification is
5142    /// semantically read-only — no mutation). Empty is excluded
5143    /// out of an abundance of caution — the no-op routes
5144    /// through the writer so any future side effect lands
5145    /// uniformly.
5146    ///
5147    /// **Not connection-state aware**. `SET LOCAL` / `RESET`
5148    /// affect session parameters and must run on the writer
5149    /// engine that owns the session state; they classify as
5150    /// writer-path here. Same for `BEGIN` / `COMMIT` /
5151    /// `ROLLBACK` / `SAVEPOINT` — transaction control is
5152    /// always writer-path.
5153    /// v7.39 (round 435) — does this statement implicitly COMMIT an open
5154    /// transaction under MySQL?
5155    ///
5156    /// PG runs DDL inside the transaction; MySQL commits before (and after)
5157    /// it, so `START TRANSACTION; INSERT …; CREATE TABLE …; ROLLBACK` keeps
5158    /// the INSERT on MySQL and loses it on PG. Measured on MariaDB 11 for
5159    /// CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, CREATE INDEX and a
5160    /// nested START TRANSACTION; and measured NOT to fire for `CREATE
5161    /// TEMPORARY TABLE`, `SET`, or a SELECT.
5162    ///
5163    /// A positive list, not "everything that is not DML": a statement
5164    /// wrongly listed here commits a client's data early, which is as bad as
5165    /// the divergence it fixes. SPG-only maintenance verbs (VACUUM, DISCARD,
5166    /// COMPACT) are left out — a MySQL session never sends them.
5167    #[must_use]
5168    pub fn mysql_implicit_commit(&self) -> bool {
5169        match self {
5170            // MySQL's documented exception, measured on MariaDB 11: a
5171            // TEMPORARY table is not DDL for this purpose and does not
5172            // commit. (Round 435 got this for free because the parser then
5173            // lowered that spelling to `Statement::Empty`; round 436 made it
5174            // a real CREATE TABLE, and the round-435 pin caught it.)
5175            Self::CreateTable(c) => !c.temporary,
5176            // MySQL commits the open transaction and opens a fresh one.
5177            Self::Begin { .. }
5178            | Self::DropTable { .. }
5179            | Self::DropIndex { .. }
5180            | Self::CreateIndex(_)
5181            | Self::AlterIndex { .. }
5182            | Self::AlterTable(_)
5183            | Self::Truncate { .. }
5184            | Self::Analyze { .. }
5185            | Self::CreateStatistics { .. }
5186            | Self::DropStatistics { .. }
5187            | Self::CreateView { .. }
5188            | Self::DropView { .. }
5189            | Self::CreateMaterializedView { .. }
5190            | Self::RefreshMaterializedView { .. }
5191            | Self::DropMaterializedView { .. }
5192            | Self::CreateSequence(_)
5193            | Self::AlterSequence { .. }
5194            | Self::DropSequence { .. }
5195            | Self::CreateFunction(_)
5196            | Self::DropFunction { .. }
5197            | Self::CreateTrigger(_)
5198            | Self::DropTrigger { .. }
5199            | Self::CreateRule(_)
5200            | Self::DropRule { .. }
5201            | Self::CreateType(_)
5202            | Self::DropType { .. }
5203            | Self::AlterTypeAddValue { .. }
5204            | Self::AlterTypeRenameValue { .. }
5205            | Self::CreateDomain(_)
5206            | Self::AlterDomain { .. }
5207            | Self::DropDomain { .. }
5208            | Self::CreateSchema { .. }
5209            | Self::DropSchema { .. }
5210            | Self::CreateUser { .. }
5211            | Self::DropUser { .. }
5212            | Self::Grant { .. }
5213            | Self::Revoke { .. }
5214            | Self::CreatePolicy(_)
5215            | Self::AlterPolicy(_)
5216            | Self::DropPolicy { .. }
5217            | Self::CommentOn { .. }
5218            | Self::CreateExtension { .. } => true,
5219            _ => false,
5220        }
5221    }
5222
5223    #[must_use]
5224    pub fn is_readonly(&self) -> bool {
5225        match self {
5226            // v7.39 (round 288) — SET CONSTRAINTS changes transaction
5227            // state, and IMMEDIATE can run the deferred checks there and
5228            // then; writer-path.
5229            Statement::SetConstraints { .. } => false,
5230            // v7.39 (round 695) — it writes nothing (SPG has no
5231            // postgresql.auto.conf), but PG classes ALTER SYSTEM as a
5232            // writer and a read-only session refuses it there too.
5233            Statement::AlterSystem { .. } => false,
5234            // Same shape: a no-op here, a writer to PG, so a read-only
5235            // session refuses it as PG's would.
5236            Statement::NoOpPreventedInTransaction { .. } => false,
5237            Statement::DropDatabase { .. } => false,
5238            // v7.39 (round 696) — they perform nothing, so nothing is
5239            // written; PG classes LOCK and the OWNED BY pair as writers and
5240            // a read-only session refuses them there.
5241            Statement::ValidateOnly { .. } => false,
5242            // v7.39 (round 750) — a credential rotation persists.
5243            Statement::AlterRolePassword { .. } => true,
5244            Statement::DropAggregate { .. } => false,
5245            // v7.39 (round 547) — records a GUC default in the catalog.
5246            Statement::SetDbRoleSetting(_) => false,
5247            // v7.39 (round 535) — REINDEX / CLUSTER rebuild nothing here,
5248            // but they name a relation and PG refuses one that is not
5249            // there, so they are not read-only in the sense this asks.
5250            Statement::Maintain { .. } => false,
5251            // v7.39 (round 277) — the prepared-statement surface is
5252            // session state, like SET; writer-path so it lands on the
5253            // engine that owns the session. EXECUTE may also run a
5254            // write, and its body is only known at execution time.
5255            Statement::Prepare { .. }
5256            | Statement::Execute { .. }
5257            | Statement::Deallocate(_)
5258            | Statement::Call(_)
5259            | Statement::PrepareTransaction(_)
5260            | Statement::CreateStatistics { .. }
5261            | Statement::DropStatistics { .. }
5262            // v7.39 (round 318, V51) — KILL signals another connection;
5263            // it must run on the writer path that owns the registry hook.
5264            | Statement::Kill { .. }
5265            // v7.39 (round 320, V53) — DISCARD throws session state away;
5266            // writer path, like SET / RESET.
5267            | Statement::Discard(_) => false,
5268            // v7.39 (round 295, E3 Phase 1b) — a SELECT that asks for row
5269            // locks MUTATES the lock table, so it is not a read. Left as
5270            // a read it went to the read-only executor and the locking
5271            // pre-pass never ran at all — the clause was honoured only
5272            // inside an explicit transaction, and silently ignored in
5273            // autocommit, which is where a queue worker runs it.
5274            Statement::Select(s) if s.locking.is_some() => false,
5275            Statement::Select(_)
5276            | Statement::CopyTo { .. }
5277            | Statement::CopyToFile { .. }
5278            | Statement::Explain(_)
5279            | Statement::ShowTables
5280            | Statement::ShowDatabases
5281            | Statement::ShowCreateTable(_)
5282            | Statement::ShowIndexes(_)
5283            | Statement::ShowStatus
5284            | Statement::ShowVariables
5285            | Statement::ShowVariablesLike(_)
5286            | Statement::ShowProcesslist
5287            | Statement::ShowColumns(_)
5288            | Statement::ShowUsers
5289            | Statement::ShowPublications
5290            | Statement::ShowSubscriptions
5291            | Statement::WaitForWalPosition { .. } => true,
5292            // Everything else mutates catalog, statistics,
5293            // session state, or transaction state — writer path.
5294            // Listed explicitly so a new Statement variant fails
5295            // the match exhaustiveness check and forces a
5296            // classification decision at add-site.
5297            Statement::Empty
5298            // v7.39 (round 169) — VACUUM mutates storage (reclaims
5299            // tombstoned versions): writer path.
5300            | Statement::Vacuum { .. }
5301            | Statement::DropTable { .. }
5302            | Statement::DropIndex { .. }
5303            | Statement::CreateTable(_)
5304            | Statement::CreateExtension(_)
5305            | Statement::DoBlock(_)
5306            | Statement::CreateIndex(_)
5307            | Statement::Insert(_)
5308            | Statement::Update(_)
5309            | Statement::Delete(_)
5310            | Statement::Merge(_)
5311            | Statement::Begin(_)
5312            | Statement::Commit
5313            | Statement::Rollback
5314            | Statement::Savepoint(_)
5315            | Statement::RollbackToSavepoint(_)
5316            | Statement::ReleaseSavepoint(_)
5317            | Statement::CreateUser(_)
5318            | Statement::DropUser { .. }
5319            | Statement::SetRole(_)
5320            | Statement::Grant(_)
5321            | Statement::Revoke(_)
5322            | Statement::CreatePolicy(_)
5323            | Statement::AlterPolicy(_)
5324            | Statement::DropPolicy(_)
5325            | Statement::AlterIndex(_)
5326            | Statement::AlterTable(_)
5327            | Statement::CreatePublication(_)
5328            | Statement::DropPublication { .. }
5329            | Statement::CreateSubscription(_)
5330            | Statement::DropSubscription { .. }
5331            | Statement::Analyze(_)
5332            | Statement::Truncate { .. }
5333            | Statement::CompactColdSegments
5334            | Statement::SetParameter { .. }
5335            | Statement::SetParameterList(_)
5336            | Statement::SetUserVars(..)
5337            | Statement::SetTransaction { .. }
5338            | Statement::ShowParameter(_)
5339            | Statement::ResetParameter(_)
5340            | Statement::CreateFunction(_)
5341            | Statement::CreateTrigger(_)
5342            | Statement::DropTrigger { .. }
5343            | Statement::CreateRule(_)
5344            | Statement::DropRule { .. }
5345            | Statement::DropFunction { .. }
5346            | Statement::CreateSequence(_)
5347            | Statement::AlterSequence(_)
5348            | Statement::DropSequence { .. }
5349            | Statement::CreateView(_)
5350            | Statement::DropView { .. }
5351            | Statement::CreateMaterializedView(_)
5352            | Statement::RefreshMaterializedView { .. }
5353            | Statement::DropMaterializedView { .. }
5354            | Statement::CreateType(_)
5355            | Statement::AlterTypeAddValue { .. }
5356            | Statement::AlterTypeRenameValue { .. }
5357            | Statement::CommentOn { .. }
5358            | Statement::DropType { .. }
5359            | Statement::CreateDomain(_)
5360            | Statement::DropDomain { .. }
5361            | Statement::CreateSchema { .. }
5362            | Statement::DropSchema { .. }
5363            // v7.39 (round 218) — cursors mutate per-session cursor state
5364            // (open/position/close) on the writer engine: writer path.
5365            | Statement::DeclareCursor { .. }
5366            | Statement::FetchCursor { .. }
5367            | Statement::MoveCursor { .. }
5368            | Statement::CloseCursor { .. }
5369            // v7.39 (round 222) — LISTEN/NOTIFY mutate session channel
5370            // state / the notification queue: writer path.
5371            | Statement::Listen(_)
5372            | Statement::Notify { .. }
5373            | Statement::Unlisten(_)
5374            | Statement::CopyFromFile { .. }
5375            | Statement::AlterDomain { .. } => false,
5376        }
5377    }
5378}
5379
5380/// v7.39 (read01 round 57) — a parsed GRANT / REVOKE. The same shape serves
5381/// both; `Statement::Grant` vs `Statement::Revoke` says which way it runs.
5382#[derive(Debug, Clone, PartialEq, Eq)]
5383pub struct GrantStatement {
5384    /// The privileges. EMPTY = `ALL [PRIVILEGES]`. In the role-membership shape
5385    /// (`GRANT devs TO alice`, no ON clause) these words are ROLE NAMES, which
5386    /// is why they keep the case the user typed.
5387    pub privileges: Vec<GrantPriv>,
5388    /// What the privileges are on.
5389    pub object: GrantObject,
5390    /// The roles granted to / revoked from. An empty string entry = PUBLIC.
5391    pub grantees: Vec<String>,
5392    /// GRANT: a trailing `WITH GRANT OPTION`. REVOKE: a leading
5393    /// `GRANT OPTION FOR` (revoke only the right to re-grant, keep the
5394    /// privilege itself).
5395    pub grant_option: bool,
5396}
5397
5398/// v7.39 (read01 round 59) — one privilege in a GRANT, with the optional COLUMN
5399/// list PG allows per privilege: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
5400/// An empty column list means the privilege is table-wide.
5401#[derive(Debug, Clone, PartialEq, Eq)]
5402pub struct GrantPriv {
5403    pub word: String,
5404    pub columns: Vec<String>,
5405}
5406
5407/// v7.39 (read01 round 57) — the object a GRANT names. SPG enforces TABLE
5408/// privileges; every other object class parses and is accepted as a no-op, so
5409/// a pg_dump that grants on schemas / sequences / functions still restores.
5410#[derive(Debug, Clone, PartialEq, Eq)]
5411pub enum GrantObject {
5412    /// `ON [TABLE] a, b` — the enforced case.
5413    Tables(Vec<String>),
5414    /// v7.39 (read01 round 58) — `GRANT devs TO alice` / `REVOKE devs FROM
5415    /// alice`: role MEMBERSHIP, which has no ON clause at all. Carries the
5416    /// granted roles; the grantees are the members.
5417    Roles(Vec<String>),
5418    /// v7.39 (read01 round 60) — `ON SEQUENCE a, b`. A sequence's meaningful
5419    /// privileges are SELECT (currval), UPDATE (setval) and USAGE (nextval).
5420    Sequences(Vec<String>),
5421    /// v7.39 (read01 round 60) — `ON SCHEMA public`. USAGE / CREATE.
5422    Schemas(Vec<String>),
5423    /// v7.39 (read01 round 60) — `ON DATABASE app`. CREATE / CONNECT / TEMP.
5424    Databases(Vec<String>),
5425    /// v7.39 (read01 round 61) — `ON FUNCTION f(int)`. The names are bare
5426    /// (SPG keys functions by name); the argument list parses and is dropped.
5427    Functions(Vec<(String, Option<Vec<String>>)>),
5428    /// v7.39 (read01 round 61) — `ON ALL TABLES IN SCHEMA public`: expands to
5429    /// every table at GRANT time, exactly like PG.
5430    AllTablesInSchema,
5431    /// `ON TYPE / LANGUAGE / …`. Carries the object-class word for the no-op
5432    /// message.
5433    Other(String),
5434}
5435
5436impl GrantStatement {
5437    /// Round-trip text. `grant = false` renders the REVOKE form.
5438    fn render(&self, grant: bool) -> alloc::string::String {
5439        use core::fmt::Write as _;
5440        let mut s = alloc::string::String::new();
5441        let privs = if self.privileges.is_empty() {
5442            alloc::string::String::from("ALL")
5443        } else {
5444            let parts: Vec<_> = self
5445                .privileges
5446                .iter()
5447                .map(|p| {
5448                    if p.columns.is_empty() {
5449                        p.word.clone()
5450                    } else {
5451                        let cols: Vec<_> = p.columns.iter().map(|c| quote_ident(c)).collect();
5452                        alloc::format!("{} ({})", p.word, cols.join(", "))
5453                    }
5454                })
5455                .collect();
5456            parts.join(", ")
5457        };
5458        let obj = match &self.object {
5459            GrantObject::Tables(t) => {
5460                let names: Vec<_> = t.iter().map(|n| quote_ident(n)).collect();
5461                alloc::format!("TABLE {}", names.join(", "))
5462            }
5463            GrantObject::Roles(r) => {
5464                let names: Vec<_> = r.iter().map(|n| quote_ident(n)).collect();
5465                names.join(", ")
5466            }
5467            GrantObject::Sequences(n) => {
5468                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5469                alloc::format!("SEQUENCE {}", names.join(", "))
5470            }
5471            GrantObject::Schemas(n) => {
5472                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5473                alloc::format!("SCHEMA {}", names.join(", "))
5474            }
5475            GrantObject::Databases(n) => {
5476                let names: Vec<_> = n.iter().map(|x| quote_ident(x)).collect();
5477                alloc::format!("DATABASE {}", names.join(", "))
5478            }
5479            GrantObject::Functions(n) => {
5480                let names: Vec<_> = n
5481                    .iter()
5482                    .map(|(name, args)| match args {
5483                        Some(a) => alloc::format!("{}({})", quote_ident(name), a.join(", ")),
5484                        None => quote_ident(name),
5485                    })
5486                    .collect();
5487                alloc::format!("FUNCTION {}", names.join(", "))
5488            }
5489            GrantObject::AllTablesInSchema => "ALL TABLES IN SCHEMA public".into(),
5490            GrantObject::Other(k) => k.clone(),
5491        };
5492        let who: Vec<_> = self
5493            .grantees
5494            .iter()
5495            .map(|g| {
5496                if g.is_empty() {
5497                    "PUBLIC".into()
5498                } else {
5499                    quote_ident(g)
5500                }
5501            })
5502            .collect();
5503        if let GrantObject::Roles(_) = &self.object {
5504            let _ = if grant {
5505                write!(s, "GRANT {obj} TO {}", who.join(", "))
5506            } else {
5507                write!(s, "REVOKE {obj} FROM {}", who.join(", "))
5508            };
5509            return s;
5510        }
5511        if grant {
5512            let _ = write!(s, "GRANT {privs} ON {obj} TO {}", who.join(", "));
5513            if self.grant_option {
5514                s.push_str(" WITH GRANT OPTION");
5515            }
5516        } else {
5517            s.push_str("REVOKE ");
5518            if self.grant_option {
5519                s.push_str("GRANT OPTION FOR ");
5520            }
5521            let _ = write!(s, "{privs} ON {obj} FROM {}", who.join(", "));
5522        }
5523        s
5524    }
5525}
5526
5527impl fmt::Display for Statement {
5528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5529        match self {
5530            Self::Empty => Ok(()),
5531            // v7.39 (round 695) — deparsed the way PG writes it.
5532            // v7.39 (round 696) — never deparsed into a dump (nothing is
5533            // stored), so the shortest faithful spelling of what it was.
5534            Self::DropAggregate { if_exists, items } => {
5535                f.write_str("DROP AGGREGATE ")?;
5536                if *if_exists {
5537                    f.write_str("IF EXISTS ")?;
5538                }
5539                for (i, (name, args)) in items.iter().enumerate() {
5540                    if i > 0 {
5541                        f.write_str(", ")?;
5542                    }
5543                    match args {
5544                        Some(a) => write!(f, "{name}({})", a.join(", "))?,
5545                        None => write!(f, "{name}(*)")?,
5546                    }
5547                }
5548                Ok(())
5549            }
5550            Self::AlterRolePassword { name, password } => {
5551                write!(f, "ALTER ROLE {}", quote_ident(name))?;
5552                match password {
5553                    Some(_) => f.write_str(" PASSWORD '<redacted>'"),
5554                    None => f.write_str(" PASSWORD NULL"),
5555                }
5556            }
5557            Self::ValidateOnly { kind, names } => match kind {
5558                ValidateOnlyKind::LockTable => write!(f, "LOCK TABLE {}", names.join(", ")),
5559                ValidateOnlyKind::RoleName => {
5560                    write!(f, "DROP OWNED BY {}", names.join(", "))
5561                }
5562                ValidateOnlyKind::SecurityLabel => f.write_str("SECURITY LABEL"),
5563                ValidateOnlyKind::ExtensionAvailable => {
5564                    write!(f, "CREATE EXTENSION {}", names.join(", "))
5565                }
5566                ValidateOnlyKind::ForeignInfra => f.write_str("CREATE SERVER"),
5567                ValidateOnlyKind::CollationName => {
5568                    write!(f, "DROP COLLATION {}", names.join(", "))
5569                }
5570                ValidateOnlyKind::TsConfigName => {
5571                    write!(f, "DROP TEXT SEARCH CONFIGURATION {}", names.join(", "))
5572                }
5573                ValidateOnlyKind::EventTriggerName => {
5574                    write!(f, "DROP EVENT TRIGGER {}", names.join(", "))
5575                }
5576                ValidateOnlyKind::TablespaceName => {
5577                    write!(f, "DROP TABLESPACE {}", names.join(", "))
5578                }
5579                ValidateOnlyKind::LargeObjectOid => {
5580                    write!(f, "ALTER LARGE OBJECT {}", names.join(", "))
5581                }
5582                ValidateOnlyKind::TypeName => write!(f, "ALTER TYPE {}", names.join(", ")),
5583                ValidateOnlyKind::AggregateName => {
5584                    write!(f, "ALTER AGGREGATE {}", names.join(", "))
5585                }
5586                ValidateOnlyKind::ConversionName => {
5587                    write!(f, "DROP CONVERSION {}", names.join(", "))
5588                }
5589                ValidateOnlyKind::LanguageName => {
5590                    write!(f, "DROP LANGUAGE {}", names.join(", "))
5591                }
5592                ValidateOnlyKind::ExtensionInstalled => {
5593                    write!(f, "DROP EXTENSION {}", names.join(", "))
5594                }
5595            },
5596            Self::AlterSystem { parameter } => match parameter {
5597                Some(p) => write!(f, "ALTER SYSTEM RESET {p}"),
5598                None => f.write_str("ALTER SYSTEM RESET ALL"),
5599            },
5600            // v7.39 (round 547) — round-trips as PG writes it.
5601            Self::SetDbRoleSetting(st) => {
5602                match (&st.database, &st.role) {
5603                    (Some(d), None) => write!(f, "ALTER DATABASE {d}")?,
5604                    (_, Some(r)) => write!(f, "ALTER ROLE {r}")?,
5605                    (None, None) => f.write_str("ALTER ROLE ALL")?,
5606                }
5607                if let (Some(d), Some(_)) = (&st.database, &st.role) {
5608                    write!(f, " IN DATABASE {d}")?;
5609                }
5610                match (&st.param, &st.value) {
5611                    (None, _) => f.write_str(" RESET ALL"),
5612                    (Some(p), None) => write!(f, " RESET {p}"),
5613                    (Some(p), Some(v)) => write!(f, " SET {p} = '{v}'"),
5614                }
5615            }
5616            Self::Maintain {
5617                kind,
5618                concurrently,
5619                target,
5620            } => {
5621                f.write_str(match kind {
5622                    crate::ast::MaintainKind::ClusterRelation => "CLUSTER ",
5623                    _ => "REINDEX ",
5624                })?;
5625                if *concurrently {
5626                    f.write_str("CONCURRENTLY ")?;
5627                }
5628                if let Some(t) = target {
5629                    f.write_str(t)?;
5630                }
5631                Ok(())
5632            }
5633            Self::DropDatabase { name, if_exists } => {
5634                f.write_str("DROP DATABASE ")?;
5635                if *if_exists {
5636                    f.write_str("IF EXISTS ")?;
5637                }
5638                f.write_str(name)
5639            }
5640            Self::NoOpPreventedInTransaction { what, .. } => f.write_str(what),
5641            Self::SetConstraints { names, deferred } => {
5642                f.write_str("SET CONSTRAINTS ")?;
5643                if names.is_empty() {
5644                    f.write_str("ALL")?;
5645                } else {
5646                    for (i, n) in names.iter().enumerate() {
5647                        if i > 0 {
5648                            f.write_str(", ")?;
5649                        }
5650                        f.write_str(n)?;
5651                    }
5652                }
5653                f.write_str(if *deferred { " DEFERRED" } else { " IMMEDIATE" })
5654            }
5655            // v7.39 (round 277) — the source text is kept verbatim so
5656            // `pg_prepared_statements.statement` can report it the way
5657            // PG does (the whole PREPARE statement, not just the body).
5658            Self::Prepare { source, .. } => f.write_str(source),
5659            Self::Execute { name, args } => {
5660                write!(f, "EXECUTE {}", quote_ident(name))?;
5661                if !args.is_empty() {
5662                    f.write_str("(")?;
5663                    for (i, a) in args.iter().enumerate() {
5664                        if i > 0 {
5665                            f.write_str(", ")?;
5666                        }
5667                        write!(f, "{a}")?;
5668                    }
5669                    f.write_str(")")?;
5670                }
5671                Ok(())
5672            }
5673            Self::CreateStatistics {
5674                name,
5675                if_not_exists,
5676                kinds,
5677                columns,
5678                table,
5679            } => {
5680                f.write_str("CREATE STATISTICS ")?;
5681                if *if_not_exists {
5682                    f.write_str("IF NOT EXISTS ")?;
5683                }
5684                write!(f, "{}", quote_ident(name))?;
5685                if !kinds.is_empty() {
5686                    write!(f, " ({})", kinds.join(", "))?;
5687                }
5688                write!(f, " ON {} FROM {}", columns.join(", "), quote_ident(table))
5689            }
5690            Self::DropStatistics { name, if_exists } => {
5691                f.write_str("DROP STATISTICS ")?;
5692                if *if_exists {
5693                    f.write_str("IF EXISTS ")?;
5694                }
5695                write!(f, "{}", quote_ident(name))
5696            }
5697            Self::Call(n) => write!(f, "CALL {}()", quote_ident(n)),
5698            Self::PrepareTransaction(gid) => write!(f, "PREPARE TRANSACTION '{gid}'"),
5699            Self::Deallocate(None) => f.write_str("DEALLOCATE ALL"),
5700            Self::Deallocate(Some(n)) => write!(f, "DEALLOCATE {}", quote_ident(n)),
5701            Self::DeclareCursor {
5702                name,
5703                scroll,
5704                hold,
5705                query,
5706            } => {
5707                write!(f, "DECLARE {} ", quote_ident(name))?;
5708                match scroll {
5709                    Some(true) => f.write_str("SCROLL ")?,
5710                    Some(false) => f.write_str("NO SCROLL ")?,
5711                    None => {}
5712                }
5713                f.write_str("CURSOR ")?;
5714                if *hold {
5715                    f.write_str("WITH HOLD ")?;
5716                }
5717                write!(f, "FOR {query}")
5718            }
5719            Self::FetchCursor { name, direction } => {
5720                write!(f, "FETCH {direction} FROM {}", quote_ident(name))
5721            }
5722            Self::MoveCursor { name, direction } => {
5723                write!(f, "MOVE {direction} FROM {}", quote_ident(name))
5724            }
5725            Self::CloseCursor { name } => match name {
5726                Some(n) => write!(f, "CLOSE {}", quote_ident(n)),
5727                None => f.write_str("CLOSE ALL"),
5728            },
5729            Self::Listen(ch) => write!(f, "LISTEN {}", quote_ident(ch)),
5730            Self::Notify { channel, payload } => {
5731                write!(f, "NOTIFY {}", quote_ident(channel))?;
5732                if let Some(p) = payload {
5733                    write!(f, ", '{}'", p.replace('\'', "''"))?;
5734                }
5735                Ok(())
5736            }
5737            Self::Unlisten(ch) => match ch {
5738                Some(c) => write!(f, "UNLISTEN {}", quote_ident(c)),
5739                None => f.write_str("UNLISTEN *"),
5740            },
5741            Self::CopyTo {
5742                table,
5743                columns,
5744                query,
5745                options,
5746            } => {
5747                if let Some(q) = query {
5748                    write!(f, "COPY ({q})")?;
5749                } else {
5750                    write!(f, "COPY {table}")?;
5751                    if let Some(cols) = columns {
5752                        write!(f, " ({})", cols.join(", "))?;
5753                    }
5754                }
5755                write!(f, " TO STDOUT")?;
5756                let mut parts: Vec<String> = Vec::new();
5757                if options.format == CopyFormat::Csv {
5758                    parts.push("FORMAT csv".to_string());
5759                }
5760                if options.header {
5761                    parts.push("HEADER true".to_string());
5762                }
5763                if let Some(d) = options.delimiter {
5764                    parts.push(alloc::format!("DELIMITER '{d}'"));
5765                }
5766                if let Some(n) = &options.null_str {
5767                    parts.push(alloc::format!("NULL '{n}'"));
5768                }
5769                if let Some(q) = options.quote {
5770                    parts.push(alloc::format!("QUOTE '{q}'"));
5771                }
5772                if !parts.is_empty() {
5773                    write!(f, " WITH ({})", parts.join(", "))?;
5774                }
5775                Ok(())
5776            }
5777            Self::CopyFromFile {
5778                table,
5779                columns,
5780                path,
5781                options,
5782            } => {
5783                write!(f, "COPY {table}")?;
5784                if let Some(cols) = columns {
5785                    write!(f, " ({})", cols.join(", "))?;
5786                }
5787                write!(f, " FROM '{path}'")?;
5788                let mut parts: Vec<String> = Vec::new();
5789                if options.format == CopyFormat::Csv {
5790                    parts.push("FORMAT csv".to_string());
5791                }
5792                if options.header {
5793                    parts.push("HEADER true".to_string());
5794                }
5795                if let Some(d) = options.delimiter {
5796                    parts.push(alloc::format!("DELIMITER '{d}'"));
5797                }
5798                if let Some(n) = &options.null_str {
5799                    parts.push(alloc::format!("NULL '{n}'"));
5800                }
5801                if let Some(q) = options.quote {
5802                    parts.push(alloc::format!("QUOTE '{q}'"));
5803                }
5804                if !parts.is_empty() {
5805                    write!(f, " WITH ({})", parts.join(", "))?;
5806                }
5807                Ok(())
5808            }
5809            Self::CopyToFile {
5810                table,
5811                columns,
5812                query,
5813                path,
5814                options,
5815            } => {
5816                if let Some(q) = query {
5817                    write!(f, "COPY ({q})")?;
5818                } else {
5819                    write!(f, "COPY {table}")?;
5820                    if let Some(cols) = columns {
5821                        write!(f, " ({})", cols.join(", "))?;
5822                    }
5823                }
5824                write!(f, " TO '{path}'")?;
5825                let mut parts: Vec<String> = Vec::new();
5826                if options.format == CopyFormat::Csv {
5827                    parts.push("FORMAT csv".to_string());
5828                }
5829                if options.header {
5830                    parts.push("HEADER true".to_string());
5831                }
5832                if let Some(d) = options.delimiter {
5833                    parts.push(alloc::format!("DELIMITER '{d}'"));
5834                }
5835                if let Some(n) = &options.null_str {
5836                    parts.push(alloc::format!("NULL '{n}'"));
5837                }
5838                if let Some(q) = options.quote {
5839                    parts.push(alloc::format!("QUOTE '{q}'"));
5840                }
5841                if !parts.is_empty() {
5842                    write!(f, " WITH ({})", parts.join(", "))?;
5843                }
5844                Ok(())
5845            }
5846            Self::AlterDomain { name, action } => {
5847                write!(f, "ALTER DOMAIN {name} ")?;
5848                match action {
5849                    AlterDomainAction::AddConstraint { name: cn, check } => match cn {
5850                        Some(cn) => write!(f, "ADD CONSTRAINT {cn} CHECK ({check})"),
5851                        None => write!(f, "ADD CHECK ({check})"),
5852                    },
5853                    AlterDomainAction::DropConstraint {
5854                        name: cn,
5855                        if_exists,
5856                    } => {
5857                        if *if_exists {
5858                            write!(f, "DROP CONSTRAINT IF EXISTS {cn}")
5859                        } else {
5860                            write!(f, "DROP CONSTRAINT {cn}")
5861                        }
5862                    }
5863                    AlterDomainAction::SetDefault(e) => write!(f, "SET DEFAULT {e}"),
5864                    AlterDomainAction::DropDefault => f.write_str("DROP DEFAULT"),
5865                    AlterDomainAction::SetNotNull => f.write_str("SET NOT NULL"),
5866                    AlterDomainAction::DropNotNull => f.write_str("DROP NOT NULL"),
5867                    AlterDomainAction::RenameTo(n) => write!(f, "RENAME TO {n}"),
5868                }
5869            }
5870            Self::Truncate {
5871                tables,
5872                restart_identity,
5873                cascade,
5874                only,
5875            } => {
5876                f.write_str("TRUNCATE TABLE ")?;
5877                if *only {
5878                    f.write_str("ONLY ")?;
5879                }
5880                for (i, t) in tables.iter().enumerate() {
5881                    if i > 0 {
5882                        f.write_str(", ")?;
5883                    }
5884                    f.write_str(t)?;
5885                }
5886                if *restart_identity {
5887                    f.write_str(" RESTART IDENTITY")?;
5888                }
5889                if *cascade {
5890                    f.write_str(" CASCADE")?;
5891                }
5892                Ok(())
5893            }
5894            Self::DropTable { names, if_exists } => {
5895                f.write_str("DROP TABLE ")?;
5896                if *if_exists {
5897                    f.write_str("IF EXISTS ")?;
5898                }
5899                for (i, n) in names.iter().enumerate() {
5900                    if i > 0 {
5901                        f.write_str(", ")?;
5902                    }
5903                    write!(f, "{}", quote_ident(n))?;
5904                }
5905                Ok(())
5906            }
5907            Self::DropIndex { name, if_exists } => {
5908                f.write_str("DROP INDEX ")?;
5909                if *if_exists {
5910                    f.write_str("IF EXISTS ")?;
5911                }
5912                write!(f, "{}", quote_ident(name))
5913            }
5914            Self::Select(s) => s.fmt(f),
5915            Self::CreateTable(s) => s.fmt(f),
5916            Self::CreateIndex(s) => s.fmt(f),
5917            Self::Insert(s) => s.fmt(f),
5918            Self::Update(s) => s.fmt(f),
5919            Self::Delete(s) => s.fmt(f),
5920            Self::Merge(s) => s.fmt(f),
5921            Self::Vacuum { table, analyze } => {
5922                f.write_str("VACUUM")?;
5923                if *analyze {
5924                    f.write_str(" ANALYZE")?;
5925                }
5926                if let Some(t) = table {
5927                    write!(f, " {}", quote_ident(t))?;
5928                }
5929                Ok(())
5930            }
5931            Self::Begin(modes) => {
5932                f.write_str("BEGIN")?;
5933                if let Some(level) = modes.isolation {
5934                    write!(f, " ISOLATION LEVEL {level}")?;
5935                }
5936                match modes.read_only {
5937                    Some(true) => f.write_str(" READ ONLY")?,
5938                    Some(false) => f.write_str(" READ WRITE")?,
5939                    None => {}
5940                }
5941                Ok(())
5942            }
5943            Self::Commit => f.write_str("COMMIT"),
5944            Self::Rollback => f.write_str("ROLLBACK"),
5945            Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
5946            Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
5947            Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
5948            Self::ShowTables => f.write_str("SHOW TABLES"),
5949            Self::ShowDatabases => f.write_str("SHOW DATABASES"),
5950            Self::ShowCreateTable(t) => write!(f, "SHOW CREATE TABLE {}", quote_ident(t)),
5951            Self::ShowIndexes(t) => write!(f, "SHOW INDEXES FROM {}", quote_ident(t)),
5952            Self::ShowStatus => f.write_str("SHOW STATUS"),
5953            Self::ShowVariables => f.write_str("SHOW VARIABLES"),
5954            Self::ShowVariablesLike(p) => {
5955                write!(f, "SHOW VARIABLES LIKE '{}'", p.replace('\'', "''"))
5956            }
5957            Self::ShowProcesslist => f.write_str("SHOW PROCESSLIST"),
5958            Self::Discard(t) => write!(f, "DISCARD {t}"),
5959            Self::Kill { query_only, id } => {
5960                if *query_only {
5961                    write!(f, "KILL QUERY {id}")
5962                } else {
5963                    write!(f, "KILL CONNECTION {id}")
5964                }
5965            }
5966            Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
5967            Self::CreateUser(s) => write!(
5968                f,
5969                "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
5970                quote_ident(&s.name),
5971                s.role
5972            ),
5973            Self::DropUser { name, if_exists } => {
5974                let ie = if *if_exists { "IF EXISTS " } else { "" };
5975                write!(f, "DROP USER {ie}{}", quote_ident(name))
5976            }
5977            Self::SetRole(Some(r)) => write!(f, "SET ROLE {}", quote_ident(r)),
5978            Self::SetRole(None) => f.write_str("RESET ROLE"),
5979            Self::Grant(g) => write!(f, "{}", g.render(true)),
5980            Self::Revoke(g) => write!(f, "{}", g.render(false)),
5981            Self::CreatePolicy(s) => {
5982                write!(
5983                    f,
5984                    "CREATE POLICY {} ON {}",
5985                    quote_ident(&s.name),
5986                    quote_ident(&s.table)
5987                )?;
5988                if !s.permissive {
5989                    f.write_str(" AS RESTRICTIVE")?;
5990                }
5991                if !matches!(s.cmd, PolicyCmd::All) {
5992                    let w = match s.cmd {
5993                        PolicyCmd::Select => "SELECT",
5994                        PolicyCmd::Insert => "INSERT",
5995                        PolicyCmd::Update => "UPDATE",
5996                        PolicyCmd::Delete => "DELETE",
5997                        PolicyCmd::All => unreachable!(),
5998                    };
5999                    write!(f, " FOR {w}")?;
6000                }
6001                if !s.roles.is_empty() {
6002                    write!(f, " TO {}", s.roles.join(", "))?;
6003                }
6004                if let Some(u) = &s.using {
6005                    write!(f, " USING ({u})")?;
6006                }
6007                if let Some(c) = &s.with_check {
6008                    write!(f, " WITH CHECK ({c})")?;
6009                }
6010                Ok(())
6011            }
6012            Self::AlterPolicy(s) => {
6013                write!(
6014                    f,
6015                    "ALTER POLICY {} ON {}",
6016                    quote_ident(&s.name),
6017                    quote_ident(&s.table)
6018                )?;
6019                if let Some(nn) = &s.rename_to {
6020                    return write!(f, " RENAME TO {}", quote_ident(nn));
6021                }
6022                if let Some(roles) = &s.roles {
6023                    write!(f, " TO {}", roles.join(", "))?;
6024                }
6025                if let Some(u) = &s.using {
6026                    write!(f, " USING ({u})")?;
6027                }
6028                if let Some(c) = &s.with_check {
6029                    write!(f, " WITH CHECK ({c})")?;
6030                }
6031                Ok(())
6032            }
6033            Self::DropPolicy(s) => {
6034                f.write_str("DROP POLICY ")?;
6035                if s.if_exists {
6036                    f.write_str("IF EXISTS ")?;
6037                }
6038                write!(f, "{} ON {}", quote_ident(&s.name), quote_ident(&s.table))
6039            }
6040            Self::ShowUsers => f.write_str("SHOW USERS"),
6041            Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
6042            Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
6043            Self::CreateSubscription(s) => {
6044                write!(
6045                    f,
6046                    "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
6047                    quote_ident(&s.name),
6048                    s.conn_str.replace('\'', "''")
6049                )?;
6050                for (i, p) in s.publications.iter().enumerate() {
6051                    if i > 0 {
6052                        f.write_str(", ")?;
6053                    }
6054                    write!(f, "{}", quote_ident(p))?;
6055                }
6056                Ok(())
6057            }
6058            Self::DropSubscription { name, if_exists } => {
6059                let opt = if *if_exists { "IF EXISTS " } else { "" };
6060                write!(f, "DROP SUBSCRIPTION {opt}{}", quote_ident(name))
6061            }
6062            Self::WaitForWalPosition { pos, timeout_ms } => {
6063                write!(f, "WAIT FOR WAL POSITION {pos}")?;
6064                if let Some(ms) = timeout_ms {
6065                    write!(f, " WITH TIMEOUT {ms}")?;
6066                }
6067                Ok(())
6068            }
6069            Self::Analyze(None) => f.write_str("ANALYZE"),
6070            Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
6071            Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
6072            Self::Explain(e) => {
6073                if e.suggest {
6074                    write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
6075                } else if e.analyze {
6076                    write!(f, "EXPLAIN ANALYZE {}", e.inner)
6077                } else {
6078                    write!(f, "EXPLAIN {}", e.inner)
6079                }
6080            }
6081            Self::AlterIndex(a) => {
6082                write!(f, "ALTER INDEX ")?;
6083                match &a.target {
6084                    // Parameters are consumed, not stored; the shortest
6085                    // faithful spelling.
6086                    AlterIndexTarget::StorageParams => {
6087                        write!(f, "{} SET ()", quote_ident(&a.name))
6088                    }
6089                    AlterIndexTarget::Rebuild { encoding } => {
6090                        write!(f, "{} REBUILD", quote_ident(&a.name))?;
6091                        if let Some(enc) = encoding {
6092                            write!(f, " WITH (encoding = {enc})")?;
6093                        }
6094                        Ok(())
6095                    }
6096                    AlterIndexTarget::Rename { new, if_exists } => {
6097                        if *if_exists {
6098                            f.write_str("IF EXISTS ")?;
6099                        }
6100                        write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
6101                    }
6102                }
6103            }
6104            Self::AlterTable(a) => {
6105                write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
6106                for (i, t) in a.targets.iter().enumerate() {
6107                    if i > 0 {
6108                        f.write_str(", ")?;
6109                    }
6110                    fmt_alter_target(f, t)?;
6111                }
6112                Ok(())
6113            }
6114            Self::CreatePublication(p) => {
6115                write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
6116                match &p.scope {
6117                    PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
6118                    PublicationScope::ForTables(ts) => {
6119                        f.write_str(" FOR TABLE ")?;
6120                        for (i, t) in ts.iter().enumerate() {
6121                            if i > 0 {
6122                                f.write_str(", ")?;
6123                            }
6124                            write!(f, "{}", quote_ident(t))?;
6125                        }
6126                        Ok(())
6127                    }
6128                    PublicationScope::TablesInSchema(schema) => {
6129                        write!(f, " FOR TABLES IN SCHEMA {}", quote_ident(schema))?;
6130                        Ok(())
6131                    }
6132                    PublicationScope::AllTablesExcept(ts) => {
6133                        f.write_str(" FOR ALL TABLES EXCEPT ")?;
6134                        for (i, t) in ts.iter().enumerate() {
6135                            if i > 0 {
6136                                f.write_str(", ")?;
6137                            }
6138                            write!(f, "{}", quote_ident(t))?;
6139                        }
6140                        Ok(())
6141                    }
6142                }
6143            }
6144            Self::CreateExtension(name) => {
6145                write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
6146            }
6147            Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
6148            Self::DropPublication { name, if_exists } => {
6149                let opt = if *if_exists { "IF EXISTS " } else { "" };
6150                write!(f, "DROP PUBLICATION {opt}{}", quote_ident(name))
6151            }
6152            Self::SetParameter { name, value, local } => {
6153                write!(f, "SET {}{name} = ", if *local { "LOCAL " } else { "" })?;
6154                match value {
6155                    SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
6156                    SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
6157                    SetValue::Default => f.write_str("DEFAULT"),
6158                }
6159            }
6160            Self::SetTransaction { modes } => {
6161                f.write_str("SET TRANSACTION")?;
6162                if let Some(isolation) = modes.isolation {
6163                    let name = match isolation {
6164                        IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
6165                        IsolationLevel::ReadCommitted => "READ COMMITTED",
6166                        IsolationLevel::RepeatableRead => "REPEATABLE READ",
6167                        IsolationLevel::Serializable => "SERIALIZABLE",
6168                    };
6169                    write!(f, " ISOLATION LEVEL {name}")?;
6170                }
6171                match modes.read_only {
6172                    Some(true) => f.write_str(" READ ONLY")?,
6173                    Some(false) => f.write_str(" READ WRITE")?,
6174                    None => {}
6175                }
6176                Ok(())
6177            }
6178            Self::ShowParameter(name) => write!(f, "SHOW {name}"),
6179            Self::SetUserVars(assigns, _) => {
6180                f.write_str("SET ")?;
6181                for (i, (name, value)) in assigns.iter().enumerate() {
6182                    if i > 0 {
6183                        f.write_str(", ")?;
6184                    }
6185                    write!(f, "@{name} = {value}")?;
6186                }
6187                Ok(())
6188            }
6189            Self::SetParameterList(pairs) => {
6190                f.write_str("SET ")?;
6191                for (i, (name, value)) in pairs.iter().enumerate() {
6192                    if i > 0 {
6193                        f.write_str(", ")?;
6194                    }
6195                    write!(f, "{name} = ")?;
6196                    match value {
6197                        SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
6198                        SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
6199                        SetValue::Default => f.write_str("DEFAULT")?,
6200                    }
6201                }
6202                Ok(())
6203            }
6204            Self::ResetParameter(None) => f.write_str("RESET ALL"),
6205            Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
6206            Self::CreateFunction(s) => s.fmt(f),
6207            Self::CreateTrigger(s) => s.fmt(f),
6208            Self::DropTrigger {
6209                name,
6210                table,
6211                if_exists,
6212            } => {
6213                f.write_str("DROP TRIGGER ")?;
6214                if *if_exists {
6215                    f.write_str("IF EXISTS ")?;
6216                }
6217                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6218            }
6219            Self::DropFunction {
6220                name,
6221                args,
6222                if_exists,
6223            } => {
6224                f.write_str("DROP FUNCTION ")?;
6225                if *if_exists {
6226                    f.write_str("IF EXISTS ")?;
6227                }
6228                write!(f, "{}", quote_ident(name))?;
6229                if let Some(a) = args {
6230                    write!(f, "({})", a.join(", "))?;
6231                }
6232                Ok(())
6233            }
6234            Self::CreateSequence(s) => s.fmt(f),
6235            Self::AlterSequence(s) => s.fmt(f),
6236            Self::DropSequence { names, if_exists } => {
6237                f.write_str("DROP SEQUENCE ")?;
6238                if *if_exists {
6239                    f.write_str("IF EXISTS ")?;
6240                }
6241                for (i, n) in names.iter().enumerate() {
6242                    if i > 0 {
6243                        f.write_str(", ")?;
6244                    }
6245                    write!(f, "{}", quote_ident(n))?;
6246                }
6247                Ok(())
6248            }
6249            Self::CreateView(v) => v.fmt(f),
6250            Self::DropView { names, if_exists } => {
6251                f.write_str("DROP VIEW ")?;
6252                if *if_exists {
6253                    f.write_str("IF EXISTS ")?;
6254                }
6255                for (i, n) in names.iter().enumerate() {
6256                    if i > 0 {
6257                        f.write_str(", ")?;
6258                    }
6259                    write!(f, "{}", quote_ident(n))?;
6260                }
6261                Ok(())
6262            }
6263            Self::CreateMaterializedView(v) => v.fmt(f),
6264            Self::RefreshMaterializedView { name, with_data } => {
6265                write!(f, "REFRESH MATERIALIZED VIEW {}", quote_ident(name))?;
6266                if !*with_data {
6267                    f.write_str(" WITH NO DATA")?;
6268                }
6269                Ok(())
6270            }
6271            Self::DropMaterializedView { names, if_exists } => {
6272                f.write_str("DROP MATERIALIZED VIEW ")?;
6273                if *if_exists {
6274                    f.write_str("IF EXISTS ")?;
6275                }
6276                for (i, n) in names.iter().enumerate() {
6277                    if i > 0 {
6278                        f.write_str(", ")?;
6279                    }
6280                    write!(f, "{}", quote_ident(n))?;
6281                }
6282                Ok(())
6283            }
6284            Self::CreateType(t) => t.fmt(f),
6285            Self::CommentOn {
6286                kind,
6287                name,
6288                comment,
6289            } => {
6290                let body = match comment {
6291                    Some(c) => alloc::format!("'{}'", c.replace('\'', "''")),
6292                    None => "NULL".into(),
6293                };
6294                write!(f, "COMMENT ON {} {name} IS {body}", kind.to_uppercase())
6295            }
6296            Self::AlterTypeRenameValue {
6297                type_name,
6298                old,
6299                new,
6300            } => write!(
6301                f,
6302                "ALTER TYPE {} RENAME VALUE '{}' TO '{}'",
6303                quote_ident(type_name),
6304                old.replace('\'', "''"),
6305                new.replace('\'', "''")
6306            ),
6307            Self::AlterTypeAddValue {
6308                type_name,
6309                label,
6310                if_not_exists,
6311                position,
6312            } => {
6313                write!(f, "ALTER TYPE {type_name} ADD VALUE ")?;
6314                if *if_not_exists {
6315                    write!(f, "IF NOT EXISTS ")?;
6316                }
6317                write!(f, "'{label}'")?;
6318                if let Some((is_before, anchor)) = position {
6319                    write!(
6320                        f,
6321                        " {} '{anchor}'",
6322                        if *is_before { "BEFORE" } else { "AFTER" }
6323                    )?;
6324                }
6325                Ok(())
6326            }
6327            Self::DropType { names, if_exists } => {
6328                f.write_str("DROP TYPE ")?;
6329                if *if_exists {
6330                    f.write_str("IF EXISTS ")?;
6331                }
6332                for (i, n) in names.iter().enumerate() {
6333                    if i > 0 {
6334                        f.write_str(", ")?;
6335                    }
6336                    write!(f, "{}", quote_ident(n))?;
6337                }
6338                Ok(())
6339            }
6340            Self::CreateDomain(d) => d.fmt(f),
6341            Self::DropDomain { names, if_exists } => {
6342                f.write_str("DROP DOMAIN ")?;
6343                if *if_exists {
6344                    f.write_str("IF EXISTS ")?;
6345                }
6346                for (i, n) in names.iter().enumerate() {
6347                    if i > 0 {
6348                        f.write_str(", ")?;
6349                    }
6350                    write!(f, "{}", quote_ident(n))?;
6351                }
6352                Ok(())
6353            }
6354            Self::CreateSchema {
6355                name,
6356                if_not_exists,
6357            } => {
6358                f.write_str("CREATE SCHEMA ")?;
6359                if *if_not_exists {
6360                    f.write_str("IF NOT EXISTS ")?;
6361                }
6362                write!(f, "{}", quote_ident(name))
6363            }
6364            Self::DropSchema { names, if_exists } => {
6365                f.write_str("DROP SCHEMA ")?;
6366                if *if_exists {
6367                    f.write_str("IF EXISTS ")?;
6368                }
6369                for (i, n) in names.iter().enumerate() {
6370                    if i > 0 {
6371                        f.write_str(", ")?;
6372                    }
6373                    write!(f, "{}", quote_ident(n))?;
6374                }
6375                Ok(())
6376            }
6377            Self::CreateRule(r) => {
6378                f.write_str("CREATE ")?;
6379                if r.or_replace {
6380                    f.write_str("OR REPLACE ")?;
6381                }
6382                write!(
6383                    f,
6384                    "RULE {} AS ON {} TO {}",
6385                    quote_ident(&r.name),
6386                    r.event,
6387                    quote_ident(&r.table)
6388                )?;
6389                if let Some(w) = &r.when_condition {
6390                    write!(f, " WHERE {w}")?;
6391                }
6392                f.write_str(if r.instead {
6393                    " DO INSTEAD "
6394                } else {
6395                    " DO ALSO "
6396                })?;
6397                if r.commands.is_empty() {
6398                    f.write_str("NOTHING")?;
6399                } else if r.commands.len() == 1 {
6400                    write!(f, "{}", r.commands[0])?;
6401                } else {
6402                    f.write_str("(")?;
6403                    for (i, c) in r.commands.iter().enumerate() {
6404                        if i > 0 {
6405                            f.write_str("; ")?;
6406                        }
6407                        write!(f, "{c}")?;
6408                    }
6409                    f.write_str(")")?;
6410                }
6411                Ok(())
6412            }
6413            Self::DropRule {
6414                name,
6415                table,
6416                if_exists,
6417            } => {
6418                f.write_str("DROP RULE ")?;
6419                if *if_exists {
6420                    f.write_str("IF EXISTS ")?;
6421                }
6422                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
6423            }
6424        }
6425    }
6426}
6427
6428impl fmt::Display for CreateDomainStatement {
6429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6430        write!(
6431            f,
6432            "CREATE DOMAIN {} AS {}",
6433            quote_ident(&self.name),
6434            self.base_type
6435        )?;
6436        if let Some(d) = &self.default {
6437            write!(f, " DEFAULT {d}")?;
6438        }
6439        if self.not_null {
6440            f.write_str(" NOT NULL")?;
6441        }
6442        for c in &self.checks {
6443            write!(f, " CHECK ({c})")?;
6444        }
6445        Ok(())
6446    }
6447}
6448
6449impl fmt::Display for CreateTypeStatement {
6450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6451        write!(f, "CREATE TYPE {} AS ", quote_ident(&self.name))?;
6452        match &self.kind {
6453            TypeKind::Enum { labels } => {
6454                f.write_str("ENUM (")?;
6455                for (i, l) in labels.iter().enumerate() {
6456                    if i > 0 {
6457                        f.write_str(", ")?;
6458                    }
6459                    write!(f, "'{}'", l.replace('\'', "''"))?;
6460                }
6461                f.write_str(")")
6462            }
6463            TypeKind::Composite { fields, .. } => {
6464                f.write_str("(")?;
6465                for (i, (n, t)) in fields.iter().enumerate() {
6466                    if i > 0 {
6467                        f.write_str(", ")?;
6468                    }
6469                    write!(f, "{} {}", quote_ident(n), t)?;
6470                }
6471                f.write_str(")")
6472            }
6473        }
6474    }
6475}
6476
6477impl fmt::Display for CreateMaterializedViewStatement {
6478    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6479        f.write_str("CREATE MATERIALIZED VIEW ")?;
6480        if self.if_not_exists {
6481            f.write_str("IF NOT EXISTS ")?;
6482        }
6483        write!(f, "{}", quote_ident(&self.name))?;
6484        if !self.columns.is_empty() {
6485            f.write_str(" (")?;
6486            for (i, c) in self.columns.iter().enumerate() {
6487                if i > 0 {
6488                    f.write_str(", ")?;
6489                }
6490                write!(f, "{}", quote_ident(c))?;
6491            }
6492            f.write_str(")")?;
6493        }
6494        write!(f, " AS {}", self.body)?;
6495        if !self.with_data {
6496            f.write_str(" WITH NO DATA")?;
6497        }
6498        Ok(())
6499    }
6500}
6501
6502impl fmt::Display for CreateViewStatement {
6503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6504        f.write_str("CREATE ")?;
6505        if self.or_replace {
6506            f.write_str("OR REPLACE ")?;
6507        }
6508        if self.temporary {
6509            f.write_str("TEMPORARY ")?;
6510        }
6511        f.write_str("VIEW ")?;
6512        if self.if_not_exists {
6513            f.write_str("IF NOT EXISTS ")?;
6514        }
6515        write!(f, "{}", quote_ident(&self.name))?;
6516        if !self.columns.is_empty() {
6517            f.write_str(" (")?;
6518            for (i, c) in self.columns.iter().enumerate() {
6519                if i > 0 {
6520                    f.write_str(", ")?;
6521                }
6522                write!(f, "{}", quote_ident(c))?;
6523            }
6524            f.write_str(")")?;
6525        }
6526        write!(f, " AS {}", self.body)?;
6527        match self.check_option {
6528            Some(ViewCheckOption::Local) => f.write_str(" WITH LOCAL CHECK OPTION"),
6529            Some(ViewCheckOption::Cascaded) => f.write_str(" WITH CASCADED CHECK OPTION"),
6530            None => Ok(()),
6531        }
6532    }
6533}
6534
6535impl fmt::Display for CreateSequenceStatement {
6536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6537        f.write_str("CREATE ")?;
6538        if self.temporary {
6539            f.write_str("TEMPORARY ")?;
6540        }
6541        f.write_str("SEQUENCE ")?;
6542        if self.if_not_exists {
6543            f.write_str("IF NOT EXISTS ")?;
6544        }
6545        write!(f, "{}", quote_ident(&self.name))?;
6546        if let Some(dt) = self.data_type {
6547            write!(f, " AS {dt}")?;
6548        }
6549        write_sequence_options(f, &self.options)
6550    }
6551}
6552
6553impl fmt::Display for AlterSequenceStatement {
6554    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6555        f.write_str("ALTER SEQUENCE ")?;
6556        if self.if_exists {
6557            f.write_str("IF EXISTS ")?;
6558        }
6559        write!(f, "{}", quote_ident(&self.name))?;
6560        write_sequence_options(f, &self.options)
6561    }
6562}
6563
6564impl fmt::Display for SequenceDataType {
6565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6566        f.write_str(match self {
6567            Self::SmallInt => "smallint",
6568            Self::Int => "integer",
6569            Self::BigInt => "bigint",
6570        })
6571    }
6572}
6573
6574fn write_sequence_options(f: &mut fmt::Formatter<'_>, o: &SequenceOptions) -> fmt::Result {
6575    if let Some(n) = o.increment {
6576        write!(f, " INCREMENT BY {n}")?;
6577    }
6578    match o.min_value {
6579        Some(SeqBound::Value(n)) => write!(f, " MINVALUE {n}")?,
6580        Some(SeqBound::NoBound) => f.write_str(" NO MINVALUE")?,
6581        None => {}
6582    }
6583    match o.max_value {
6584        Some(SeqBound::Value(n)) => write!(f, " MAXVALUE {n}")?,
6585        Some(SeqBound::NoBound) => f.write_str(" NO MAXVALUE")?,
6586        None => {}
6587    }
6588    if let Some(n) = o.start {
6589        write!(f, " START WITH {n}")?;
6590    }
6591    match o.restart {
6592        Some(Some(n)) => write!(f, " RESTART WITH {n}")?,
6593        Some(None) => f.write_str(" RESTART")?,
6594        None => {}
6595    }
6596    if let Some(n) = o.cache {
6597        write!(f, " CACHE {n}")?;
6598    }
6599    match o.cycle {
6600        Some(true) => f.write_str(" CYCLE")?,
6601        Some(false) => f.write_str(" NO CYCLE")?,
6602        None => {}
6603    }
6604    if let Some(ob) = &o.owned_by {
6605        match ob {
6606            SequenceOwnedBy::None => f.write_str(" OWNED BY NONE")?,
6607            SequenceOwnedBy::Column { table, column } => {
6608                write!(
6609                    f,
6610                    " OWNED BY {}.{}",
6611                    quote_ident(table),
6612                    quote_ident(column)
6613                )?;
6614            }
6615        }
6616    }
6617    Ok(())
6618}
6619
6620impl fmt::Display for CreateFunctionStatement {
6621    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6622        f.write_str("CREATE ")?;
6623        if self.or_replace {
6624            f.write_str("OR REPLACE ")?;
6625        }
6626        write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
6627        for (i, arg) in self.args.iter().enumerate() {
6628            if i > 0 {
6629                f.write_str(", ")?;
6630            }
6631            match arg.mode {
6632                FunctionArgMode::In => {}
6633                FunctionArgMode::Out => f.write_str("OUT ")?,
6634                FunctionArgMode::InOut => f.write_str("INOUT ")?,
6635            }
6636            if let Some(name) = &arg.name {
6637                write!(f, "{} ", quote_ident(name))?;
6638            }
6639            match &arg.ty {
6640                FunctionArgType::Typed(t) => write!(f, "{t}")?,
6641                FunctionArgType::Raw(s) => f.write_str(s)?,
6642            }
6643        }
6644        f.write_str(") RETURNS ")?;
6645        match &self.returns {
6646            FunctionReturn::Trigger => f.write_str("TRIGGER")?,
6647            FunctionReturn::Void => f.write_str("VOID")?,
6648            FunctionReturn::Type(t) => write!(f, "{t}")?,
6649            FunctionReturn::Other(s) => f.write_str(s)?,
6650        }
6651        write!(f, " LANGUAGE {} AS $$", self.language)?;
6652        match &self.body {
6653            FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
6654            FunctionBody::Raw(s) => f.write_str(s)?,
6655        }
6656        f.write_str("$$")
6657    }
6658}
6659
6660impl fmt::Display for PlPgSqlBlock {
6661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6662        if !self.declarations.is_empty() {
6663            f.write_str("DECLARE\n")?;
6664            for d in &self.declarations {
6665                write!(f, "  {} ", quote_ident(&d.name))?;
6666                match &d.ty {
6667                    FunctionArgType::Typed(t) => write!(f, "{t}")?,
6668                    FunctionArgType::Raw(s) => f.write_str(s)?,
6669                }
6670                if let Some(e) = &d.default {
6671                    write!(f, " := {e}")?;
6672                }
6673                f.write_str(";\n")?;
6674            }
6675        }
6676        f.write_str("BEGIN\n")?;
6677        for stmt in &self.statements {
6678            writeln!(f, "  {stmt};")?;
6679        }
6680        // v7.39 (read01 round 64) — the EXCEPTION section. It was MISSING from
6681        // this Display, and `CREATE FUNCTION` stores a body by re-rendering the
6682        // parsed block through it — so every exception handler a function
6683        // declared was thrown away AT STORE TIME. The block executed fine while
6684        // it was still an AST (a DO block never round-trips through text), which
6685        // is why only functions and triggers lost theirs.
6686        if !self.exception_handlers.is_empty() {
6687            f.write_str("EXCEPTION\n")?;
6688            for h in &self.exception_handlers {
6689                writeln!(f, "  WHEN {} THEN", h.conditions.join(" OR "))?;
6690                for stmt in &h.body {
6691                    writeln!(f, "    {stmt};")?;
6692                }
6693            }
6694        }
6695        f.write_str("END")
6696    }
6697}
6698
6699impl fmt::Display for PlPgSqlStmt {
6700    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6701        match self {
6702            Self::Assign { target, value } => write!(f, "{target} := {value}"),
6703            Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
6704            Self::ReturnNext(e) => write!(f, "RETURN NEXT {e}"),
6705            Self::ReturnQuery(s) => write!(f, "RETURN QUERY {s}"),
6706            Self::ReturnQueryExecute { sql } => write!(f, "RETURN QUERY EXECUTE {sql}"),
6707            Self::Return(t) => match t {
6708                ReturnTarget::New => f.write_str("RETURN NEW"),
6709                ReturnTarget::Old => f.write_str("RETURN OLD"),
6710                ReturnTarget::Null => f.write_str("RETURN NULL"),
6711                ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
6712            },
6713            Self::If {
6714                branches,
6715                else_branch,
6716            } => {
6717                for (i, (cond, body)) in branches.iter().enumerate() {
6718                    if i == 0 {
6719                        write!(f, "IF {cond} THEN ")?;
6720                    } else {
6721                        write!(f, " ELSIF {cond} THEN ")?;
6722                    }
6723                    for (j, s) in body.iter().enumerate() {
6724                        if j > 0 {
6725                            f.write_str("; ")?;
6726                        }
6727                        write!(f, "{s}")?;
6728                    }
6729                }
6730                if !else_branch.is_empty() {
6731                    f.write_str(" ELSE ")?;
6732                    for (j, s) in else_branch.iter().enumerate() {
6733                        if j > 0 {
6734                            f.write_str("; ")?;
6735                        }
6736                        write!(f, "{s}")?;
6737                    }
6738                }
6739                f.write_str(" END IF")
6740            }
6741            Self::Raise {
6742                level,
6743                message,
6744                args,
6745            } => {
6746                let lvl = match level {
6747                    RaiseLevel::Notice => "NOTICE",
6748                    RaiseLevel::Warning => "WARNING",
6749                    RaiseLevel::Info => "INFO",
6750                    RaiseLevel::Log => "LOG",
6751                    RaiseLevel::Debug => "DEBUG",
6752                    RaiseLevel::Exception => "EXCEPTION",
6753                };
6754                write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
6755                for a in args {
6756                    write!(f, ", {a}")?;
6757                }
6758                Ok(())
6759            }
6760            Self::EmbeddedSql(s) => write!(f, "{s}"),
6761            Self::Assert { condition, message } => {
6762                write!(f, "ASSERT {condition}")?;
6763                if let Some(m) = message {
6764                    write!(f, ", {m}")?;
6765                }
6766                Ok(())
6767            }
6768            Self::While { condition, body } => {
6769                writeln!(f, "WHILE {condition} LOOP")?;
6770                for s in body {
6771                    writeln!(f, "  {s};")?;
6772                }
6773                f.write_str("END LOOP")
6774            }
6775            Self::ForRange {
6776                var,
6777                start,
6778                end,
6779                reverse,
6780                body,
6781            } => {
6782                write!(f, "FOR {var} IN ")?;
6783                if *reverse {
6784                    f.write_str("REVERSE ")?;
6785                }
6786                writeln!(f, "{start}..{end} LOOP")?;
6787                for s in body {
6788                    writeln!(f, "  {s};")?;
6789                }
6790                f.write_str("END LOOP")
6791            }
6792            Self::Loop { body } => {
6793                writeln!(f, "LOOP")?;
6794                for s in body {
6795                    writeln!(f, "  {s};")?;
6796                }
6797                f.write_str("END LOOP")
6798            }
6799            Self::Exit { when } => {
6800                f.write_str("EXIT")?;
6801                if let Some(c) = when {
6802                    write!(f, " WHEN {c}")?;
6803                }
6804                Ok(())
6805            }
6806            Self::Continue { when } => {
6807                f.write_str("CONTINUE")?;
6808                if let Some(c) = when {
6809                    write!(f, " WHEN {c}")?;
6810                }
6811                Ok(())
6812            }
6813            Self::ExecuteDynamic { sql } => write!(f, "EXECUTE {sql}"),
6814            Self::ForQuery { var, query, body } => {
6815                writeln!(f, "FOR {var} IN ({query}) LOOP")?;
6816                for s in body {
6817                    writeln!(f, "  {s};")?;
6818                }
6819                f.write_str("END LOOP")
6820            }
6821            Self::ForExecute {
6822                var,
6823                sql_expr,
6824                body,
6825            } => {
6826                writeln!(f, "FOR {var} IN EXECUTE {sql_expr} LOOP")?;
6827                for s in body {
6828                    writeln!(f, "  {s};")?;
6829                }
6830                f.write_str("END LOOP")
6831            }
6832        }
6833    }
6834}
6835
6836impl fmt::Display for AssignTarget {
6837    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6838        match self {
6839            Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
6840            Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
6841            Self::Local(n) => f.write_str(n),
6842        }
6843    }
6844}
6845
6846impl fmt::Display for CreateTriggerStatement {
6847    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6848        f.write_str("CREATE ")?;
6849        if self.or_replace {
6850            f.write_str("OR REPLACE ")?;
6851        }
6852        write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
6853        match self.timing {
6854            TriggerTiming::Before => f.write_str("BEFORE")?,
6855            TriggerTiming::After => f.write_str("AFTER")?,
6856            TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
6857        }
6858        for (i, e) in self.events.iter().enumerate() {
6859            if i == 0 {
6860                f.write_str(" ")?;
6861            } else {
6862                f.write_str(" OR ")?;
6863            }
6864            match e {
6865                TriggerEvent::Insert => f.write_str("INSERT")?,
6866                TriggerEvent::Update => {
6867                    f.write_str("UPDATE")?;
6868                    if !self.update_columns.is_empty() {
6869                        f.write_str(" OF ")?;
6870                        for (j, col) in self.update_columns.iter().enumerate() {
6871                            if j > 0 {
6872                                f.write_str(", ")?;
6873                            }
6874                            f.write_str(&quote_ident(col))?;
6875                        }
6876                    }
6877                }
6878                TriggerEvent::Delete => f.write_str("DELETE")?,
6879                TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
6880            }
6881        }
6882        write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
6883        match self.for_each {
6884            TriggerForEach::Row => f.write_str("ROW")?,
6885            TriggerForEach::Statement => f.write_str("STATEMENT")?,
6886        }
6887        write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
6888    }
6889}
6890
6891impl fmt::Display for CreateIndexStatement {
6892    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6893        if self.is_unique {
6894            f.write_str("CREATE UNIQUE INDEX ")?;
6895        } else {
6896            f.write_str("CREATE INDEX ")?;
6897        }
6898        if self.if_not_exists {
6899            f.write_str("IF NOT EXISTS ")?;
6900        }
6901        write!(
6902            f,
6903            "{} ON {} ",
6904            quote_ident(&self.name),
6905            quote_ident(&self.table)
6906        )?;
6907        match self.method {
6908            IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
6909            IndexMethod::Brin => f.write_str("USING brin ")?,
6910            IndexMethod::Gin => f.write_str("USING gin ")?,
6911            IndexMethod::BTree => {}
6912        }
6913        if let Some(expr) = &self.expression {
6914            write!(f, "({})", expr)?;
6915        } else if self.extra_columns.is_empty() {
6916            // v7.15.0 — preserve operator class on round-trip
6917            // (`(col opclass)`) so WAL replay reconstructs the
6918            // engine-routing intent (e.g. `gin_trgm_ops` →
6919            // trigram-GIN build path).
6920            if let Some(op) = &self.opclass {
6921                write!(f, "({} {})", quote_ident(&self.column), op)?;
6922            } else {
6923                write!(f, "({})", quote_ident(&self.column))?;
6924            }
6925        } else {
6926            // v7.9.14 — multi-column key. Emit each column quoted
6927            // so the round-tripped form re-parses to identical AST.
6928            f.write_str("(")?;
6929            write!(f, "{}", quote_ident(&self.column))?;
6930            for c in &self.extra_columns {
6931                write!(f, ", {}", quote_ident(c))?;
6932            }
6933            f.write_str(")")?;
6934        }
6935        if !self.included_columns.is_empty() {
6936            f.write_str(" INCLUDE (")?;
6937            for (i, c) in self.included_columns.iter().enumerate() {
6938                if i > 0 {
6939                    f.write_str(", ")?;
6940                }
6941                write!(f, "{}", quote_ident(c))?;
6942            }
6943            f.write_str(")")?;
6944        }
6945        if let Some(pred) = &self.partial_predicate {
6946            write!(f, " WHERE {}", pred)?;
6947        }
6948        Ok(())
6949    }
6950}
6951
6952impl fmt::Display for CreateTableStatement {
6953    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6954        f.write_str("CREATE TABLE ")?;
6955        if self.if_not_exists {
6956            f.write_str("IF NOT EXISTS ")?;
6957        }
6958        write!(f, "{}", quote_ident(&self.name))?;
6959        // v7.37.6-B — `PARTITION OF parent <bounds>` child form has
6960        // no column list and no constraints; the table inherits its
6961        // columns from the parent at engine-DDL time.
6962        if let Some(spec) = &self.partition_of {
6963            write!(f, " PARTITION OF {} ", quote_ident(&spec.parent_name))?;
6964            return match &spec.bounds {
6965                PartitionOfBoundsAst::Range { lower, upper } => {
6966                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
6967                }
6968                PartitionOfBoundsAst::List { values } => {
6969                    f.write_str("FOR VALUES IN (")?;
6970                    for (i, v) in values.iter().enumerate() {
6971                        if i > 0 {
6972                            f.write_str(", ")?;
6973                        }
6974                        write!(f, "{}", v)?;
6975                    }
6976                    f.write_str(")")
6977                }
6978                PartitionOfBoundsAst::Hash { modulus, remainder } => {
6979                    write!(
6980                        f,
6981                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
6982                        modulus, remainder
6983                    )
6984                }
6985                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
6986            };
6987        }
6988        f.write_str(" (")?;
6989        for (i, col) in self.columns.iter().enumerate() {
6990            if i > 0 {
6991                f.write_str(", ")?;
6992            }
6993            write!(f, "{col}")?;
6994        }
6995        // v7.6.0 — render FK constraints in table-level form, after
6996        // the column list. WAL replay round-trips through Display, so
6997        // every FK must serialise here for replay to reconstruct the
6998        // schema bit-for-bit.
6999        for fk in &self.foreign_keys {
7000            f.write_str(", ")?;
7001            write!(f, "{fk}")?;
7002        }
7003        // v7.13.0 — render table-level constraints (PRIMARY KEY /
7004        // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
7005        // column-level UNIQUE / CHECK get lifted to this list at
7006        // parse time, so emitting only here avoids double-counting.
7007        for tc in &self.table_constraints {
7008            f.write_str(", ")?;
7009            write!(f, "{tc}")?;
7010        }
7011        f.write_str(")")?;
7012        // v7.37.6-B — partition-parent suffix renders after the
7013        // closing column-list paren, before the optional MySQL
7014        // table-options tail (which Display doesn't currently emit).
7015        if let Some(spec) = &self.partition_by {
7016            f.write_str(" PARTITION BY ")?;
7017            match spec.kind {
7018                PartitionKindAst::Range => f.write_str("RANGE ")?,
7019                PartitionKindAst::List => f.write_str("LIST ")?,
7020                PartitionKindAst::Hash => f.write_str("HASH ")?,
7021            }
7022            f.write_str("(")?;
7023            for (i, col) in spec.key_columns.iter().enumerate() {
7024                if i > 0 {
7025                    f.write_str(", ")?;
7026                }
7027                f.write_str(&quote_ident(col))?;
7028            }
7029            f.write_str(")")?;
7030        }
7031        Ok(())
7032    }
7033}
7034
7035fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
7036    match t {
7037        AlterTableTarget::OfType { type_name } => write!(f, "OF {type_name}"),
7038        AlterTableTarget::ReplicaIdentityUsingIndex { index } => {
7039            write!(f, "REPLICA IDENTITY USING INDEX {index}")
7040        }
7041        AlterTableTarget::Inherit { parent, detach } => {
7042            if *detach {
7043                write!(f, "NO INHERIT {parent}")
7044            } else {
7045                write!(f, "INHERIT {parent}")
7046            }
7047        }
7048        AlterTableTarget::SetHotTierBytes(n) => {
7049            write!(f, "SET hot_tier_bytes = {n}")
7050        }
7051        AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
7052        AlterTableTarget::DropForeignKey { name, if_exists } => {
7053            f.write_str("DROP CONSTRAINT ")?;
7054            if *if_exists {
7055                f.write_str("IF EXISTS ")?;
7056            }
7057            write!(f, "{}", quote_ident(name))
7058        }
7059        AlterTableTarget::DropIndex { name, if_exists } => {
7060            f.write_str("DROP INDEX ")?;
7061            if *if_exists {
7062                f.write_str("IF EXISTS ")?;
7063            }
7064            write!(f, "{}", quote_ident(name))
7065        }
7066        AlterTableTarget::AddColumn {
7067            column,
7068            if_not_exists,
7069        } => {
7070            f.write_str("ADD COLUMN ")?;
7071            if *if_not_exists {
7072                f.write_str("IF NOT EXISTS ")?;
7073            }
7074            write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
7075            if !column.nullable {
7076                f.write_str(" NOT NULL")?;
7077            }
7078            if let Some(d) = &column.default {
7079                write!(f, " DEFAULT {d}")?;
7080            }
7081            if column.auto_increment {
7082                f.write_str(" AUTO_INCREMENT")?;
7083            }
7084            if column.is_primary_key {
7085                f.write_str(" PRIMARY KEY")?;
7086            }
7087            Ok(())
7088        }
7089        AlterTableTarget::AlterColumnType {
7090            column,
7091            new_type,
7092            using,
7093            collation,
7094        } => {
7095            write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
7096            if let Some((_, name)) = collation {
7097                write!(f, " COLLATE {}", quote_ident(name))?;
7098            }
7099            if let Some(u) = using {
7100                write!(f, " USING {u}")?;
7101            }
7102            Ok(())
7103        }
7104        AlterTableTarget::DropColumn {
7105            column,
7106            if_exists,
7107            cascade,
7108        } => {
7109            f.write_str("DROP COLUMN ")?;
7110            if *if_exists {
7111                f.write_str("IF EXISTS ")?;
7112            }
7113            write!(f, "{}", quote_ident(column))?;
7114            if *cascade {
7115                f.write_str(" CASCADE")?;
7116            }
7117            Ok(())
7118        }
7119        AlterTableTarget::AddTableConstraint(tc) => {
7120            write!(f, "ADD {tc}")
7121        }
7122        AlterTableTarget::ValidateConstraint { name } => {
7123            write!(f, "VALIDATE CONSTRAINT {}", quote_ident(name))
7124        }
7125        AlterTableTarget::OwnerTo { role } => write!(f, "OWNER TO {}", quote_ident(role)),
7126        AlterTableTarget::ClusterOn { index } => match index {
7127            Some(i) => write!(f, "CLUSTER ON {}", quote_ident(i)),
7128            None => f.write_str("SET WITHOUT CLUSTER"),
7129        },
7130        AlterTableTarget::SetColumnAutoIncrement { column, seq_name } => {
7131            // Round-trip-safe spelling: re-parsing this form lowers
7132            // back to SetColumnAutoIncrement (the nextval default is
7133            // how pg_dump says "serial").
7134            let seq = seq_name
7135                .clone()
7136                .unwrap_or_else(|| alloc::format!("{column}_seq"));
7137            write!(
7138                f,
7139                "ALTER COLUMN {} SET DEFAULT nextval('{seq}')",
7140                quote_ident(column)
7141            )
7142        }
7143        AlterTableTarget::RenameColumn { old, new } => {
7144            write!(
7145                f,
7146                "RENAME COLUMN {} TO {}",
7147                quote_ident(old),
7148                quote_ident(new)
7149            )
7150        }
7151        AlterTableTarget::RenameConstraint { old, new } => {
7152            write!(
7153                f,
7154                "RENAME CONSTRAINT {} TO {}",
7155                quote_ident(old),
7156                quote_ident(new)
7157            )
7158        }
7159        AlterTableTarget::RenameTable { new } => {
7160            write!(f, "RENAME TO {}", quote_ident(new))
7161        }
7162        AlterTableTarget::SetTriggerEnabled { which, enabled } => {
7163            f.write_str(if *enabled {
7164                "ENABLE TRIGGER "
7165            } else {
7166                "DISABLE TRIGGER "
7167            })?;
7168            match which {
7169                TriggerSelector::All => f.write_str("ALL"),
7170                TriggerSelector::Named(n) => f.write_str(&quote_ident(n)),
7171            }
7172        }
7173        AlterTableTarget::SetRowSecurity { enabled, force } => match (enabled, force) {
7174            (Some(true), _) => f.write_str("ENABLE ROW LEVEL SECURITY"),
7175            (Some(false), _) => f.write_str("DISABLE ROW LEVEL SECURITY"),
7176            (_, Some(true)) => f.write_str("FORCE ROW LEVEL SECURITY"),
7177            (_, Some(false)) => f.write_str("NO FORCE ROW LEVEL SECURITY"),
7178            (None, None) => Ok(()),
7179        },
7180        AlterTableTarget::AttachPartition { child, bounds } => {
7181            write!(f, "ATTACH PARTITION {} ", quote_ident(child))?;
7182            match bounds {
7183                PartitionOfBoundsAst::Range { lower, upper } => {
7184                    write!(f, "FOR VALUES FROM ({}) TO ({})", *lower, *upper)
7185                }
7186                PartitionOfBoundsAst::List { values } => {
7187                    f.write_str("FOR VALUES IN (")?;
7188                    for (i, v) in values.iter().enumerate() {
7189                        if i > 0 {
7190                            f.write_str(", ")?;
7191                        }
7192                        write!(f, "{}", v)?;
7193                    }
7194                    f.write_str(")")
7195                }
7196                PartitionOfBoundsAst::Hash { modulus, remainder } => {
7197                    write!(
7198                        f,
7199                        "FOR VALUES WITH (MODULUS {}, REMAINDER {})",
7200                        modulus, remainder
7201                    )
7202                }
7203                PartitionOfBoundsAst::Default => f.write_str("DEFAULT"),
7204            }
7205        }
7206        AlterTableTarget::DetachPartition {
7207            child,
7208            concurrently,
7209            finalize,
7210        } => {
7211            write!(f, "DETACH PARTITION {}", quote_ident(child))?;
7212            if *concurrently {
7213                f.write_str(" CONCURRENTLY")?;
7214            }
7215            if *finalize {
7216                f.write_str(" FINALIZE")?;
7217            }
7218            Ok(())
7219        }
7220        AlterTableTarget::AlterColumnSetDefault {
7221            column,
7222            default_expr,
7223        } => write!(
7224            f,
7225            "ALTER COLUMN {} SET DEFAULT {}",
7226            quote_ident(column),
7227            default_expr
7228        ),
7229        AlterTableTarget::AlterColumnDropDefault { column } => {
7230            write!(f, "ALTER COLUMN {} DROP DEFAULT", quote_ident(column))
7231        }
7232        AlterTableTarget::AlterColumnSetNotNull { column } => {
7233            write!(f, "ALTER COLUMN {} SET NOT NULL", quote_ident(column))
7234        }
7235        AlterTableTarget::AlterColumnDropNotNull { column } => {
7236            write!(f, "ALTER COLUMN {} DROP NOT NULL", quote_ident(column))
7237        }
7238        AlterTableTarget::AlterColumnRestart { column, with } => {
7239            write!(f, "ALTER COLUMN {} RESTART", quote_ident(column))?;
7240            if let Some(n) = with {
7241                write!(f, " WITH {n}")?;
7242            }
7243            Ok(())
7244        }
7245        AlterTableTarget::AlterColumnDropExpression { column, if_exists } => {
7246            write!(
7247                f,
7248                "ALTER COLUMN {} DROP EXPRESSION{}",
7249                quote_ident(column),
7250                if *if_exists { " IF EXISTS" } else { "" }
7251            )
7252        }
7253        AlterTableTarget::AlterColumnDropIdentity { column, if_exists } => {
7254            write!(
7255                f,
7256                "ALTER COLUMN {} DROP IDENTITY{}",
7257                quote_ident(column),
7258                if *if_exists { " IF EXISTS" } else { "" }
7259            )
7260        }
7261        AlterTableTarget::AlterColumnSetExpression { column, expr } => {
7262            write!(
7263                f,
7264                "ALTER COLUMN {} SET EXPRESSION AS ({expr})",
7265                quote_ident(column)
7266            )
7267        }
7268    }
7269}
7270
7271impl fmt::Display for TableConstraint {
7272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7273        match self {
7274            Self::PrimaryKey { name, columns, .. } => {
7275                if let Some(n) = name {
7276                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7277                }
7278                f.write_str("PRIMARY KEY (")?;
7279                for (i, c) in columns.iter().enumerate() {
7280                    if i > 0 {
7281                        f.write_str(", ")?;
7282                    }
7283                    f.write_str(&quote_ident(c))?;
7284                }
7285                f.write_str(")")
7286            }
7287            Self::Unique {
7288                name,
7289                columns,
7290                nulls_not_distinct,
7291                ..
7292            } => {
7293                if let Some(n) = name {
7294                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7295                }
7296                f.write_str("UNIQUE ")?;
7297                if *nulls_not_distinct {
7298                    f.write_str("NULLS NOT DISTINCT ")?;
7299                }
7300                f.write_str("(")?;
7301                for (i, c) in columns.iter().enumerate() {
7302                    if i > 0 {
7303                        f.write_str(", ")?;
7304                    }
7305                    f.write_str(&quote_ident(c))?;
7306                }
7307                f.write_str(")")
7308            }
7309            Self::Check {
7310                name,
7311                expr,
7312                not_valid,
7313            } => {
7314                if let Some(n) = name {
7315                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7316                }
7317                write!(f, "CHECK ({expr})")?;
7318                if *not_valid {
7319                    write!(f, " NOT VALID")?;
7320                }
7321                Ok(())
7322            }
7323            Self::Index { name, columns } => {
7324                f.write_str("KEY ")?;
7325                if let Some(n) = name {
7326                    write!(f, "{} ", quote_ident(n))?;
7327                }
7328                f.write_str("(")?;
7329                for (i, c) in columns.iter().enumerate() {
7330                    if i > 0 {
7331                        f.write_str(", ")?;
7332                    }
7333                    f.write_str(&quote_ident(c))?;
7334                }
7335                f.write_str(")")
7336            }
7337            Self::FulltextIndex { name, columns } => {
7338                // Mysqldump emits `FULLTEXT KEY name (cols)` —
7339                // Display rounds back to that shape so dump
7340                // replay reproduces the input verbatim.
7341                f.write_str("FULLTEXT KEY ")?;
7342                if let Some(n) = name {
7343                    write!(f, "{} ", quote_ident(n))?;
7344                }
7345                f.write_str("(")?;
7346                for (i, c) in columns.iter().enumerate() {
7347                    if i > 0 {
7348                        f.write_str(", ")?;
7349                    }
7350                    f.write_str(&quote_ident(c))?;
7351                }
7352                f.write_str(")")
7353            }
7354            Self::Exclude {
7355                name,
7356                method,
7357                elements,
7358            } => {
7359                if let Some(n) = name {
7360                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
7361                }
7362                f.write_str("EXCLUDE ")?;
7363                if let Some(m) = method {
7364                    write!(f, "USING {m} ")?;
7365                }
7366                f.write_str("(")?;
7367                for (i, (col, op)) in elements.iter().enumerate() {
7368                    if i > 0 {
7369                        f.write_str(", ")?;
7370                    }
7371                    write!(f, "{} WITH {op}", quote_ident(col))?;
7372                }
7373                f.write_str(")")
7374            }
7375        }
7376    }
7377}
7378
7379impl fmt::Display for ForeignKeyConstraint {
7380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7381        if let Some(name) = &self.name {
7382            write!(f, "CONSTRAINT {} ", quote_ident(name))?;
7383        }
7384        f.write_str("FOREIGN KEY (")?;
7385        for (i, c) in self.columns.iter().enumerate() {
7386            if i > 0 {
7387                f.write_str(", ")?;
7388            }
7389            f.write_str(&quote_ident(c))?;
7390        }
7391        write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
7392        if !self.parent_columns.is_empty() {
7393            f.write_str(" (")?;
7394            for (i, c) in self.parent_columns.iter().enumerate() {
7395                if i > 0 {
7396                    f.write_str(", ")?;
7397                }
7398                f.write_str(&quote_ident(c))?;
7399            }
7400            f.write_str(")")?;
7401        }
7402        // Only render non-default actions to keep Display output
7403        // close to user input. SPG's default is RESTRICT (matches
7404        // SQL spec).
7405        if self.on_delete != FkAction::Restrict {
7406            write!(f, " ON DELETE {}", self.on_delete)?;
7407        }
7408        if self.on_update != FkAction::Restrict {
7409            write!(f, " ON UPDATE {}", self.on_update)?;
7410        }
7411        Ok(())
7412    }
7413}
7414
7415impl fmt::Display for FkAction {
7416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7417        match self {
7418            Self::Restrict => f.write_str("RESTRICT"),
7419            Self::Cascade => f.write_str("CASCADE"),
7420            Self::SetNull => f.write_str("SET NULL"),
7421            Self::SetDefault => f.write_str("SET DEFAULT"),
7422            Self::NoAction => f.write_str("NO ACTION"),
7423        }
7424    }
7425}
7426
7427impl fmt::Display for ColumnDef {
7428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7429        // v7.30.1 (mailrs round-24 class audit) — the type position
7430        // must re-parse to the same ColumnDef: a user-defined type
7431        // reference and the MySQL inline ENUM / SET value lists all
7432        // lower `ty` to Text, so rendering `ty` lost them.
7433        write!(f, "{}", quote_ident(&self.name))?;
7434        if let Some(ut) = &self.user_type_ref {
7435            write!(f, " {}", quote_ident(ut))?;
7436        } else if let Some(variants) = &self.inline_enum_variants {
7437            write_variant_list(f, "ENUM", variants)?;
7438        } else if let Some(variants) = &self.inline_set_variants {
7439            write_variant_list(f, "SET", variants)?;
7440        } else {
7441            write!(f, " {}", self.ty)?;
7442        }
7443        if self.is_unsigned {
7444            f.write_str(" UNSIGNED")?;
7445        }
7446        // v7.17.0 Phase 2.5 — render COLLATE for round-trippable
7447        // DDL. Only emits when non-default so the typical output
7448        // stays unchanged.
7449        match self.collation {
7450            Collation::Binary => {}
7451            Collation::CaseInsensitive => f.write_str(" COLLATE \"case_insensitive\"")?,
7452        }
7453        if let Some(d) = &self.default {
7454            write!(f, " DEFAULT {d}")?;
7455        }
7456        if self.auto_increment {
7457            f.write_str(" AUTO_INCREMENT")?;
7458        }
7459        if !self.nullable {
7460            f.write_str(" NOT NULL")?;
7461        }
7462        // v7.30.1 (mailrs round-24 class audit) — inline PRIMARY KEY
7463        // is NOT lifted to a table-level constraint at parse time
7464        // (unlike UNIQUE / CHECK), so the WAL round trip of a
7465        // prepared CREATE TABLE silently dropped the primary key.
7466        if self.is_primary_key {
7467            f.write_str(" PRIMARY KEY")?;
7468        }
7469        // The parser accepts only CURRENT_TIMESTAMP here (stored as
7470        // now()), so that spelling is the lossless round trip.
7471        if self.on_update_runtime.is_some() {
7472            f.write_str(" ON UPDATE CURRENT_TIMESTAMP")?;
7473        }
7474        // v7.37.7 — render GENERATED ALWAYS AS (…) STORED so WAL
7475        // replay reconstructs the computed-column declaration. The
7476        // expression sits inside a single set of parens; STORED is
7477        // the only variant the parser accepts.
7478        if let Some(gen_expr) = &self.generated_stored_expr {
7479            write!(f, " GENERATED ALWAYS AS ({gen_expr}) STORED")?;
7480        }
7481        Ok(())
7482    }
7483}
7484
7485/// v7.30.1 — `ENUM('a', 'b')` / `SET('a', 'b')` inline value-list
7486/// types (MySQL flavour; `ty` is Text underneath).
7487fn write_variant_list(f: &mut fmt::Formatter<'_>, kw: &str, variants: &[String]) -> fmt::Result {
7488    write!(f, " {kw}(")?;
7489    for (i, v) in variants.iter().enumerate() {
7490        if i > 0 {
7491            f.write_str(", ")?;
7492        }
7493        write!(f, "'{}'", v.replace('\'', "''"))?;
7494    }
7495    f.write_str(")")
7496}
7497
7498impl fmt::Display for InsertStatement {
7499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7500        write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
7501        if let Some(cols) = &self.columns {
7502            f.write_str(" (")?;
7503            for (i, c) in cols.iter().enumerate() {
7504                if i > 0 {
7505                    f.write_str(", ")?;
7506                }
7507                f.write_str(&quote_ident(c))?;
7508            }
7509            f.write_str(")")?;
7510        }
7511        // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
7512        // skipping the VALUES list (mailrs round-5 G4).
7513        if let Some(sel) = &self.select_source {
7514            write!(f, " {sel}")?;
7515        } else {
7516            f.write_str(" VALUES ")?;
7517            for (ri, row) in self.rows.iter().enumerate() {
7518                if ri > 0 {
7519                    f.write_str(", ")?;
7520                }
7521                f.write_str("(")?;
7522                for (i, v) in row.iter().enumerate() {
7523                    if i > 0 {
7524                        f.write_str(", ")?;
7525                    }
7526                    write!(f, "{v}")?;
7527                }
7528                f.write_str(")")?;
7529            }
7530        }
7531        // v7.30.1 (mailrs round-24) — ON CONFLICT must survive the
7532        // Display round trip: WAL persistence renders the bind-final
7533        // AST through this impl, and a replayed bare INSERT turns a
7534        // legal upsert no-op into a UNIQUE violation that refuses to
7535        // open the catalog.
7536        if let Some(oc) = &self.on_conflict {
7537            write!(f, " {oc}")?;
7538        }
7539        write_returning(self.returning.as_deref(), f)?;
7540        Ok(())
7541    }
7542}
7543
7544/// v7.30.1 (mailrs round-24) — render the ON CONFLICT clause the
7545/// parser produced, so the AST→SQL round trip preserves upsert
7546/// semantics (WAL replay depends on it).
7547impl fmt::Display for OnConflictClause {
7548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7549        f.write_str("ON CONFLICT")?;
7550        if let Some(name) = &self.constraint_name {
7551            write!(f, " ON CONSTRAINT {name}")?;
7552        }
7553        if !self.target_columns.is_empty() {
7554            f.write_str(" (")?;
7555            for (i, c) in self.target_columns.iter().enumerate() {
7556                if i > 0 {
7557                    f.write_str(", ")?;
7558                }
7559                f.write_str(&quote_ident(c))?;
7560            }
7561            f.write_str(")")?;
7562        }
7563        if let Some(w) = &self.index_where {
7564            write!(f, " WHERE {w}")?;
7565        }
7566        match &self.action {
7567            OnConflictAction::Nothing => f.write_str(" DO NOTHING"),
7568            OnConflictAction::Update {
7569                assignments,
7570                where_,
7571            } => {
7572                f.write_str(" DO UPDATE SET ")?;
7573                for (i, (col, expr)) in assignments.iter().enumerate() {
7574                    if i > 0 {
7575                        f.write_str(", ")?;
7576                    }
7577                    write!(f, "{} = {expr}", quote_ident(col))?;
7578                }
7579                if let Some(w) = where_ {
7580                    write!(f, " WHERE {w}")?;
7581                }
7582                Ok(())
7583            }
7584        }
7585    }
7586}
7587
7588/// v7.30.1 (mailrs round-24) — shared `RETURNING <projection>`
7589/// tail for the three DML Display impls.
7590fn write_returning(ret: Option<&[SelectItem]>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7591    let Some(items) = ret else {
7592        return Ok(());
7593    };
7594    f.write_str(" RETURNING ")?;
7595    for (i, item) in items.iter().enumerate() {
7596        if i > 0 {
7597            f.write_str(", ")?;
7598        }
7599        write!(f, "{item}")?;
7600    }
7601    Ok(())
7602}
7603
7604impl fmt::Display for UpdateStatement {
7605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7606        write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
7607        for (i, (col, expr)) in self.assignments.iter().enumerate() {
7608            if i > 0 {
7609                f.write_str(", ")?;
7610            }
7611            write!(f, "{} = {expr}", quote_ident(col))?;
7612        }
7613        if let Some(w) = &self.where_ {
7614            write!(f, " WHERE {w}")?;
7615        }
7616        // v7.39 (round 413) — MySQL `UPDATE … ORDER BY … LIMIT n`.
7617        if let Some(ol) = self.order_limit.as_deref() {
7618            if !ol.order_by.is_empty() {
7619                f.write_str(" ORDER BY ")?;
7620                for (i, o) in ol.order_by.iter().enumerate() {
7621                    if i > 0 {
7622                        f.write_str(", ")?;
7623                    }
7624                    write!(f, "{}", o.expr)?;
7625                    if o.desc {
7626                        f.write_str(" DESC")?;
7627                    }
7628                    match o.nulls_first {
7629                        Some(true) => f.write_str(" NULLS FIRST")?,
7630                        Some(false) => f.write_str(" NULLS LAST")?,
7631                        None => {}
7632                    }
7633                }
7634            }
7635            if let Some(n) = ol.limit {
7636                write!(f, " LIMIT {n}")?;
7637            }
7638        }
7639        write_returning(self.returning.as_deref(), f)?;
7640        Ok(())
7641    }
7642}
7643
7644impl fmt::Display for DeleteStatement {
7645    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7646        write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
7647        if let Some(w) = &self.where_ {
7648            write!(f, " WHERE {w}")?;
7649        }
7650        write_returning(self.returning.as_deref(), f)?;
7651        Ok(())
7652    }
7653}
7654
7655impl fmt::Display for CteBody {
7656    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7657        match self {
7658            Self::Select(s) => write!(f, "{s}"),
7659            Self::Insert(s) => write!(f, "{s}"),
7660            Self::Update(s) => write!(f, "{s}"),
7661            Self::Delete(s) => write!(f, "{s}"),
7662            Self::Merge(s) => write!(f, "{s}"),
7663        }
7664    }
7665}
7666
7667impl fmt::Display for MergeStatement {
7668    // v7.17.0 Phase 3.P0-42 — MERGE display is approximate
7669    // (it round-trips for the cases tests cover, not for
7670    // round-tripping every edge of the surface).
7671    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7672        fmt_with_clause(&self.ctes, f)?;
7673        f.write_str("MERGE INTO ")?;
7674        write!(f, "{}", quote_ident(&self.target))?;
7675        if let Some(a) = &self.target_alias {
7676            write!(f, " {}", quote_ident(a))?;
7677        }
7678        f.write_str(" USING ")?;
7679        if let Some(sub) = &self.source_select {
7680            write!(f, "({sub})")?;
7681        } else {
7682            write!(f, "{}", quote_ident(&self.source))?;
7683        }
7684        if let Some(a) = &self.source_alias {
7685            write!(f, " {}", quote_ident(a))?;
7686        }
7687        if !self.source_column_aliases.is_empty() {
7688            f.write_str("(")?;
7689            for (i, c) in self.source_column_aliases.iter().enumerate() {
7690                if i > 0 {
7691                    f.write_str(", ")?;
7692                }
7693                write!(f, "{}", quote_ident(c))?;
7694            }
7695            f.write_str(")")?;
7696        }
7697        write!(f, " ON {}", self.on)?;
7698        for clause in &self.clauses {
7699            f.write_str(" WHEN ")?;
7700            f.write_str(match clause.matched {
7701                MergeMatched::Matched => "MATCHED",
7702                MergeMatched::NotMatched => "NOT MATCHED",
7703                MergeMatched::NotMatchedBySource => "NOT MATCHED BY SOURCE",
7704            })?;
7705            if let Some(c) = &clause.condition {
7706                write!(f, " AND {c}")?;
7707            }
7708            f.write_str(" THEN ")?;
7709            match &clause.action {
7710                MergeAction::Insert { columns, values } => {
7711                    f.write_str("INSERT ")?;
7712                    // A column list is optional (round 146): the bare
7713                    // `INSERT VALUES (…)` form maps positionally.
7714                    if !columns.is_empty() {
7715                        f.write_str("(")?;
7716                        for (i, c) in columns.iter().enumerate() {
7717                            if i > 0 {
7718                                f.write_str(", ")?;
7719                            }
7720                            write!(f, "{}", quote_ident(c))?;
7721                        }
7722                        f.write_str(") ")?;
7723                    }
7724                    f.write_str("VALUES (")?;
7725                    for (i, v) in values.iter().enumerate() {
7726                        if i > 0 {
7727                            f.write_str(", ")?;
7728                        }
7729                        write!(f, "{v}")?;
7730                    }
7731                    f.write_str(")")?;
7732                }
7733                MergeAction::Update { assignments } => {
7734                    f.write_str("UPDATE SET ")?;
7735                    for (i, (c, e)) in assignments.iter().enumerate() {
7736                        if i > 0 {
7737                            f.write_str(", ")?;
7738                        }
7739                        write!(f, "{} = {e}", quote_ident(c))?;
7740                    }
7741                }
7742                MergeAction::Delete => f.write_str("DELETE")?,
7743                MergeAction::DoNothing => f.write_str("DO NOTHING")?,
7744            }
7745        }
7746        if let Some(items) = &self.returning {
7747            f.write_str(" RETURNING ")?;
7748            for (i, it) in items.iter().enumerate() {
7749                if i > 0 {
7750                    f.write_str(", ")?;
7751                }
7752                write!(f, "{it}")?;
7753            }
7754        }
7755        Ok(())
7756    }
7757}
7758
7759/// Shared `WITH <cte> [, …] ` prefix renderer — SELECT and MERGE both
7760/// carry a CTE list and must round-trip it identically.
7761fn fmt_with_clause(ctes: &[Cte], f: &mut fmt::Formatter<'_>) -> fmt::Result {
7762    if ctes.is_empty() {
7763        return Ok(());
7764    }
7765    f.write_str("WITH ")?;
7766    if ctes.iter().any(|c| c.recursive) {
7767        f.write_str("RECURSIVE ")?;
7768    }
7769    for (i, cte) in ctes.iter().enumerate() {
7770        if i > 0 {
7771            f.write_str(", ")?;
7772        }
7773        f.write_str(&quote_ident(&cte.name))?;
7774        if !cte.column_overrides.is_empty() {
7775            f.write_str(" (")?;
7776            for (ci, c) in cte.column_overrides.iter().enumerate() {
7777                if ci > 0 {
7778                    f.write_str(", ")?;
7779                }
7780                f.write_str(&quote_ident(c))?;
7781            }
7782            f.write_str(")")?;
7783        }
7784        write!(f, " AS ({})", cte.body)?;
7785    }
7786    f.write_str(" ")
7787}
7788
7789impl fmt::Display for SelectStatement {
7790    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7791        // v7.30.1 (mailrs round-24 class audit) — the WITH clause
7792        // must survive the round trip; a CTE-using statement
7793        // re-parsed without it references undefined tables.
7794        fmt_with_clause(&self.ctes, f)?;
7795        write_bare_select(self, f)?;
7796        for (kind, peer) in &self.unions {
7797            f.write_str(match kind {
7798                UnionKind::Distinct => " UNION ",
7799                UnionKind::All => " UNION ALL ",
7800                UnionKind::Intersect => " INTERSECT ",
7801                UnionKind::IntersectAll => " INTERSECT ALL ",
7802                UnionKind::Except => " EXCEPT ",
7803                UnionKind::ExceptAll => " EXCEPT ALL ",
7804            })?;
7805            write_bare_select(peer, f)?;
7806        }
7807        if !self.order_by.is_empty() {
7808            f.write_str(" ORDER BY ")?;
7809            for (i, o) in self.order_by.iter().enumerate() {
7810                if i > 0 {
7811                    f.write_str(", ")?;
7812                }
7813                write!(f, "{}", o.expr)?;
7814                if o.desc {
7815                    f.write_str(" DESC")?;
7816                }
7817                match o.nulls_first {
7818                    Some(true) => f.write_str(" NULLS FIRST")?,
7819                    Some(false) => f.write_str(" NULLS LAST")?,
7820                    None => {}
7821                }
7822            }
7823        }
7824        // v7.30.1 (mailrs round-24 class audit) — WITH TIES only
7825        // exists in the FETCH FIRST spelling; rendering it as LIMIT
7826        // dropped the tie-extension semantics on replay. The parser
7827        // accepts OFFSET before FETCH, so keep that order here.
7828        if self.limit_with_ties {
7829            if let Some(o) = &self.offset {
7830                write!(f, " OFFSET {o}")?;
7831            }
7832            if let Some(n) = &self.limit {
7833                write!(f, " FETCH FIRST {n} ROWS WITH TIES")?;
7834            }
7835        } else {
7836            if let Some(n) = &self.limit {
7837                write!(f, " LIMIT {n}")?;
7838            }
7839            if let Some(o) = &self.offset {
7840                write!(f, " OFFSET {o}")?;
7841            }
7842        }
7843        Ok(())
7844    }
7845}
7846
7847fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7848    f.write_str("SELECT ")?;
7849    if s.distinct {
7850        f.write_str("DISTINCT ")?;
7851    }
7852    write_bare_select_body(s, f)
7853}
7854
7855fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7856    for (i, item) in s.items.iter().enumerate() {
7857        if i > 0 {
7858            f.write_str(", ")?;
7859        }
7860        write!(f, "{item}")?;
7861    }
7862    if let Some(t) = &s.from {
7863        write!(f, " FROM {t}")?;
7864    }
7865    if let Some(e) = &s.where_ {
7866        write!(f, " WHERE {e}")?;
7867    }
7868    if let Some(gs) = &s.group_by {
7869        f.write_str(" GROUP BY ")?;
7870        for (i, g) in gs.iter().enumerate() {
7871            if i > 0 {
7872                f.write_str(", ")?;
7873            }
7874            write!(f, "{g}")?;
7875        }
7876    } else if s.group_by_all {
7877        // v7.30.1 (mailrs round-24 class audit) — the GROUP BY ALL
7878        // shortcut parses to group_by: None + this flag; dropping
7879        // it turned an aggregate query into a bare projection on
7880        // re-parse.
7881        f.write_str(" GROUP BY ALL")?;
7882    }
7883    if let Some(h) = &s.having {
7884        write!(f, " HAVING {h}")?;
7885    }
7886    Ok(())
7887}
7888
7889impl fmt::Display for SelectItem {
7890    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7891        match self {
7892            Self::Wildcard => f.write_str("*"),
7893            Self::QualifiedWildcard(q) => write!(f, "{}.*", quote_ident(q)),
7894            Self::Expr { expr, alias } => {
7895                write!(f, "{expr}")?;
7896                if let Some(a) = alias {
7897                    write!(f, " AS {}", quote_ident(a))?;
7898                }
7899                Ok(())
7900            }
7901        }
7902    }
7903}
7904
7905impl fmt::Display for FromClause {
7906    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7907        write!(f, "{}", self.primary)?;
7908        for j in &self.joins {
7909            match j.kind {
7910                JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
7911                JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
7912                JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
7913                JoinKind::Right => write!(f, " RIGHT JOIN {}", j.table)?,
7914                JoinKind::FullOuter => write!(f, " FULL OUTER JOIN {}", j.table)?,
7915                JoinKind::Semi => write!(f, " SEMI JOIN {}", j.table)?,
7916            }
7917            if let Some(on) = &j.on {
7918                write!(f, " ON {on}")?;
7919            }
7920        }
7921        Ok(())
7922    }
7923}
7924
7925/// v7.39 (round 205) — render a JSON_TABLE COLUMNS list (recursive
7926/// for NESTED). Kept close to the parser's grammar so it re-parses.
7927fn fmt_json_table_columns(f: &mut fmt::Formatter<'_>, cols: &[JsonTableColumn]) -> fmt::Result {
7928    for (i, c) in cols.iter().enumerate() {
7929        if i > 0 {
7930            f.write_str(", ")?;
7931        }
7932        match c {
7933            JsonTableColumn::Ordinality { name } => {
7934                write!(f, "{} FOR ORDINALITY", quote_ident(name))?;
7935            }
7936            JsonTableColumn::Nested { path, columns } => {
7937                write!(f, "NESTED PATH '{path}' COLUMNS (")?;
7938                fmt_json_table_columns(f, columns)?;
7939                f.write_str(")")?;
7940            }
7941            JsonTableColumn::Regular {
7942                name,
7943                ty,
7944                path,
7945                exists,
7946                format_json,
7947                wrapper,
7948                on_empty,
7949                on_error,
7950            } => {
7951                write!(f, "{} {ty}", quote_ident(name))?;
7952                if *format_json {
7953                    f.write_str(" FORMAT JSON")?;
7954                }
7955                if *exists {
7956                    write!(f, " EXISTS PATH '{path}'")?;
7957                } else {
7958                    write!(f, " PATH '{path}'")?;
7959                }
7960                if *wrapper {
7961                    f.write_str(" WITH WRAPPER")?;
7962                }
7963                if let JsonTableOnBehavior::Error = on_empty {
7964                    f.write_str(" ERROR ON EMPTY")?;
7965                } else if let JsonTableOnBehavior::Default(e) = on_empty {
7966                    write!(f, " DEFAULT {e} ON EMPTY")?;
7967                }
7968                if let JsonTableOnBehavior::Error = on_error {
7969                    f.write_str(" ERROR ON ERROR")?;
7970                } else if let JsonTableOnBehavior::Default(e) = on_error {
7971                    write!(f, " DEFAULT {e} ON ERROR")?;
7972                }
7973            }
7974        }
7975    }
7976    Ok(())
7977}
7978
7979impl fmt::Display for TableRef {
7980    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7981        // v7.30.1 (mailrs round-24 class audit) — the dynamic
7982        // table-ref shapes must round-trip: rendering only the
7983        // (synthetic) name turned LATERAL / unnest() /
7984        // generate_series() into references to nonexistent tables
7985        // on re-parse.
7986        // v7.39 (round 205) — JSON_TABLE round-trips through Display
7987        // (view bodies, WAL replay of `INSERT … SELECT FROM JSON_TABLE`).
7988        if let Some(jt) = &self.json_table {
7989            write!(f, "JSON_TABLE({}, '{}'", jt.doc, jt.row_path)?;
7990            if !jt.passing.is_empty() {
7991                f.write_str(" PASSING ")?;
7992                for (i, (n, e)) in jt.passing.iter().enumerate() {
7993                    if i > 0 {
7994                        f.write_str(", ")?;
7995                    }
7996                    write!(f, "{e} AS {}", quote_ident(n))?;
7997                }
7998            }
7999            f.write_str(" COLUMNS (")?;
8000            fmt_json_table_columns(f, &jt.columns)?;
8001            f.write_str(")")?;
8002            if let Some(a) = &self.alias {
8003                write!(f, " AS {}", quote_ident(a))?;
8004            }
8005            return Ok(());
8006        }
8007        if let Some(inner) = &self.lateral_subquery {
8008            write!(f, "LATERAL ({inner})")?;
8009            if let Some(a) = &self.alias {
8010                write!(f, " AS {}", quote_ident(a))?;
8011                // v7.37 D.28 — a derived table on the lateral_subquery channel
8012                // may carry `AS t(cols)` column aliases (e.g. `(VALUES …) t(g)`
8013                // lowers here). Rendering the alias without the column list lost
8014                // the column names on re-parse (a view body round-trips through
8015                // Display), so `SELECT g FROM the_view` failed ColumnNotFound.
8016                if !self.unnest_column_aliases.is_empty() {
8017                    f.write_str(" (")?;
8018                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8019                        if i > 0 {
8020                            f.write_str(", ")?;
8021                        }
8022                        f.write_str(&quote_ident(c))?;
8023                    }
8024                    f.write_str(")")?;
8025                }
8026            }
8027            return Ok(());
8028        }
8029        if let Some(expr) = &self.unnest_expr {
8030            write!(f, "UNNEST({expr})")?;
8031            if let Some(a) = &self.alias {
8032                write!(f, " AS {}", quote_ident(a))?;
8033                if !self.unnest_column_aliases.is_empty() {
8034                    f.write_str(" (")?;
8035                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8036                        if i > 0 {
8037                            f.write_str(", ")?;
8038                        }
8039                        f.write_str(&quote_ident(c))?;
8040                    }
8041                    f.write_str(")")?;
8042                }
8043            }
8044            return Ok(());
8045        }
8046        // 7.38.1 S5.1 — a FROM-position table function must re-render
8047        // as the CALL, not its bare name: ARRAY(subquery) desugars by
8048        // re-parsing the subquery's canonical text, and a dropped
8049        // argument list turned `pg_options_to_table(x)` into a
8050        // relation lookup that does not exist.
8051        if let Some(call) = &self.table_fn_call {
8052            let (fn_name, args) = call.as_ref();
8053            write!(f, "{fn_name}(")?;
8054            for (i, a) in args.iter().enumerate() {
8055                if i > 0 {
8056                    f.write_str(", ")?;
8057                }
8058                write!(f, "{a}")?;
8059            }
8060            f.write_str(")")?;
8061            if let Some(a) = &self.alias {
8062                write!(f, " AS {}", quote_ident(a))?;
8063                if !self.unnest_column_aliases.is_empty() {
8064                    f.write_str("(")?;
8065                    for (i, c) in self.unnest_column_aliases.iter().enumerate() {
8066                        if i > 0 {
8067                            f.write_str(", ")?;
8068                        }
8069                        write!(f, "{}", quote_ident(c))?;
8070                    }
8071                    f.write_str(")")?;
8072                }
8073            }
8074            return Ok(());
8075        }
8076        if let Some(args) = &self.generate_series_args {
8077            f.write_str("generate_series(")?;
8078            for (i, a) in args.iter().enumerate() {
8079                if i > 0 {
8080                    f.write_str(", ")?;
8081                }
8082                write!(f, "{a}")?;
8083            }
8084            f.write_str(")")?;
8085            if let Some(a) = &self.alias {
8086                write!(f, " AS {}", quote_ident(a))?;
8087            }
8088            return Ok(());
8089        }
8090        write!(f, "{}", quote_ident(&self.name))?;
8091        if let Some(seg) = self.as_of_segment {
8092            write!(f, " AS OF SEGMENT {seg}")?;
8093        }
8094        if let Some(a) = &self.alias {
8095            write!(f, " AS {}", quote_ident(a))?;
8096        }
8097        Ok(())
8098    }
8099}
8100
8101impl fmt::Display for ColumnName {
8102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8103        if let Some(q) = &self.qualifier {
8104            write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
8105        } else {
8106            write!(f, "{}", quote_ident(&self.name))
8107        }
8108    }
8109}
8110
8111/// v7.39 (round 311) — render the left spine of an AND / OR chain
8112/// without re-parenthesising each step, so `((a AND b) AND c)` comes out
8113/// as `(a) AND (b) AND (c)` the way PG's deparse writes it. Only the
8114/// SAME operator flattens; anything else is an ordinary operand.
8115fn write_bool_chain(f: &mut fmt::Formatter<'_>, e: &Expr, op: BinOp) -> fmt::Result {
8116    if let Expr::Binary {
8117        lhs,
8118        op: inner,
8119        rhs,
8120    } = e
8121        && *inner == op
8122    {
8123        write_bool_chain(f, lhs, op)?;
8124        return write!(f, " {op} {rhs}");
8125    }
8126    write!(f, "{e}")
8127}
8128
8129/// v7.39 (round 311, V32) — PG's PRETTY deparse of an expression, the
8130/// form `pg_get_constraintdef(oid, true)` and friends return.
8131///
8132/// The default [`fmt::Display`] parenthesises every operator node, which
8133/// is what PG's non-pretty deparse does and what makes the text
8134/// round-trip. Pretty drops the pairs the grammar can put back, and the
8135/// rule is NOT plain precedence minimisation — measured against PG 18.4
8136/// across 37 shapes:
8137///
8138///   * the boolean layer follows precedence (NOT > AND > OR): an OR
8139///     under an AND keeps its parens, an AND under an OR does not, and a
8140///     comparison under any of them does not (`NOT a > 1`);
8141///   * an associative chain flattens completely, even where the source
8142///     nested it to the right (`a AND (b AND c)` prints as one chain);
8143///   * but an operand of a comparison or arithmetic operator keeps its
8144///     parens whenever it is itself an operator expression — so
8145///     `(a + b) > 10` and `(- a) + b`, even though precedence alone
8146///     would not require either. A cast, function call, column or
8147///     literal in that position does not (`a::text = t`,
8148///     `length(code) > 2`); a cast counts as compound exactly when the
8149///     thing it casts is (`((a + b)::text) = t`).
8150///
8151/// Anything outside that layer defers to `Display`, which is never
8152/// wrong — only more parenthesised than PG would print.
8153#[must_use]
8154pub fn pretty_expr(e: &Expr) -> String {
8155    let mut out = String::new();
8156    write_pretty(&mut out, e, PrettyParent::None, false, false);
8157    out
8158}
8159
8160/// v7.39 (round 527) — the same deparse, spelling a cast the way MySQL
8161/// writes it.
8162///
8163/// MariaDB names the offending expression in its out-of-range message
8164/// and quotes the user's own syntax: `cast(1 as unsigned) - 2`. SPG
8165/// answered `1::unsigned - 2` — PG's spelling, in a message going to a
8166/// MySQL client, for a cast the client had just written the other way.
8167#[must_use]
8168pub fn pretty_expr_mysql(e: &Expr) -> String {
8169    let mut out = String::new();
8170    write_pretty(&mut out, e, PrettyParent::None, false, true);
8171    out
8172}
8173
8174/// v7.39 (round 505) — how strongly an expression suggests its own column
8175/// name. A cast keeps its argument's name only when that name is STRONG;
8176/// otherwise the cast reports the type it casts to.
8177///
8178/// Deduced from measurement, not from a rulebook. `upper(s)::text` names
8179/// itself `upper` on PG18 but `(CASE WHEN a=1 THEN 1 END)::text` names
8180/// itself `text` — so `case` and a function name cannot be the same kind of
8181/// answer, even though a bare `CASE …` does report `case`.
8182#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8183enum NameStrength {
8184    /// Nothing to go on — PG reports `?column?`.
8185    None,
8186    /// A name, but one a cast overrides: `case`, or a type name.
8187    Weak,
8188    /// A name a cast keeps: a column, or the function that produced it.
8189    Strong,
8190}
8191
8192/// v7.39 (round 505) — the column name PG18 gives a projected expression
8193/// that carries no `AS` alias. `None` means `?column?`.
8194///
8195/// SPG used to print the parsed expression back out, which matched neither
8196/// oracle and made name-keyed row access miss on both wires:
8197///
8198/// | query        | PG18       | SPG (before) |
8199/// |--------------|------------|--------------|
8200/// | `upper(s)`   | `upper`    | `upper(s)`   |
8201/// | `a+b`        | `?column?` | `(a + b)`    |
8202/// | `'lit'`      | `?column?` | `'lit'`      |
8203/// | `CASE …`     | `case`     | `CASE WHEN (a = 1) THEN …` |
8204///
8205/// Every rule below is one of those measurements, taken with `\gdesc`
8206/// against PG18: a call is named for its function, a cast recurses into its
8207/// argument and falls back to the type, a scalar subquery takes the name of
8208/// the column it selects, and operators have no name at all.
8209#[must_use]
8210pub fn figure_column_name(expr: &Expr) -> Option<String> {
8211    let (name, _) = figure_name_inner(expr);
8212    name
8213}
8214
8215/// The name a function reports, which is not always the name SPG parsed it
8216/// under: `count(*)` is held as `count_star` so the star arity survives the
8217/// AST, and that internal spelling must not reach a client. PG18 reports
8218/// `count`.
8219fn canonical_function_name(name: &str) -> String {
8220    match name {
8221        "count_star" => "count".to_string(),
8222        other => other.to_ascii_lowercase(),
8223    }
8224}
8225
8226/// v7.38.7 — a cast target's `pg_type.typname`, for the name a cast
8227/// reports when its operand has none of its own. Only the spellings that
8228/// differ from what the user writes need an entry; everything else is
8229/// already its own typname.
8230fn cast_target_typname(target: &CastTarget) -> String {
8231    let written = target.to_string().to_ascii_lowercase();
8232    let base = written.strip_suffix("[]").unwrap_or(&written);
8233    let mapped = match base {
8234        "bigint" => "int8",
8235        "integer" | "int" => "int4",
8236        "smallint" => "int2",
8237        "boolean" => "bool",
8238        "double precision" => "float8",
8239        "real" => "float4",
8240        "character varying" => "varchar",
8241        "character" => "bpchar",
8242        "timestamp with time zone" => "timestamptz",
8243        "timestamp without time zone" => "timestamp",
8244        "time without time zone" => "time",
8245        "decimal" => "numeric",
8246        other => other,
8247    };
8248    if written.ends_with("[]") {
8249        alloc::format!("_{mapped}")
8250    } else {
8251        String::from(mapped)
8252    }
8253}
8254
8255fn figure_name_inner(expr: &Expr) -> (Option<String>, NameStrength) {
8256    let strong = |n: String| (Some(n), NameStrength::Strong);
8257    match expr {
8258        // A column keeps its own name, qualifier and all discarded:
8259        // `lbl.a` reports `a`.
8260        Expr::Column(c) => strong(c.name.clone()),
8261        // Calls are named for the function. This covers the shapes that
8262        // only LOOK like syntax — `EXTRACT(year FROM …)` reports
8263        // `extract`, `SUBSTRING(x FROM 1 FOR 2)` reports `substring` —
8264        // because PG resolves them to functions before naming them.
8265        Expr::FunctionCall { name, .. } | Expr::WindowFunction { name, .. } => {
8266            strong(canonical_function_name(name))
8267        }
8268        Expr::AggregateOrdered { call, .. } => figure_name_inner(call),
8269        Expr::Extract { .. } => strong("extract".to_string()),
8270        Expr::Exists { .. } => strong("exists".to_string()),
8271        Expr::Array(_) => strong("array".to_string()),
8272        // `(expr).field` is named for the field, as a column would be.
8273        Expr::FieldAccess { field, .. } => strong(field.clone()),
8274        // A cast prefers its argument's name and settles for the type:
8275        // `upper(s)::text` is `upper`, `(a+b)::text` is `text`.
8276        Expr::Cast {
8277            expr: inner,
8278            target,
8279        } => match figure_name_inner(inner) {
8280            (Some(n), NameStrength::Strong) => strong(n),
8281            // v7.38.7 — the fallback is the target type's INTERNAL name,
8282            // which is what PG reports: `SELECT 7::bigint` is `int8`, not
8283            // the `bigint` the user typed. Measured on PG18 alongside
8284            // `CAST(7 AS bigint)`, which answers `int8` too.
8285            _ => (Some(cast_target_typname(target)), NameStrength::Weak),
8286        },
8287        // A scalar subquery reports whatever its single output column
8288        // reports: `(SELECT max(b) …)` is `max`, `(SELECT a+b …)` is not.
8289        Expr::ScalarSubquery(sel) => scalar_subquery_name(sel),
8290        // `CASE …` names itself, but weakly — a cast around it wins.
8291        Expr::Case { .. } => (Some("case".to_string()), NameStrength::Weak),
8292        // A literal that carries its own type names itself for that type:
8293        // `INTERVAL '1 day'` reports `interval`, while a bare `'1 day'`
8294        // reports nothing. Weak, like any other type name.
8295        Expr::Literal(Literal::Interval { .. }) => {
8296            (Some("interval".to_string()), NameStrength::Weak)
8297        }
8298        // A wrapper that adds no name of its own.
8299        Expr::Variadic(inner) => figure_name_inner(inner),
8300        Expr::NamedArg { expr: inner, .. } => figure_name_inner(inner),
8301        // Everything else — operators, comparisons, IS NULL, LIKE, IN,
8302        // literals, placeholders — reports `?column?`.
8303        _ => (None, NameStrength::None),
8304    }
8305}
8306
8307/// The name a scalar subquery's single projected column reports.
8308fn scalar_subquery_name(sel: &SelectStatement) -> (Option<String>, NameStrength) {
8309    match sel.items.as_slice() {
8310        [SelectItem::Expr { alias: Some(a), .. }] => (Some(a.clone()), NameStrength::Strong),
8311        [SelectItem::Expr { expr, alias: None }] => figure_name_inner(expr),
8312        _ => (None, NameStrength::None),
8313    }
8314}
8315
8316/// Binding power. Higher binds tighter; 0 means "no enclosing operator".
8317fn pretty_prec(e: &Expr) -> u8 {
8318    match e {
8319        Expr::Binary { op, .. } => match op {
8320            // v7.39 (round 407) — this deparse ladder mirrors the parser's:
8321            // OR < XOR < AND < NOT < comparison < additive < multiplicative.
8322            // XOR (MySQL-only) sits between OR and AND, so AND and everything
8323            // above shifted +1 to open rung 2 for it.
8324            BinOp::Or => 1,
8325            BinOp::LogicalXor => 2,
8326            BinOp::And => 3,
8327            BinOp::Add | BinOp::Sub | BinOp::Concat => 6,
8328            BinOp::Mul | BinOp::Div | BinOp::Mod => 7,
8329            // Everything else in this enum is a comparison-shaped
8330            // operator; they share one level, as in the grammar.
8331            _ => 5,
8332        },
8333        Expr::Unary { op, .. } => match op {
8334            UnOp::Not => 4,
8335            UnOp::Neg | UnOp::BitNot | UnOp::Plus => 8,
8336        },
8337        _ => u8::MAX,
8338    }
8339}
8340
8341/// Is this node an operator expression — the thing an arithmetic or
8342/// comparison parent keeps parentheses around? A cast inherits the
8343/// answer from what it casts.
8344fn pretty_is_compound(e: &Expr) -> bool {
8345    match e {
8346        Expr::Binary { .. } | Expr::Unary { .. } => true,
8347        Expr::Cast { expr, .. } => pretty_is_compound(expr),
8348        _ => false,
8349    }
8350}
8351
8352/// `parent` describes the enclosing operator: its binding power, and
8353/// whether it is a comparison (which keeps parens around any operator
8354/// operand) or a NOT (which keeps them at equal power too).
8355#[derive(Clone, Copy, PartialEq)]
8356enum PrettyParent {
8357    /// Nothing encloses this node.
8358    None,
8359    /// A comparison-shaped operator: an operator operand always keeps
8360    /// its parens, whatever precedence would allow.
8361    Comparison,
8362    /// Arithmetic / concatenation: precedence decides.
8363    Arith(u8),
8364    /// A boolean connective: precedence decides.
8365    Bool(u8),
8366    /// `NOT`: precedence decides, but equal power still needs parens so
8367    /// `NOT (NOT a > 1)` does not collapse.
8368    Not,
8369}
8370
8371fn write_pretty(out: &mut String, e: &Expr, parent: PrettyParent, is_rhs: bool, mysql: bool) {
8372    let prec = pretty_prec(e);
8373    let is_unary_sign = matches!(
8374        e,
8375        Expr::Unary {
8376            op: UnOp::Neg | UnOp::BitNot | UnOp::Plus,
8377            ..
8378        }
8379    );
8380    let needs = match parent {
8381        PrettyParent::None => false,
8382        PrettyParent::Comparison => pretty_is_compound(e),
8383        // A sign always keeps its parens under an operator — PG writes
8384        // `(- a) + b` even though precedence would not require it.
8385        PrettyParent::Arith(p) => {
8386            is_unary_sign
8387                || (matches!(e, Expr::Binary { .. } | Expr::Unary { .. })
8388                    && (prec < p || (prec == p && is_rhs)))
8389        }
8390        PrettyParent::Bool(p) => matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec < p,
8391        PrettyParent::Not => {
8392            matches!(e, Expr::Binary { .. } | Expr::Unary { .. }) && prec <= pretty_prec_not()
8393        }
8394    };
8395    if needs {
8396        out.push('(');
8397    }
8398    match e {
8399        Expr::Binary { lhs, op, rhs } => {
8400            let child = match op {
8401                BinOp::And | BinOp::Or => PrettyParent::Bool(prec),
8402                BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
8403                    PrettyParent::Arith(prec)
8404                }
8405                _ => PrettyParent::Comparison,
8406            };
8407            write_pretty(out, lhs, child, false, mysql);
8408            out.push(' ');
8409            out.push_str(&alloc::format!("{op}"));
8410            out.push(' ');
8411            // AND / OR are associative, so an explicitly right-nested
8412            // chain still prints as one chain.
8413            let rhs_is_rhs = !matches!(op, BinOp::And | BinOp::Or);
8414            write_pretty(out, rhs, child, rhs_is_rhs, mysql);
8415        }
8416        Expr::Unary { op, expr } => match op {
8417            UnOp::Not => {
8418                out.push_str("NOT ");
8419                write_pretty(out, expr, PrettyParent::Not, false, mysql);
8420            }
8421            UnOp::Neg => {
8422                out.push_str("- ");
8423                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8424            }
8425            UnOp::Plus => {
8426                out.push_str("+ ");
8427                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8428            }
8429            UnOp::BitNot => {
8430                out.push('~');
8431                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8432            }
8433        },
8434        Expr::Cast { expr, target } => {
8435            if mysql {
8436                // MySQL's own spelling, which is what its error messages
8437                // quote back.
8438                out.push_str("cast(");
8439                write_pretty(out, expr, PrettyParent::None, false, mysql);
8440                out.push_str(&alloc::format!(
8441                    " as {})",
8442                    target.to_string().to_lowercase()
8443                ));
8444            } else {
8445                write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8446                out.push_str(&alloc::format!("::{target}"));
8447            }
8448        }
8449        Expr::IsNull { expr, negated } => {
8450            write_pretty(out, expr, PrettyParent::Comparison, false, mysql);
8451            out.push_str(if *negated { " IS NOT NULL" } else { " IS NULL" });
8452        }
8453        other => out.push_str(&alloc::format!("{other}")),
8454    }
8455    if needs {
8456        out.push(')');
8457    }
8458}
8459
8460const fn pretty_prec_not() -> u8 {
8461    // Must match `pretty_prec`'s `UnOp::Not` rung (v7.39 round 407: 3 → 4
8462    // when the XOR insertion shifted the deparse ladder up by one).
8463    4
8464}
8465
8466impl fmt::Display for Expr {
8467    #[allow(clippy::too_many_lines)]
8468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8469        match self {
8470            Self::Literal(l) => write!(f, "{l}"),
8471            Self::Column(c) => write!(f, "{c}"),
8472            Self::Placeholder(n) => write!(f, "${n}"),
8473            // Round-trips as the spelling PG's docs lead with.
8474            Self::NamedArg { name, expr } => write!(f, "{} := {expr}", quote_ident(name)),
8475            Self::Variadic(expr) => write!(f, "VARIADIC {expr}"),
8476            // v7.39 (round 311) — an AND / OR chain that nests to the
8477            // LEFT is one chain, and renders flat: `(a) AND (b) AND (c)`,
8478            // not `((a) AND (b)) AND (c)`. Explicit right nesting keeps
8479            // its parentheses, because that is a different grouping as
8480            // written. Both halves measured against PG 18.4's deparse,
8481            // which flattens a same-operator left chain at parse time and
8482            // leaves `a AND (b AND c)` alone.
8483            Self::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
8484                f.write_str("(")?;
8485                write_bool_chain(f, lhs, *op)?;
8486                write!(f, " {op} {rhs}")?;
8487                f.write_str(")")
8488            }
8489            Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
8490            Self::Unary { op, expr } => match op {
8491                UnOp::Not => write!(f, "(NOT {expr})"),
8492                // A space after the sign, as PG's deparse writes it.
8493                UnOp::Neg => write!(f, "(- {expr})"),
8494                UnOp::Plus => write!(f, "(+ {expr})"),
8495                UnOp::BitNot => write!(f, "(~{expr})"),
8496            },
8497            // The OPERAND carries the parentheses, not the cast:
8498            // `(a)::text`, `((a + b))::text`. PG words it this way, and
8499            // it is what keeps `a::text = t` from reading as a cast of
8500            // the comparison.
8501            Self::Cast { expr, target } => write!(f, "({expr})::{target}"),
8502            Self::FieldAccess { base, field } => write!(f, "({base}).{field}"),
8503            Self::AggregateOrdered {
8504                call,
8505                order_by,
8506                distinct,
8507                filter,
8508            } => {
8509                let fmt_order_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8510                    for (i, o) in order_by.iter().enumerate() {
8511                        if i > 0 {
8512                            f.write_str(", ")?;
8513                        }
8514                        write!(f, "{}", o.expr)?;
8515                        if o.desc {
8516                            f.write_str(" DESC")?;
8517                        }
8518                        match o.nulls_first {
8519                            Some(true) => f.write_str(" NULLS FIRST")?,
8520                            Some(false) => f.write_str(" NULLS LAST")?,
8521                            None => {}
8522                        }
8523                    }
8524                    Ok(())
8525                };
8526                // Ordered-set aggregates (`percentile_cont(f) WITHIN
8527                // GROUP (ORDER BY x)`) render the in-parens args as the
8528                // direct argument and the sort spec under WITHIN GROUP —
8529                // not as an in-argument ORDER BY.
8530                let ordered_set = matches!(
8531                    call.as_ref(),
8532                    Expr::FunctionCall { name, .. }
8533                        if matches!(
8534                            name.to_ascii_lowercase().as_str(),
8535                            "percentile_cont" | "percentile_disc" | "mode"
8536                        )
8537                );
8538                if ordered_set {
8539                    write!(f, "{call} WITHIN GROUP (ORDER BY ")?;
8540                    fmt_order_by(f)?;
8541                    f.write_str(")")?;
8542                } else {
8543                    // `name([DISTINCT ]args [ORDER BY …])` — peel the
8544                    // inner call's parens to splice modifiers.
8545                    let inner = alloc::format!("{call}");
8546                    let body = inner.strip_suffix(')').unwrap_or(&inner);
8547                    let (head, args_part) = body.split_once('(').unwrap_or((body, ""));
8548                    write!(f, "{head}(")?;
8549                    if *distinct {
8550                        f.write_str("DISTINCT ")?;
8551                    }
8552                    write!(f, "{args_part}")?;
8553                    if !order_by.is_empty() {
8554                        f.write_str(" ORDER BY ")?;
8555                        fmt_order_by(f)?;
8556                    }
8557                    f.write_str(")")?;
8558                }
8559                if let Some(cond) = filter {
8560                    write!(f, " FILTER (WHERE {cond})")?;
8561                }
8562                Ok(())
8563            }
8564            Self::IsNull { expr, negated } => {
8565                if *negated {
8566                    write!(f, "({expr} IS NOT NULL)")
8567                } else {
8568                    write!(f, "({expr} IS NULL)")
8569                }
8570            }
8571            Self::BoolTest {
8572                expr,
8573                value,
8574                negated,
8575            } => {
8576                let word = match value {
8577                    Some(true) => "TRUE",
8578                    Some(false) => "FALSE",
8579                    None => "UNKNOWN",
8580                };
8581                if *negated {
8582                    write!(f, "({expr} IS NOT {word})")
8583                } else {
8584                    write!(f, "({expr} IS {word})")
8585                }
8586            }
8587            Self::FunctionCall { name, args } => {
8588                write!(f, "{name}(")?;
8589                for (i, a) in args.iter().enumerate() {
8590                    if i > 0 {
8591                        f.write_str(", ")?;
8592                    }
8593                    write!(f, "{a}")?;
8594                }
8595                f.write_str(")")
8596            }
8597            Self::Like {
8598                expr,
8599                pattern,
8600                negated,
8601                case_insensitive,
8602            } => {
8603                let op = match (negated, case_insensitive) {
8604                    (false, false) => "LIKE",
8605                    (true, false) => "NOT LIKE",
8606                    (false, true) => "ILIKE",
8607                    (true, true) => "NOT ILIKE",
8608                };
8609                write!(f, "({expr} {op} {pattern})")
8610            }
8611            Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
8612            Self::WindowFunction {
8613                name,
8614                args,
8615                partition_by,
8616                order_by,
8617                frame,
8618                null_treatment,
8619                filter,
8620            } => {
8621                write!(f, "{name}(")?;
8622                for (i, a) in args.iter().enumerate() {
8623                    if i > 0 {
8624                        f.write_str(", ")?;
8625                    }
8626                    write!(f, "{a}")?;
8627                }
8628                f.write_str(")")?;
8629                // v7.37 D.40 — `FILTER (WHERE …)` sits between the arg list and
8630                // OVER; it round-trips so a window body's Display re-parses.
8631                if let Some(cond) = filter {
8632                    write!(f, " FILTER (WHERE {cond})")?;
8633                }
8634                // v7.30.1 (mailrs round-24 class audit) — IGNORE
8635                // NULLS sits between the arg list and OVER; dropping
8636                // it reverted replayed queries to RESPECT NULLS.
8637                if matches!(null_treatment, NullTreatment::Ignore) {
8638                    f.write_str(" IGNORE NULLS")?;
8639                }
8640                f.write_str(" OVER (")?;
8641                if !partition_by.is_empty() {
8642                    f.write_str("PARTITION BY ")?;
8643                    for (i, p) in partition_by.iter().enumerate() {
8644                        if i > 0 {
8645                            f.write_str(", ")?;
8646                        }
8647                        write!(f, "{p}")?;
8648                    }
8649                }
8650                if !order_by.is_empty() {
8651                    if !partition_by.is_empty() {
8652                        f.write_str(" ")?;
8653                    }
8654                    f.write_str("ORDER BY ")?;
8655                    for (i, (e, desc, nulls_first)) in order_by.iter().enumerate() {
8656                        if i > 0 {
8657                            f.write_str(", ")?;
8658                        }
8659                        write!(f, "{e}")?;
8660                        if *desc {
8661                            f.write_str(" DESC")?;
8662                        }
8663                        match nulls_first {
8664                            Some(true) => f.write_str(" NULLS FIRST")?,
8665                            Some(false) => f.write_str(" NULLS LAST")?,
8666                            None => {}
8667                        }
8668                    }
8669                }
8670                if let Some(fr) = frame {
8671                    if !partition_by.is_empty() || !order_by.is_empty() {
8672                        f.write_str(" ")?;
8673                    }
8674                    let k = match fr.kind {
8675                        FrameKind::Rows => "ROWS",
8676                        FrameKind::Range => "RANGE",
8677                        FrameKind::Groups => "GROUPS",
8678                    };
8679                    if let Some(end) = &fr.end {
8680                        write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
8681                    } else {
8682                        write!(f, "{k} {}", fr.start)?;
8683                    }
8684                }
8685                f.write_str(")")
8686            }
8687            Self::ScalarSubquery(s) => write!(f, "({s})"),
8688            Self::Exists { subquery, negated } => {
8689                if *negated {
8690                    write!(f, "NOT EXISTS ({subquery})")
8691                } else {
8692                    write!(f, "EXISTS ({subquery})")
8693                }
8694            }
8695            Self::InSubquery {
8696                expr,
8697                subquery,
8698                negated,
8699            } => {
8700                if *negated {
8701                    write!(f, "({expr} NOT IN ({subquery}))")
8702                } else {
8703                    write!(f, "({expr} IN ({subquery}))")
8704                }
8705            }
8706            Self::RowInSubquery {
8707                row,
8708                subquery,
8709                negated,
8710            } => {
8711                write!(f, "(")?;
8712                for (i, e) in row.iter().enumerate() {
8713                    if i > 0 {
8714                        write!(f, ", ")?;
8715                    }
8716                    write!(f, "{e}")?;
8717                }
8718                let kw = if *negated { ") NOT IN (" } else { ") IN (" };
8719                write!(f, "{kw}{subquery})")
8720            }
8721            Self::RowCmpSubquery { row, op, subquery } => {
8722                write!(f, "(")?;
8723                for (i, e) in row.iter().enumerate() {
8724                    if i > 0 {
8725                        write!(f, ", ")?;
8726                    }
8727                    write!(f, "{e}")?;
8728                }
8729                write!(f, ") {op} ({subquery})")
8730            }
8731            Self::InList {
8732                expr,
8733                list,
8734                negated,
8735            } => {
8736                let kw = if *negated { " NOT IN (" } else { " IN (" };
8737                write!(f, "({expr}{kw}")?;
8738                for (i, e) in list.iter().enumerate() {
8739                    if i > 0 {
8740                        f.write_str(", ")?;
8741                    }
8742                    write!(f, "{e}")?;
8743                }
8744                f.write_str("))")
8745            }
8746            Self::Array(items) => {
8747                f.write_str("ARRAY[")?;
8748                for (i, e) in items.iter().enumerate() {
8749                    if i > 0 {
8750                        f.write_str(", ")?;
8751                    }
8752                    write!(f, "{e}")?;
8753                }
8754                f.write_str("]")
8755            }
8756            Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
8757            Self::ArraySlice { target, lo, hi } => {
8758                write!(f, "({target}[")?;
8759                if let Some(l) = lo {
8760                    write!(f, "{l}")?;
8761                }
8762                write!(f, ":")?;
8763                if let Some(h) = hi {
8764                    write!(f, "{h}")?;
8765                }
8766                write!(f, "])")
8767            }
8768            Self::AnyAll {
8769                expr,
8770                op,
8771                array,
8772                is_any,
8773            } => {
8774                let kw = if *is_any { "ANY" } else { "ALL" };
8775                write!(f, "({expr} {op} {kw}({array}))")
8776            }
8777            Self::Case {
8778                operand,
8779                branches,
8780                else_branch,
8781            } => {
8782                f.write_str("CASE")?;
8783                if let Some(op) = operand {
8784                    write!(f, " {op}")?;
8785                }
8786                for (w, t) in branches {
8787                    write!(f, " WHEN {w} THEN {t}")?;
8788                }
8789                if let Some(e) = else_branch {
8790                    write!(f, " ELSE {e}")?;
8791                }
8792                f.write_str(" END")
8793            }
8794        }
8795    }
8796}
8797
8798/// Render an exact decimal `unscaled / 10^scale`, keeping the scale
8799/// (trailing zeros): `(200, 2)` → `2.00`, `(1, 1)` → `0.1`, `(-15, 1)` → `-1.5`.
8800pub fn render_exact_decimal(unscaled: i128, scale: u16) -> alloc::string::String {
8801    use alloc::string::ToString;
8802    if scale == 0 {
8803        return alloc::format!("{unscaled}");
8804    }
8805    let neg = unscaled < 0;
8806    let digits = alloc::format!("{}", unscaled.unsigned_abs());
8807    let scale = scale as usize;
8808    let (int_part, frac_part) = if digits.len() > scale {
8809        (
8810            digits[..digits.len() - scale].to_string(),
8811            digits[digits.len() - scale..].to_string(),
8812        )
8813    } else {
8814        ("0".to_string(), alloc::format!("{digits:0>scale$}"))
8815    };
8816    alloc::format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" })
8817}
8818
8819/// A single-quoted SQL string, with an embedded quote doubled.
8820fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
8821    f.write_str("'")?;
8822    for c in s.chars() {
8823        if c == '\'' {
8824            f.write_str("''")?;
8825        } else {
8826            write!(f, "{c}")?;
8827        }
8828    }
8829    f.write_str("'")
8830}
8831
8832impl fmt::Display for Literal {
8833    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8834        match self {
8835            Self::Integer(n) => write!(f, "{n}"),
8836            Self::Float(x) => {
8837                let s = format!("{x}");
8838                // Default Display for an integral f64 (e.g. 1.0) emits "1",
8839                // which would round-trip back to Integer. Force a dot.
8840                if s.contains('.') || s.contains('e') || s.contains('E') {
8841                    f.write_str(&s)
8842                } else {
8843                    write!(f, "{s}.0")
8844                }
8845            }
8846            Self::Numeric { unscaled, scale } => {
8847                // Render the exact decimal `unscaled / 10^scale`, preserving
8848                // scale (trailing zeros) — round-trips to the same literal.
8849                f.write_str(&render_exact_decimal(*unscaled, *scale))
8850            }
8851            Self::NumericBig(s) => f.write_str(s),
8852            // Printed exactly as the text form was, so a reader cannot
8853            // tell whether the constant was decoded or not.
8854            Self::Timestamp { text, .. } | Self::Date { text, .. } => write_quoted(f, text),
8855            Self::String(s) => write_quoted(f, s),
8856            Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
8857            Self::Null => f.write_str("NULL"),
8858            // PG external array form. Display round-trip re-enters
8859            // through the column-typed text coerce, same as pgwire.
8860            Self::TextArray(items) => {
8861                f.write_str("'{")?;
8862                for (i, it) in items.iter().enumerate() {
8863                    if i > 0 {
8864                        f.write_str(",")?;
8865                    }
8866                    match it {
8867                        None => f.write_str("NULL")?,
8868                        Some(s) => {
8869                            f.write_str("\"")?;
8870                            for c in s.chars() {
8871                                match c {
8872                                    // array-element escapes
8873                                    '"' | '\\' => write!(f, "\\{c}")?,
8874                                    // the OUTER wrapper is a SQL string
8875                                    // literal — embedded quotes must
8876                                    // double, or the rendered form
8877                                    // (WAL replay parses it back) is
8878                                    // invalid SQL
8879                                    '\'' => f.write_str("''")?,
8880                                    _ => write!(f, "{c}")?,
8881                                }
8882                            }
8883                            f.write_str("\"")?;
8884                        }
8885                    }
8886                }
8887                f.write_str("}'")
8888            }
8889            Self::IntArray(items) => {
8890                f.write_str("'{")?;
8891                for (i, it) in items.iter().enumerate() {
8892                    if i > 0 {
8893                        f.write_str(",")?;
8894                    }
8895                    match it {
8896                        None => f.write_str("NULL")?,
8897                        Some(n) => write!(f, "{n}")?,
8898                    }
8899                }
8900                f.write_str("}'")
8901            }
8902            Self::BigIntArray(items) => {
8903                f.write_str("'{")?;
8904                for (i, it) in items.iter().enumerate() {
8905                    if i > 0 {
8906                        f.write_str(",")?;
8907                    }
8908                    match it {
8909                        None => f.write_str("NULL")?,
8910                        Some(n) => write!(f, "{n}")?,
8911                    }
8912                }
8913                f.write_str("}'")
8914            }
8915            Self::Vector(v) => {
8916                f.write_str("[")?;
8917                for (i, x) in v.iter().enumerate() {
8918                    if i > 0 {
8919                        f.write_str(", ")?;
8920                    }
8921                    let s = format!("{x}");
8922                    // Mirror Float Display: force a dot so re-parse stays
8923                    // numerically literal.
8924                    if s.contains('.') || s.contains('e') || s.contains('E') {
8925                        f.write_str(&s)?;
8926                    } else {
8927                        write!(f, "{s}.0")?;
8928                    }
8929                }
8930                f.write_str("]")
8931            }
8932            Self::Interval { text, .. } => {
8933                f.write_str("INTERVAL '")?;
8934                for c in text.chars() {
8935                    if c == '\'' {
8936                        f.write_str("''")?;
8937                    } else {
8938                        write!(f, "{c}")?;
8939                    }
8940                }
8941                f.write_str("'")
8942            }
8943        }
8944    }
8945}
8946
8947impl fmt::Display for BinOp {
8948    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8949        f.write_str(match self {
8950            Self::Or => "OR",
8951            Self::And => "AND",
8952            Self::Eq => "=",
8953            Self::NotEq => "<>",
8954            Self::IsDistinctFrom => "IS DISTINCT FROM",
8955            Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
8956            Self::IntDiv => "DIV",
8957            Self::Lt => "<",
8958            Self::LtEq => "<=",
8959            Self::Gt => ">",
8960            Self::GtEq => ">=",
8961            Self::Add => "+",
8962            Self::Sub => "-",
8963            Self::Mul => "*",
8964            Self::Div => "/",
8965            Self::Mod => "%",
8966            Self::L2Distance => "<->",
8967            Self::GeomParallel => "?||",
8968            Self::OverLeft => "&<",
8969            Self::OverRight => "&>",
8970            Self::GeomPerp => "?-|",
8971            Self::GeomSameAs => "~=",
8972            Self::ClosestPoint => "##",
8973            Self::GeomHoriz => "?-",
8974            Self::InnerProduct => "<#>",
8975            Self::CosineDistance => "<=>",
8976            Self::Concat => "||",
8977            Self::BitOr => "|",
8978            Self::BitAnd => "&",
8979            Self::BitXor => "#",
8980            Self::LogicalXor => "xor",
8981            Self::JsonGet => "->",
8982            Self::JsonGetText => "->>",
8983            Self::JsonGetPath => "#>",
8984            Self::JsonGetPathText => "#>>",
8985            Self::JsonContains => "@>",
8986            Self::JsonPathExists => "@?",
8987            Self::JsonContainedBy => "<@",
8988            Self::JsonKeyExists => "?",
8989            Self::JsonKeysAny => "?|",
8990            Self::JsonKeysAll => "?&",
8991            Self::JsonDeletePath => "#-",
8992            Self::TsMatch => "@@",
8993            Self::InetContainedBy => "<<",
8994            Self::InetContainedByEq => "<<=",
8995            Self::InetContains => ">>",
8996            Self::InetContainsEq => ">>=",
8997            Self::InetOverlap => "&&",
8998            Self::Intersects => "?#",
8999            Self::IsBelow => "<^",
9000            Self::IsAbove => ">^",
9001            Self::PatternLt => "~<~",
9002            Self::PatternLtEq => "~<=~",
9003            Self::PatternGt => "~>~",
9004            Self::PatternGtEq => "~>=~",
9005        })
9006    }
9007}
9008
9009/// Quote `s` as a PG double-quoted identifier when required (keyword,
9010/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
9011/// Otherwise return it as-is. Returns an owned `String` to keep the call site
9012/// uniform.
9013pub(crate) fn quote_ident(s: &str) -> String {
9014    let needs_quote = match s.chars().next() {
9015        None => true,
9016        Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
9017        _ => {
9018            s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
9019                || s.chars().any(|c| c.is_ascii_uppercase())
9020                || is_keyword(s)
9021        }
9022    };
9023    if !needs_quote {
9024        return s.to_string();
9025    }
9026    let mut out = String::with_capacity(s.len() + 2);
9027    out.push('"');
9028    for c in s.chars() {
9029        if c == '"' {
9030            out.push_str("\"\"");
9031        } else {
9032            out.push(c);
9033        }
9034    }
9035    out.push('"');
9036    out
9037}
9038
9039fn is_keyword(s: &str) -> bool {
9040    matches!(
9041        &*s.to_ascii_lowercase(),
9042        "select"
9043            | "from"
9044            | "where"
9045            | "as"
9046            | "null"
9047            | "true"
9048            | "false"
9049            | "and"
9050            | "or"
9051            | "not"
9052            | "create"
9053            | "table"
9054            | "insert"
9055            | "into"
9056            | "values"
9057            | "index"
9058            | "on"
9059            | "begin"
9060            | "commit"
9061            | "rollback"
9062            | "is"
9063            | "between"
9064            | "in"
9065            | "like"
9066            | "group"
9067            | "distinct"
9068            | "union"
9069            | "all"
9070            | "join"
9071            | "inner"
9072            | "left"
9073            | "cross"
9074            | "outer"
9075            | "default"
9076            | "savepoint"
9077            | "release"
9078            | "to"
9079            | "having"
9080            | "show"
9081            | "extract"
9082            | "offset"
9083            | "asc"
9084            | "desc"
9085            | "interval"
9086    )
9087}
9088
9089#[cfg(test)]
9090mod tests {
9091    use super::*;
9092    use alloc::vec;
9093
9094    #[test]
9095    fn integer_literal_renders_without_dot() {
9096        assert_eq!(Literal::Integer(42).to_string(), "42");
9097    }
9098
9099    #[test]
9100    fn integral_float_keeps_dot() {
9101        assert_eq!(Literal::Float(1.0).to_string(), "1.0");
9102        assert_eq!(Literal::Float(1.5).to_string(), "1.5");
9103        assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
9104    }
9105
9106    #[test]
9107    fn string_literal_doubles_quote() {
9108        assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
9109    }
9110
9111    #[test]
9112    fn bool_and_null_render_uppercase() {
9113        assert_eq!(Literal::Bool(true).to_string(), "TRUE");
9114        assert_eq!(Literal::Bool(false).to_string(), "FALSE");
9115        assert_eq!(Literal::Null.to_string(), "NULL");
9116    }
9117
9118    #[test]
9119    fn binary_op_always_parenthesised() {
9120        let e = Expr::Binary {
9121            lhs: Box::new(Expr::Literal(Literal::Integer(1))),
9122            op: BinOp::Add,
9123            rhs: Box::new(Expr::Literal(Literal::Integer(2))),
9124        };
9125        assert_eq!(e.to_string(), "(1 + 2)");
9126    }
9127
9128    #[test]
9129    fn select_star_from_table() {
9130        let s = SelectStatement {
9131            locking: None,
9132            items: vec![SelectItem::Wildcard],
9133            from: Some(FromClause {
9134                primary: TableRef {
9135                    name: "users".into(),
9136                    alias: None,
9137                    only: false,
9138                    as_of_segment: None,
9139                    unnest_expr: None,
9140                    unnest_column_aliases: Vec::new(),
9141                    with_ordinality: false,
9142                    generate_series_args: None,
9143                    lateral_subquery: None,
9144                    jsonb_each_text_arg: None,
9145                    table_fn_call: None,
9146                    rows_from: None,
9147                    json_table: None,
9148                    scalar_fn_item: false,
9149                },
9150                joins: vec![],
9151            }),
9152            where_: None,
9153            group_by: None,
9154            group_by_all: false,
9155            having: None,
9156            unions: vec![],
9157            order_by: Vec::new(),
9158            limit: None,
9159            offset: None,
9160            limit_with_ties: false,
9161            window_check_exprs: Vec::new(),
9162            distinct: false,
9163            distinct_on: Vec::new(),
9164            ctes: vec![],
9165        };
9166        assert_eq!(s.to_string(), "SELECT * FROM users");
9167    }
9168
9169    #[test]
9170    fn quote_ident_for_uppercase_and_keyword() {
9171        assert_eq!(quote_ident("foo"), "foo");
9172        assert_eq!(quote_ident("Foo"), "\"Foo\"");
9173        assert_eq!(quote_ident("select"), "\"select\"");
9174        assert_eq!(quote_ident(""), "\"\"");
9175        assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
9176    }
9177}