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#[derive(Debug, Clone, PartialEq)]
15#[allow(clippy::large_enum_variant)] // Statement::Select dominates; Boxing would touch every match site
16pub enum Statement {
17    /// v7.14.0 — `DROP TABLE [IF EXISTS] name [, name…]
18    /// [CASCADE | RESTRICT]`. Engine removes the matching tables
19    /// (each one) from the catalog; IF EXISTS makes the drop
20    /// idempotent. CASCADE / RESTRICT trailers parsed silently
21    /// (SPG always cascades index drops on table drop).
22    DropTable {
23        names: Vec<String>,
24        if_exists: bool,
25    },
26    /// v7.14.0 — `DROP INDEX [IF EXISTS] name`. Removes the
27    /// matching index across whichever table holds it.
28    DropIndex {
29        name: String,
30        if_exists: bool,
31    },
32    /// v7.14.0 — empty / comment-only statement. The lexer strips
33    /// `--` line comments and `/* … */` block comments (including
34    /// the MySQL conditional `/*!NNNNN … */` form) before the
35    /// parser ever sees them; a SQL chunk that contains nothing
36    /// else lands here. Engine returns CommandOk no-op so
37    /// pg_dump / mysqldump preambles (`SET NAMES utf8mb4`
38    /// wrapped in conditional comments, etc.) load cleanly.
39    Empty,
40    Select(SelectStatement),
41    CreateTable(CreateTableStatement),
42    /// v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
43    /// [WITH SCHEMA <s>] [VERSION <v>] [CASCADE]` accepted as a
44    /// no-op so PG dumps that include extension declarations
45    /// (notably `pgvector`) load against SPG without splitting
46    /// init scripts. mailrs migration follow-up F3.
47    CreateExtension(String),
48    /// v7.9.27 → v7.16.2 — PG `DO $$ … $$ [LANGUAGE plpgsql];`
49    /// block. The body is now CAPTURED as a [`PlPgSqlBlock`] and
50    /// the engine executes it at top level (mailrs round-10
51    /// A.2). Pre-v7.16.2 the parser discarded the body and the
52    /// engine returned CommandOk — a SEV-1 silent no-op that
53    /// turned mailrs's `DO BEGIN IF EXISTS … THEN ALTER … END
54    /// $$` idempotent migrations into invisible no-ops.
55    DoBlock(PlPgSqlBlock),
56    CreateIndex(CreateIndexStatement),
57    Insert(InsertStatement),
58    /// v4.4 — `UPDATE <table> SET col=expr [, ...] [WHERE cond]`.
59    Update(UpdateStatement),
60    /// v4.4 — `DELETE FROM <table> [WHERE cond]`.
61    Delete(DeleteStatement),
62    Begin,
63    Commit,
64    Rollback,
65    /// `SAVEPOINT <name>` — push a named savepoint onto the active TX's
66    /// stack so a later `ROLLBACK TO <name>` can undo just the work
67    /// since this point.
68    Savepoint(String),
69    /// `ROLLBACK TO [SAVEPOINT] <name>` — restore catalog state to the
70    /// named savepoint and discard later savepoints. Does not end the
71    /// transaction.
72    RollbackToSavepoint(String),
73    /// `RELEASE [SAVEPOINT] <name>` — discard a savepoint without
74    /// rolling back. Keeps the work done since then.
75    ReleaseSavepoint(String),
76    /// `SHOW TABLES` — return the list of tables in the catalog.
77    ShowTables,
78    /// `SHOW COLUMNS FROM <table>` — return one row per column with
79    /// its declared name / type / nullability.
80    ShowColumns(String),
81    /// `CREATE USER 'name' WITH PASSWORD 'pw' ROLE 'admin'` (v4.1).
82    /// Role is optional; defaults to `readonly` when omitted.
83    CreateUser(CreateUserStatement),
84    /// `DROP USER 'name'` (v4.1).
85    DropUser(String),
86    /// `SHOW USERS` (v4.1) — admin-only listing of (name, role).
87    ShowUsers,
88    /// v4.26 — `EXPLAIN [ANALYZE] <select>`. The engine returns a
89    /// single-column text table describing the rewritten plan tree
90    /// for `inner`. `analyze` triggers an actual exec to attach
91    /// observed row counts and elapsed micros to each node.
92    Explain(ExplainStatement),
93    /// v6.0.4 — `ALTER INDEX <name> REBUILD [WITH (encoding = ...)]`.
94    /// Synchronous rebuild of an NSW index. With the optional
95    /// encoding clause, every stored cell at the indexed column is
96    /// also re-encoded through `coerce_value` before the new graph
97    /// builds.
98    AlterIndex(AlterIndexStatement),
99    /// v6.7.2 — `ALTER TABLE <name> SET <setting> = <value>`.
100    /// The only setting in v6.7.2 is `hot_tier_bytes`, which
101    /// overrides the global `SPG_HOT_TIER_BYTES` freezer trigger
102    /// for the named table.
103    AlterTable(AlterTableStatement),
104    /// v6.1.2 — `CREATE PUBLICATION <name> [FOR ALL TABLES]`.
105    /// The catalog row lives in `spg_publications`. Publisher-side
106    /// WAL filtering arrives in v6.1.5.
107    CreatePublication(CreatePublicationStatement),
108    /// v6.1.2 — `DROP PUBLICATION <name>`. PG-compatible silent
109    /// no-op when the publication does not exist.
110    DropPublication(String),
111    /// v6.1.3 — `SHOW PUBLICATIONS`. Returns one row per
112    /// publication ordered by name with `(name, scope_summary,
113    /// table_count)` columns. The scope summary is the human-
114    /// readable form `ALL TABLES` / `FOR TABLE …` / `FOR ALL
115    /// TABLES EXCEPT …`; `table_count` is `NULL` for the
116    /// `AllTables` scope and the table-list length otherwise.
117    ShowPublications,
118    /// v6.1.4 — `CREATE SUBSCRIPTION <name> CONNECTION '<conn>'
119    /// PUBLICATION <pub_name> [, <pub_name> …]`. Catalog lands
120    /// in `spg_subscriptions`; when the subscription is
121    /// `enabled = true` (default) the server spawns a
122    /// background worker that connects to `conn` and drains the
123    /// requested publication(s) into the local engine.
124    CreateSubscription(CreateSubscriptionStatement),
125    /// v6.1.4 — `DROP SUBSCRIPTION <name>`. Like DROP
126    /// PUBLICATION, silent no-op when absent. Stops the
127    /// associated worker thread before removing the row.
128    DropSubscription(String),
129    /// v6.1.4 — `SHOW SUBSCRIPTIONS`. Returns one row per
130    /// subscription ordered by name with `(name, conn_str,
131    /// publications, enabled, last_received_pos)`.
132    ShowSubscriptions,
133    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
134    /// Blocks until the local server's apply position reaches
135    /// `<pos>` or `<ms>` elapses. Server-layer command: the
136    /// engine refuses it (`EngineError::Unsupported`) since
137    /// `lag_state` lives in `spg-server`'s `ServerState`.
138    WaitForWalPosition {
139        pos: u64,
140        /// `None` → wait forever; `Some(ms)` → return after `ms`
141        /// milliseconds even if the target isn't reached.
142        timeout_ms: Option<u64>,
143    },
144    /// v6.2.0 — `ANALYZE [<table>]`. Bare form walks every user
145    /// table; `ANALYZE <name>` re-stats just one. Populates
146    /// `spg_statistic` with per-column null_frac + n_distinct +
147    /// 100-bucket equi-depth histogram.
148    Analyze(Option<String>),
149    /// v6.7.3 — `COMPACT COLD SEGMENTS`. Walks every user table's
150    /// BTree-cold indices and merges small cold-tier segments
151    /// (size below `SPG_COMPACTION_TARGET_SEGMENT_BYTES`, default
152    /// 4 MiB) into a single larger segment per (table, index).
153    /// `WHERE` predicate filtering on which tables to compact is
154    /// carved out of v6.7.3 (per V6_7_DESIGN.md STABILITY entry);
155    /// v6.7.3 only supports the bare form.
156    CompactColdSegments,
157    /// v7.12.1 — `SET <name> [TO|=] <value>`. Records a session
158    /// parameter on the engine; v7.12.1 honours
159    /// `default_text_search_config` (consumed by `to_tsvector` /
160    /// `plainto_tsquery` family when called without an explicit
161    /// config arg). All other names are accepted as a no-op so PG
162    /// dumps with `SET client_encoding`, `SET search_path` etc.
163    /// load cleanly.
164    SetParameter {
165        name: String,
166        value: SetValue,
167    },
168    /// v7.14.0 — `SET a = 1, b = 2, …` MySQL-flavoured
169    /// multi-assignment (mysqldump preamble uses
170    /// `SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS,
171    /// FOREIGN_KEY_CHECKS=0`). Engine applies each pair in
172    /// source order. Pairs whose LHS is a MySQL session/user
173    /// variable (`@VAR` / `@@VAR`) are recorded with the raw
174    /// name so the engine can ignore them; pairs whose LHS is
175    /// a recognised engine parameter (e.g. `FOREIGN_KEY_CHECKS`)
176    /// go through the regular `set_session_param` path.
177    SetParameterList(Vec<(String, SetValue)>),
178    /// v7.12.1 — `RESET <name>` / `RESET ALL`. Restores parameter
179    /// to its default. No-op for parameters SPG does not track.
180    ResetParameter(Option<String>),
181    /// v7.12.4 — `CREATE [OR REPLACE] FUNCTION name(args) RETURNS
182    /// <type> [LANGUAGE <lang>] AS $$ body $$ [LANGUAGE <lang>]`.
183    /// v7.12.4 ships `plpgsql` for `RETURNS TRIGGER` bodies (the
184    /// CREATE TRIGGER + AFTER/BEFORE row-level pipeline). Other
185    /// languages parse but error at exec time with a clear
186    /// unsupported message.
187    CreateFunction(CreateFunctionStatement),
188    /// v7.12.4 — `CREATE [OR REPLACE] TRIGGER name {BEFORE|AFTER}
189    /// {INSERT|UPDATE|DELETE} [OR ...] ON tbl FOR EACH ROW
190    /// EXECUTE {FUNCTION|PROCEDURE} fn_name()`. STATEMENT-level
191    /// triggers and column-list / WHEN clauses are out of scope
192    /// for v7.12.4.
193    CreateTrigger(CreateTriggerStatement),
194    /// v7.12.4 — `DROP TRIGGER [IF EXISTS] name ON tbl`. Silent
195    /// no-op when missing if `IF EXISTS` is set.
196    DropTrigger {
197        name: String,
198        table: String,
199        if_exists: bool,
200    },
201    /// v7.12.4 — `DROP FUNCTION [IF EXISTS] name`. Same shape as
202    /// DROP TRIGGER but global (no table scope).
203    DropFunction {
204        name: String,
205        if_exists: bool,
206    },
207}
208
209/// v7.12.1 — payload of a SET right-hand side. PG syntax accepts
210/// a string literal, an identifier (often a config name), an
211/// integer/float, or the bare `DEFAULT` keyword.
212#[derive(Debug, Clone, PartialEq)]
213pub enum SetValue {
214    String(String),
215    Ident(String),
216    Number(String),
217    Default,
218}
219
220/// v6.1.4 — `CREATE SUBSCRIPTION` AST node. v6.1.4 ships a
221/// single fixed-shape DDL; the WITH-clause options PG supports
222/// (`enabled`, `slot_name`, `streaming`, `binary`) are out of
223/// scope for v6.1.4 — `enabled` defaults to true and there are
224/// no other knobs to set in v6.1.x.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct CreateSubscriptionStatement {
227    pub name: String,
228    /// Connection string in PG keyword=value form (e.g.
229    /// `host=127.0.0.1 port=20002`). v6.1.4 only consumes the
230    /// `host` and `port` fields; the rest is reserved for
231    /// future v6.1.x options.
232    pub conn_str: String,
233    /// One or more publications on the remote side. Order is
234    /// preserved verbatim from the DDL; the worker requests them
235    /// in this order. v6.1.4 records the list; v6.1.5
236    /// publisher-side filtering enforces it.
237    pub publications: Vec<String>,
238}
239
240/// v6.1.2 — `CREATE PUBLICATION` AST node. The `scope` field uses
241/// the [`PublicationScope`] shape. v6.1.2 only accepted
242/// `AllTables`; v6.1.3 unlocks the `ForTables` / `AllTablesExcept`
243/// variants by flipping the parser gate (no AST migration).
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct CreatePublicationStatement {
246    pub name: String,
247    pub scope: PublicationScope,
248}
249
250/// v6.1.2 — Which tables a publication covers. v6.1.3 (this commit)
251/// flips the parser gate for the `ForTables` / `AllTablesExcept`
252/// variants — the on-disk shape, snapshot serialisation, and the
253/// AST round-trip Display path were already in place in v6.1.2
254/// so this is a parser-only widening.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub enum PublicationScope {
257    AllTables,
258    ForTables(Vec<String>),
259    AllTablesExcept(Vec<String>),
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct AlterIndexStatement {
264    pub name: String,
265    pub target: AlterIndexTarget,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum AlterIndexTarget {
270    /// `REBUILD [WITH (encoding = <enc>)]`. `encoding = None`
271    /// rebuilds the existing graph in place without touching the
272    /// column encoding; `Some(enc)` re-encodes every cell first.
273    Rebuild { encoding: Option<VecEncoding> },
274    /// v7.16.2 — `[IF EXISTS] RENAME TO <new>`. mailrs migrate-042
275    /// uses this; PG drops the IF EXISTS noisily as ERROR, mailrs
276    /// uses it to make the migration idempotent (re-running on a
277    /// DB where the rename already happened is a no-op rather
278    /// than an error).
279    Rename { new: String, if_exists: bool },
280}
281
282/// v6.7.2 — `ALTER TABLE t SET <setting> = <value>`. v6.7.2 ships
283/// the single `hot_tier_bytes` setting; later v6.7.x sub-versions
284/// can add more SET subjects without changing the dispatch shape.
285#[derive(Debug, Clone, PartialEq)]
286pub struct AlterTableStatement {
287    pub name: String,
288    /// v7.13.2 — mailrs round-6 S1. One or more subactions
289    /// separated by commas in the source SQL. PG-semantic apply
290    /// is sequential; engine bails on first error (no
291    /// transactional rollback of completed subactions in v7.13).
292    /// Single-subaction shape stays a 1-element vec.
293    pub targets: Vec<AlterTableTarget>,
294}
295
296#[derive(Debug, Clone, PartialEq)]
297pub enum AlterTableTarget {
298    /// Per-table hot-tier byte budget override. The freezer
299    /// reads this before falling back to `SPG_HOT_TIER_BYTES`.
300    SetHotTierBytes(u64),
301    /// v7.6.8 — `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY
302    /// (cols) REFERENCES parent[(pcols)] [ON DELETE/UPDATE …]`.
303    /// Engine validates existing rows against the new constraint
304    /// before installing it.
305    AddForeignKey(ForeignKeyConstraint),
306    /// v7.6.8 — `ALTER TABLE t DROP CONSTRAINT [IF EXISTS] name`.
307    /// `if_exists` (v7.13.2 mailrs round-6 S7) makes the drop a
308    /// no-op when no FK with that name exists; otherwise raises.
309    DropForeignKey { name: String, if_exists: bool },
310    /// v7.13.0 — `ALTER TABLE t ADD [COLUMN] [IF NOT EXISTS] <col>
311    /// <type> [DEFAULT <expr>] [NOT NULL]`. mailrs round-5 G1
312    /// (20 migrate-*.sql hits). Engine appends the column to the
313    /// schema and back-fills every existing row with the DEFAULT
314    /// (or NULL when no DEFAULT and the column is nullable).
315    AddColumn {
316        column: ColumnDef,
317        if_not_exists: bool,
318    },
319    /// v7.13.0 — `ALTER TABLE t ALTER COLUMN <col> TYPE <ty>
320    /// [USING <expr>]` (mailrs round-5 G8). Engine rewrites every
321    /// existing row's column value by evaluating the optional
322    /// USING expression (default `col::<ty>`) and re-coercing
323    /// against the new column type.
324    AlterColumnType {
325        column: String,
326        new_type: ColumnTypeName,
327        using: Option<Expr>,
328    },
329    /// v7.13.3 — `ALTER TABLE t DROP [COLUMN] [IF EXISTS] <col>
330    /// [CASCADE | RESTRICT]` (mailrs round-7 S8). The column +
331    /// every row's value at that position is removed; any index
332    /// on the column is dropped. `if_exists` makes the drop a
333    /// no-op when the column is missing. `cascade` removes
334    /// dependents (FKs referencing the column, partial indexes
335    /// whose predicate names the column); without it, the engine
336    /// rejects when dependents exist.
337    DropColumn {
338        column: String,
339        if_exists: bool,
340        cascade: bool,
341    },
342    /// v7.14.0 — `ALTER TABLE t ADD CONSTRAINT name PRIMARY KEY
343    /// (cols)` / `ADD CONSTRAINT name UNIQUE (cols)` / `ADD
344    /// CONSTRAINT name CHECK (expr)` — table-level constraints
345    /// installed post-CREATE-TABLE. pg_dump emits PKs as a
346    /// separate ALTER TABLE statement, so this surface lets the
347    /// dump load straight through.
348    AddTableConstraint(TableConstraint),
349    /// v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
350    /// Renames the column in the schema and propagates the rename
351    /// to every stored source string that references it as a
352    /// (potentially-qualified) column identifier: CHECK predicates,
353    /// partial-index predicates, runtime DEFAULT expressions, and
354    /// triggers' `UPDATE OF` column lists. Function bodies and
355    /// trigger bodies are NOT auto-rewritten — they're loose
356    /// source text and may contain references SPG can't statically
357    /// resolve to this column (NEW./OLD. + dynamic SQL). Renames
358    /// the column even if dependents exist; users renaming a
359    /// column referenced by a function body update the function
360    /// body separately.
361    RenameColumn { old: String, new: String },
362    /// v7.16.2 — `ALTER TABLE old RENAME TO new`. Renames the
363    /// table itself (mailrs round-10 A.5 carve-out — mailrs's
364    /// migrate-042 uses it). The engine moves the table entry
365    /// in the catalog under the new name; child catalog state
366    /// (FKs pointing at this table, triggers watching this
367    /// table) tracks the rename through the storage layer.
368    RenameTable { new: String },
369    /// v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
370    /// { ALL | <name> }`. Toggles whether row-level triggers
371    /// fire on subsequent INSERT/UPDATE/DELETE on the table.
372    /// `pg_dump --disable-triggers` emits a DISABLE wrapper +
373    /// ENABLE epilogue around every table's data block so the
374    /// rows already-computed in prod don't get re-rewritten
375    /// (and so trigger-driven side effects like
376    /// audit/queueing don't re-fire during a bulk reload).
377    /// `which == TriggerSelector::All` toggles every trigger
378    /// on the table; `Named(name)` toggles one trigger. The
379    /// engine persists the disabled state on `TriggerDef.enabled`
380    /// (catalog FILE_VERSION 25+) and the row-write paths skip
381    /// the trigger when `!enabled`.
382    SetTriggerEnabled {
383        which: TriggerSelector,
384        enabled: bool,
385    },
386}
387
388/// v7.16.1 — target of `ALTER TABLE … { ENABLE | DISABLE }
389/// TRIGGER …`. PG also accepts `USER`, `REPLICA`, `ALWAYS`
390/// modifiers; v7.16.1 ships the two shapes pg_dump actually
391/// emits (`ALL` + per-name) — the rest parse-accept as `Named`
392/// shouldn't surface from a dump.
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub enum TriggerSelector {
395    /// Every trigger on the table.
396    All,
397    /// A specific trigger by name.
398    Named(String),
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub struct ExplainStatement {
403    pub analyze: bool,
404    pub inner: Box<SelectStatement>,
405    /// v6.8.3 — `EXPLAIN (SUGGEST) <SELECT>` enables the index
406    /// advisor pass: after the regular plan tree, the engine
407    /// emits one suggestion line per column referenced in the
408    /// query's WHERE / JOIN that has no covering index on the
409    /// owning table.
410    pub suggest: bool,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct CreateUserStatement {
415    pub name: String,
416    pub password: String,
417    /// One of `admin` / `readwrite` / `readonly`. Stored verbatim from
418    /// the parser; the engine validates against `Role::parse` so a
419    /// typo lands as a runtime error with a clear message rather than
420    /// a parse failure.
421    pub role: String,
422}
423
424/// v7.12.4 — `CREATE [OR REPLACE] FUNCTION`. v7.12.4 ships
425/// `RETURNS TRIGGER LANGUAGE plpgsql` as the primary use case
426/// (the row-level trigger body the CREATE TRIGGER below references).
427/// Non-trigger user-defined functions parse but error at execution
428/// time with a clear unsupported message; that surface lands in
429/// v7.12.5+.
430#[derive(Debug, Clone, PartialEq)]
431pub struct CreateFunctionStatement {
432    pub name: String,
433    /// `OR REPLACE` was present; an existing function with the
434    /// same name is overwritten instead of erroring.
435    pub or_replace: bool,
436    /// `(arg1 type1, ...)` — v7.12.4 only accepts the empty arg
437    /// list `()` (sufficient for trigger functions). Other shapes
438    /// parse and store the args but the executor refuses to call
439    /// them.
440    pub args: Vec<FunctionArg>,
441    /// `RETURNS <type>` — `trigger` is the supported shape for
442    /// v7.12.4; arbitrary return types parse to
443    /// [`FunctionReturn::Other`].
444    pub returns: FunctionReturn,
445    /// `LANGUAGE <lang>` clause. PG accepts the clause on either
446    /// side of `AS $$...$$`; the parser canonicalises to one slot.
447    /// `plpgsql` and `sql` are the two interesting values.
448    pub language: String,
449    /// `AS $$ ... $$` body. v7.12.4 parses PL/pgSQL bodies into
450    /// a structured AST; non-trigger / non-plpgsql bodies stay as
451    /// the raw source text so the v7.12.5+ executor can pick them
452    /// up without a parser rev.
453    pub body: FunctionBody,
454}
455
456/// v7.12.4 — one positional argument to a `CREATE FUNCTION`.
457#[derive(Debug, Clone, PartialEq)]
458pub struct FunctionArg {
459    /// `IN` / `OUT` / `INOUT` mode. v7.12.4 only accepts `IN`
460    /// (the default); `OUT` / `INOUT` parse but the executor
461    /// refuses them.
462    pub mode: FunctionArgMode,
463    /// Optional arg name. Trigger functions traditionally don't
464    /// name their args (they read NEW/OLD instead), so `None` is
465    /// the common case.
466    pub name: Option<String>,
467    /// Declared type, normalised to the SPG `DataType` mapping
468    /// where one exists. Unknown / extension types parse as a
469    /// raw string under [`FunctionArgType::Raw`].
470    pub ty: FunctionArgType,
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
474pub enum FunctionArgMode {
475    In,
476    Out,
477    InOut,
478}
479
480#[derive(Debug, Clone, PartialEq)]
481pub enum FunctionArgType {
482    Typed(ColumnTypeName),
483    /// Unknown / extension types — kept as the parser-side raw
484    /// identifier so error messages can name them precisely.
485    Raw(String),
486}
487
488#[derive(Debug, Clone, PartialEq)]
489pub enum FunctionReturn {
490    /// `RETURNS TRIGGER` — the row-level trigger function shape.
491    /// v7.12.4 ships exactly this for execution.
492    Trigger,
493    /// `RETURNS VOID`. Parses; executor rejects in v7.12.4 unless
494    /// the function is unused (since v7.12.4 doesn't ship scalar
495    /// function invocation).
496    Void,
497    /// `RETURNS <type>` for any concrete data type. Reserved for
498    /// v7.12.5+'s scalar UDF surface.
499    Type(ColumnTypeName),
500    /// `RETURNS <ident>` for types SPG doesn't know — extension
501    /// types, RETURNS SETOF rows, RETURNS TABLE(...), etc.
502    Other(String),
503}
504
505#[derive(Debug, Clone, PartialEq)]
506pub enum FunctionBody {
507    /// v7.12.4 — parsed PL/pgSQL `BEGIN … END` block. The
508    /// trigger-function executor walks this directly without
509    /// re-parsing.
510    PlPgSql(PlPgSqlBlock),
511    /// Raw source text — parser couldn't (or didn't try to)
512    /// structure-parse the body. Used for `LANGUAGE sql`
513    /// functions and any PL/pgSQL body that contains v7.12.5+
514    /// features the v7.12.4 parser doesn't yet recognise. The
515    /// executor returns an unsupported error when invoked.
516    Raw(String),
517}
518
519/// v7.12.4 — PL/pgSQL `BEGIN ... END;` block. v7.12.6 widens
520/// from assignment + return to a real-PL/pgSQL surface:
521/// `DECLARE`-block local variables, `IF/ELSIF/ELSE/END IF`
522/// control flow, `RAISE` diagnostics, and embedded SQL
523/// statements that execute through the regular engine path.
524/// The remaining v7.12.x carve-out is loops (`LOOP/WHILE/FOR`),
525/// which mailrs's trigger doesn't need but other PG customers
526/// may; deferred to a future minor release.
527#[derive(Debug, Clone, PartialEq)]
528pub struct PlPgSqlBlock {
529    /// v7.12.6 — `DECLARE var TYPE [:= init_expr];` declarations
530    /// preceding `BEGIN`. Empty when the body opens directly with
531    /// `BEGIN`. Declarations execute in order; each may reference
532    /// earlier-declared locals in its init expression.
533    pub declarations: Vec<PlPgSqlDeclare>,
534    pub statements: Vec<PlPgSqlStmt>,
535}
536
537/// v7.12.6 — single `DECLARE` entry: variable name + declared
538/// type + optional initialiser. Variables default to SQL NULL
539/// when no init is given (matches PG).
540#[derive(Debug, Clone, PartialEq)]
541pub struct PlPgSqlDeclare {
542    pub name: String,
543    /// Declared SQL type (mapped to [`ColumnTypeName`] where SPG
544    /// knows it; raw text otherwise).
545    pub ty: FunctionArgType,
546    pub default: Option<Expr>,
547}
548
549#[derive(Debug, Clone, PartialEq)]
550pub enum PlPgSqlStmt {
551    /// `NEW.col := expr;` or `OLD.col := expr;`. OLD is parsed
552    /// for clarity in error reporting (PG also forbids it) — the
553    /// executor errors with a clear "OLD is read-only" message.
554    Assign { target: AssignTarget, value: Expr },
555    /// v7.16.2 — plpgsql `SELECT <projection> INTO <var>
556    /// [FROM …]` (mailrs round-10 migrate-042). The `body` is
557    /// the SELECT statement with the INTO clause stripped; the
558    /// engine runs it via `Engine::execute`, takes the first
559    /// row's first column, and assigns to the local variable
560    /// in the DECLARE scope. Single-column / single-row
561    /// queries only at v7.16.2; multi-target (`INTO a, b`) is
562    /// a v7.16.x follow-up.
563    SelectInto {
564        var: String,
565        body: Box<SelectStatement>,
566    },
567    /// `RETURN <target>;` — trigger functions canonically return
568    /// `NEW` / `OLD` / `NULL`; v7.12.4 also accepts a bare
569    /// expression for forward compatibility with scalar UDFs.
570    Return(ReturnTarget),
571    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
572    /// [ELSE body] END IF;`. Branches are tried in order; first
573    /// truthy condition wins; the optional ELSE runs when no
574    /// condition matched.
575    If {
576        branches: Vec<(Expr, Vec<PlPgSqlStmt>)>,
577        else_branch: Vec<PlPgSqlStmt>,
578    },
579    /// v7.12.6 — `RAISE <level> '<fmt>' [, args]*;`. Level is one
580    /// of `NOTICE` / `WARNING` / `INFO` / `LOG` / `DEBUG`
581    /// (logging — observable side effect only) or `EXCEPTION`
582    /// (aborts the trigger and propagates as an error). v7.12.6
583    /// supports the basic format-string substitution PG uses
584    /// (`%` placeholders consumed positionally).
585    Raise {
586        level: RaiseLevel,
587        message: String,
588        args: Vec<Expr>,
589    },
590    /// v7.12.6 — embedded SQL statement inside the trigger body
591    /// (`INSERT INTO …`, `UPDATE …`, `DELETE FROM …`, `SELECT …`).
592    /// NEW.col / OLD.col references inside the embedded
593    /// statement's expression tree are substituted with the
594    /// current trigger context before the engine re-executes the
595    /// statement. Recursion depth into nested triggers is
596    /// bounded by the engine's existing trigger-fire guard.
597    EmbeddedSql(Box<Statement>),
598}
599
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
601pub enum RaiseLevel {
602    /// `RAISE NOTICE` — diagnostic message, observable in the
603    /// server log. Does not affect the trigger's outcome.
604    Notice,
605    /// `RAISE WARNING` — like NOTICE, slightly louder severity.
606    Warning,
607    /// `RAISE INFO` — like NOTICE, slightly quieter.
608    Info,
609    /// `RAISE LOG` — like NOTICE, lower priority.
610    Log,
611    /// `RAISE DEBUG` — like NOTICE, lowest priority.
612    Debug,
613    /// `RAISE EXCEPTION` — aborts the trigger function with the
614    /// given message, propagating up to the caller as a query-
615    /// level error.
616    Exception,
617}
618
619#[derive(Debug, Clone, PartialEq)]
620pub enum AssignTarget {
621    NewColumn(String),
622    OldColumn(String),
623    /// Reserved for v7.12.5 DECLARE'd local variables.
624    Local(String),
625}
626
627#[derive(Debug, Clone, PartialEq)]
628pub enum ReturnTarget {
629    /// `RETURN NEW;` — for BEFORE triggers, this is the row that
630    /// actually gets written (possibly with NEW.col mutations
631    /// applied). For AFTER triggers, the return value is ignored.
632    New,
633    /// `RETURN OLD;` — pass-through. For BEFORE DELETE this lets
634    /// the delete proceed; for BEFORE UPDATE / INSERT it's
635    /// equivalent to dropping the write.
636    Old,
637    /// `RETURN NULL;` — for BEFORE triggers, skips the write
638    /// entirely. For AFTER, the return value is ignored.
639    Null,
640    /// `RETURN <expr>;` — non-row return shape; reserved for the
641    /// scalar UDF surface in v7.12.5+. Executor errors when used
642    /// inside a trigger function.
643    Expr(Expr),
644}
645
646/// v7.12.4 — `CREATE [OR REPLACE] TRIGGER`. Always row-level
647/// (`FOR EACH ROW`) in v7.12.4 — statement-level triggers parse
648/// but the executor refuses them. `WHEN (cond)` clauses are out
649/// of scope; the trigger function can short-circuit on a leading
650/// IF inside its body once v7.12.5 lands IF.
651#[derive(Debug, Clone, PartialEq)]
652pub struct CreateTriggerStatement {
653    pub name: String,
654    pub or_replace: bool,
655    pub timing: TriggerTiming,
656    /// At least one event; `INSERT OR UPDATE OR DELETE` parses to
657    /// three entries in order.
658    pub events: Vec<TriggerEvent>,
659    pub table: String,
660    /// `FOR EACH ROW` vs `FOR EACH STATEMENT`. v7.12.4 ships
661    /// only `Row`; `Statement` parses but the executor refuses.
662    pub for_each: TriggerForEach,
663    /// Name of the function to invoke. v7.12.4 requires the
664    /// function to be `CREATE FUNCTION`'d earlier; forward
665    /// references (PG accepts) are deferred to v7.12.5.
666    pub function: String,
667    /// v7.13.0 — `UPDATE OF col, col, …` column-list filter
668    /// (mailrs round-5 G7). Non-empty only when the events list
669    /// contains UPDATE and the user wrote the column-list filter.
670    /// PG fires the trigger only when at least one of these
671    /// columns appears in the SET clause; SPG conservatively
672    /// fires on any UPDATE matching the listed columns or
673    /// rewriting them at the row level. Empty vec = no filter
674    /// (fire on every UPDATE).
675    pub update_columns: Vec<String>,
676}
677
678#[derive(Debug, Clone, Copy, PartialEq, Eq)]
679pub enum TriggerTiming {
680    /// Fires before the row is written; the trigger function's
681    /// return value (NEW or NULL) decides the row content and
682    /// whether the write proceeds at all.
683    Before,
684    /// Fires after the row is written; the return value is
685    /// ignored.
686    After,
687    /// `INSTEAD OF` is PG-VIEW-trigger-only and out of scope for
688    /// v7.12.4 (SPG has no updatable-view surface).
689    InsteadOf,
690}
691
692#[derive(Debug, Clone, Copy, PartialEq, Eq)]
693pub enum TriggerEvent {
694    Insert,
695    Update,
696    Delete,
697    /// `TRUNCATE` event parses; SPG has no TRUNCATE statement
698    /// so the trigger never fires.
699    Truncate,
700}
701
702#[derive(Debug, Clone, Copy, PartialEq, Eq)]
703pub enum TriggerForEach {
704    Row,
705    Statement,
706}
707
708#[derive(Debug, Clone, PartialEq)]
709pub struct CreateIndexStatement {
710    pub name: String,
711    pub table: String,
712    pub column: String,
713    /// Optional `USING <method>` clause. v2.0 recognises `hnsw` (NSW
714    /// graph for vector kNN); unspecified is the default B-tree index.
715    pub method: IndexMethod,
716    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
717    /// index name already exists, instead of raising `DuplicateIndex`.
718    pub if_not_exists: bool,
719    /// v6.8.0 — `INCLUDE (col1, col2, …)` columns. Identifies the
720    /// non-key columns the planner should treat as "covered" by
721    /// this index when checking whether a query can run as an
722    /// index-only scan. Empty when no `INCLUDE` clause was given.
723    pub included_columns: Vec<String>,
724    /// v6.8.1 — `WHERE <expr>` partial-index predicate. Only rows
725    /// for which `<expr>` evaluates truthy enter the index;
726    /// queries whose `WHERE` clause's canonical Display form
727    /// matches this expression's Display form can be served by the
728    /// partial index. Stored as a parsed `Expr` so the engine
729    /// re-uses the existing evaluation path; storage persists the
730    /// Display form on the catalog snapshot.
731    pub partial_predicate: Option<Expr>,
732    /// v6.8.2 — expression-based index. When `Some(expr)`, the
733    /// index key is the result of `expr` evaluated on each row
734    /// (e.g. `CREATE INDEX … (lower(name))`). The `column`
735    /// field still names the *primary* column the expression
736    /// touches so existing planner shortcuts that resolve a
737    /// column position stay valid. `None` = plain
738    /// column-reference index (the legacy shape).
739    pub expression: Option<Expr>,
740    /// v7.9.14 — extra column names after the leading column in a
741    /// multi-column `CREATE INDEX … (a, b, c)`. mailrs F2. The
742    /// planner today still only uses the leading column for index
743    /// seeks; the extras are tracked verbatim so the same DDL
744    /// round-trips through WAL replay + catalog snapshot, and so
745    /// the engine can emit a clear warning at INDEX CREATE time
746    /// that only the leading column is currently honoured.
747    /// Composite BTree index keys land in v7.10.
748    pub extra_columns: Vec<String>,
749    /// v7.9.29 — `CREATE UNIQUE INDEX …`. When true the engine
750    /// enforces uniqueness on the indexed key (combined with the
751    /// `partial_predicate` filter — only rows where the predicate
752    /// evaluates truthy enter the uniqueness check). Standard SQL
753    /// and PG's canonical way to express conditional uniqueness.
754    /// mailrs K1.
755    pub is_unique: bool,
756    /// v7.15.0 — operator class on the leading column, when the
757    /// CREATE INDEX named one (`(col vector_cosine_ops)` shape).
758    /// Lower-cased. Most opclasses are still informational; the
759    /// engine routes on `gin_trgm_ops` specifically to build a
760    /// trigram-shingle GIN over a TEXT column, and otherwise
761    /// keeps the current "accepted and discarded" behaviour for
762    /// pg_dump compatibility.
763    pub opclass: Option<String>,
764}
765
766#[derive(Debug, Clone, Copy, PartialEq, Eq)]
767pub enum IndexMethod {
768    /// Default — B-tree over `IndexKey`. Used for equality / range
769    /// lookups on scalar columns.
770    BTree,
771    /// `USING hnsw` — NSW graph for kNN over a vector column.
772    Hnsw,
773    /// v6.7.1 — `USING brin` — Block Range INdex. Per-segment
774    /// metadata that records (min_key, max_key) for each page in a
775    /// cold-tier segment, on the indexed column. The optimizer
776    /// can use these summaries to skip pages whose range does NOT
777    /// overlap a query's WHERE predicate. BRIN indexes carry no
778    /// in-memory data — the summaries live in the segment v2
779    /// envelope's sidecar. Created via the standard
780    /// `CREATE INDEX … USING brin (col)` syntax.
781    Brin,
782    /// v7.12.3 — `USING gin` — inverted index over a `tsvector`
783    /// column. Posting lists map `lexeme word` → row locators; the
784    /// planner uses them to narrow `WHERE col @@ tsquery` to the
785    /// candidate rows whose vectors contain a matching term, then
786    /// re-evaluates the full `@@` semantics on each candidate.
787    /// Replaces the v7.9.26b `USING gin` → BTree fallback that
788    /// silently degraded to a full scan at query time.
789    Gin,
790}
791
792#[derive(Debug, Clone, PartialEq)]
793pub struct CreateTableStatement {
794    pub name: String,
795    pub columns: Vec<ColumnDef>,
796    /// `IF NOT EXISTS` — engine returns `CommandOk` no-op when the
797    /// table name already exists, instead of raising `DuplicateTable`.
798    pub if_not_exists: bool,
799    /// v7.6.0 — table-level `FOREIGN KEY (...) REFERENCES ...`
800    /// constraints. Column-level `REFERENCES` (single-column inline
801    /// form) is normalised into this vec at parse time so the engine
802    /// sees one uniform list.
803    pub foreign_keys: Vec<ForeignKeyConstraint>,
804    /// v7.9.18 — table-level constraints: `PRIMARY KEY (a, b)` and
805    /// `UNIQUE (a, b, ...)`. mailrs migration follow-up G1 + G6.
806    /// Engine resolves each into a BTree index named after the
807    /// constraint's leading column at CREATE TABLE time; INSERT
808    /// path enforces composite uniqueness via row scan on the
809    /// leading column index.
810    pub table_constraints: Vec<TableConstraint>,
811}
812
813/// v7.9.18 — table-level constraint at the end of a CREATE TABLE
814/// column list. Either a composite PRIMARY KEY or a UNIQUE
815/// (single- or multi-column).
816#[derive(Debug, Clone, PartialEq)]
817pub enum TableConstraint {
818    /// `PRIMARY KEY (col1, col2, ...)`. Implies NOT NULL on each
819    /// referenced column. Engine builds a BTree index named
820    /// `<table>_pkey` and enforces composite uniqueness on INSERT.
821    PrimaryKey {
822        name: Option<String>,
823        columns: Vec<String>,
824    },
825    /// `UNIQUE (col1, col2, ...)`. Engine builds a BTree index
826    /// named `<table>_<leading_col>_key` (single-column) or
827    /// `<table>_<leading_col>_<…>_key` (composite) and enforces
828    /// uniqueness on INSERT.
829    Unique {
830        name: Option<String>,
831        columns: Vec<String>,
832        /// v7.13.0 — `NULLS NOT DISTINCT` modifier (mailrs round-5
833        /// G10). PG 15+ flips the NULL handling so any number of
834        /// NULL rows collide on the constraint. Default is
835        /// `false` (NULLS DISTINCT, standard SQL behaviour).
836        nulls_not_distinct: bool,
837    },
838    /// v7.13.0 — `CHECK (<expr>)` table-level constraint
839    /// (mailrs round-5 G3). Column-level inline CHECKs fold into
840    /// this same variant at parse time. Engine evaluates the
841    /// predicate against each INSERT/UPDATE candidate row; a
842    /// false / NULL result rejects the mutation.
843    Check { name: Option<String>, expr: Expr },
844    /// v7.15.0 — MySQL `KEY name (cols)` / `INDEX name (cols)`
845    /// non-unique secondary-index declaration inline in CREATE
846    /// TABLE. Engine builds a BTree index on the leading column
847    /// (composite columns parse but only the leading column is
848    /// honoured at v7.15 — matches the existing
849    /// `CreateIndexStatement::extra_columns` semantics). Useful
850    /// for `mysql/blog`-style schemas that lean on routine
851    /// secondary indexes for ORM lookups.
852    Index {
853        name: Option<String>,
854        columns: Vec<String>,
855    },
856}
857
858#[derive(Debug, Clone, PartialEq)]
859#[allow(clippy::struct_excessive_bools)] // grammar-driven; each flag maps to a distinct PG column-constraint keyword
860pub struct ColumnDef {
861    pub name: String,
862    pub ty: ColumnTypeName,
863    pub nullable: bool,
864    /// `DEFAULT <expr>` literal supplied at CREATE TABLE. Engine
865    /// evaluates this once (with an empty row) and caches the resulting
866    /// `Value` on the column schema.
867    pub default: Option<Expr>,
868    /// MySQL-style `AUTO_INCREMENT` — the engine maintains a counter
869    /// per such column and fills the slot when INSERT leaves it
870    /// unbound (omitted from a column-list INSERT or explicitly NULL).
871    pub auto_increment: bool,
872    /// v7.9.13 — inline `PRIMARY KEY` column constraint. mailrs
873    /// migration follow-up F1. Implies `NOT NULL`. Engine creates
874    /// an implicit BTree index named `<table>_pkey` over this
875    /// column at CREATE TABLE time, satisfying the parent-side
876    /// index requirement for any FOREIGN KEY pointing at it.
877    pub is_primary_key: bool,
878    /// v7.13.0 — inline `UNIQUE` column constraint
879    /// (mailrs round-5 G2). The CREATE TABLE handler folds this
880    /// into a single-column `TableConstraint::Unique` so the
881    /// engine path stays uniform with table-level UNIQUE.
882    pub is_unique: bool,
883    /// v7.13.0 — inline `CHECK (<expr>)` column constraint
884    /// (mailrs round-5 G3). Stored alongside the column so the
885    /// CREATE TABLE handler can fold these into table-level
886    /// CHECK constraints. Multiple inline CHECKs on the same
887    /// column are concatenated with AND at the table level.
888    pub check: Option<Expr>,
889}
890
891/// v7.6.0 — A single FOREIGN KEY constraint. Both column-level
892/// `REFERENCES` and table-level `FOREIGN KEY (...) REFERENCES ...`
893/// parse into this shape — the column-level form has a single-entry
894/// `columns` / `parent_columns`.
895#[derive(Debug, Clone, PartialEq)]
896pub struct ForeignKeyConstraint {
897    /// Optional `CONSTRAINT <name>` prefix. Engine ignores the name
898    /// today but parses + stores it so a future ALTER TABLE DROP
899    /// CONSTRAINT can target by name (v7.6.8).
900    pub name: Option<String>,
901    /// Local columns participating in the FK (≥ 1).
902    pub columns: Vec<String>,
903    /// Referenced parent table.
904    pub parent_table: String,
905    /// Referenced parent columns. Must have the same arity as
906    /// `columns`; engine validates parent has a PK / UNIQUE index
907    /// on exactly this column set (v7.6.1).
908    pub parent_columns: Vec<String>,
909    /// `ON DELETE` action. Defaults to `Restrict` if absent.
910    pub on_delete: FkAction,
911    /// `ON UPDATE` action. Defaults to `Restrict` if absent.
912    pub on_update: FkAction,
913}
914
915/// v7.6.0 — Referential action for `ON DELETE` / `ON UPDATE`.
916#[derive(Debug, Clone, Copy, PartialEq, Eq)]
917pub enum FkAction {
918    /// Reject the parent mutation if any child row references it.
919    /// SQL spec default; SPG default when no clause is given.
920    Restrict,
921    /// Recursively propagate the parent's delete / update to the
922    /// child rows. Same TX.
923    Cascade,
924    /// Set the child FK column(s) to NULL. Requires the FK columns
925    /// to be NULL-able.
926    SetNull,
927    /// Set the child FK column(s) to their declared DEFAULT.
928    /// Requires the child column(s) to have DEFAULT.
929    SetDefault,
930    /// SQL spec `NO ACTION` (deferred check). SPG treats this as
931    /// `Restrict` because the single-writer model has no deferred
932    /// constraint window; the keyword is accepted for compatibility.
933    NoAction,
934}
935
936/// In-cell encoding for a `VECTOR(N)` column. v6.0.1 added the
937/// optional `USING <encoding>` clause; omitting it keeps the
938/// pre-v6 `F32` default. `Sq8` quantises each cell to a per-vector
939/// affine `(min, max, [u8; dim])` triple (4× compression). `F16`
940/// (v6.0.3, DDL keyword `HALF`) stores each element as IEEE-754
941/// binary16 (2× compression, ~3 decimal digits of precision).
942#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
943pub enum VecEncoding {
944    /// IEEE-754 binary32. Pre-v6 default; matches pgvector's
945    /// uncompressed `vector` type wire / storage layout.
946    #[default]
947    F32,
948    /// v6.0.1 SQ8 — per-vector affine 8-bit quantisation. See
949    /// `spg_storage::quantize::Sq8Vector` for the math + recall
950    /// envelope (≥ 0.95 on Gaussian / unit-sphere corpora at
951    /// dim ≥ 32).
952    Sq8,
953    /// v6.0.3 halfvec — IEEE-754 binary16 (half-precision)
954    /// per-element. DDL keyword `HALF` (pgvector convention).
955    /// Bit-exact dequantise to f32 at the storage layer; no
956    /// rerank pass needed for kNN search.
957    F16,
958}
959
960impl fmt::Display for VecEncoding {
961    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
962        match self {
963            Self::F32 => f.write_str("F32"),
964            Self::Sq8 => f.write_str("SQ8"),
965            // pgvector convention: DDL keyword is `HALF`, not `F16`.
966            Self::F16 => f.write_str("HALF"),
967        }
968    }
969}
970
971/// SQL-level type names. The mapping to the storage runtime's `DataType`
972/// happens in `spg-engine` — keeping `spg-sql` free of storage deps.
973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
974pub enum ColumnTypeName {
975    SmallInt,
976    Int,
977    BigInt,
978    Float,
979    Text,
980    /// `VARCHAR(N)` — TEXT capped at N Unicode characters.
981    Varchar(u32),
982    /// `CHAR(N)` — TEXT right-padded with spaces to exactly N characters.
983    Char(u32),
984    Bool,
985    /// pgvector fixed-dimension `VECTOR(N)`. v6.0.1 added the
986    /// `USING <encoding>` clause; omitting it surfaces as
987    /// `encoding = VecEncoding::F32` (the pre-v6 default).
988    Vector {
989        dim: u32,
990        encoding: VecEncoding,
991    },
992    /// `NUMERIC` / `NUMERIC(p)` / `NUMERIC(p, s)` — exact decimal.
993    /// Bare `NUMERIC` and `NUMERIC(p)` both surface with `scale=0`.
994    Numeric(u8, u8),
995    /// `DATE` — calendar day, no time-of-day component.
996    Date,
997    /// `TIMESTAMP` / `MySQL` `DATETIME` — instant with microsecond
998    /// precision.
999    Timestamp,
1000    /// v7.9.2 `TIMESTAMPTZ` / `TIMESTAMP WITH TIME ZONE`. SPG
1001    /// stores all timestamps as UTC microseconds-since-epoch and
1002    /// does not carry per-row offset (PG's internal representation
1003    /// is the same — TZ is a display convention). The distinction
1004    /// from `TIMESTAMP` exists for the PG-wire layer to advertise
1005    /// OID 1184 so sqlx-style clients decode into
1006    /// `chrono::DateTime<Utc>` instead of `NaiveDateTime`.
1007    Timestamptz,
1008    /// v4.9 `JSON` — text-backed JSON document. No parse-time
1009    /// validation; the engine round-trips the literal verbatim.
1010    /// PG OID 114 on the wire.
1011    Json,
1012    /// v7.9.0 `JSONB` — same storage shape as Json, advertised as
1013    /// PG OID 3802 on the wire so sqlx-style binary-typed clients
1014    /// decode without a custom type registration.
1015    Jsonb,
1016    /// v7.10.4 `BYTES` / `BYTEA` — raw binary blob. PG wire OID 17.
1017    /// Literal forms (decoded by the engine at coercion time):
1018    ///   - PG hex form: `'\xDEADBEEF'`
1019    ///   - Escape form: `'foo\\000bar'` (backslash octal triples)
1020    Bytes,
1021    /// v7.10.10 `TEXT[]` — single-dimension TEXT array. PG wire
1022    /// OID 1009. Literal forms accepted by the parser:
1023    ///   - `ARRAY['a', 'b', NULL]`
1024    ///   - `'{a,b,NULL}'::TEXT[]` (engine decodes the external
1025    ///     form at coerce time)
1026    TextArray,
1027    /// v7.11.13 `INT[]` — single-dimension i32 array. PG wire OID
1028    /// 1007. Same literal forms as TEXT[] (substituting integer
1029    /// elements).
1030    IntArray,
1031    /// v7.11.13 `BIGINT[]` — single-dimension i64 array. PG wire
1032    /// OID 1016.
1033    BigIntArray,
1034    /// v7.12.0 `tsvector` — PG full-text search lexeme set. PG
1035    /// wire OID 3614. Literal: `'foo:1 bar:2'::tsvector` (PG
1036    /// external form). G-CRIT-3.
1037    TsVector,
1038    /// v7.12.0 `tsquery` — PG full-text search parse tree. PG
1039    /// wire OID 3615.
1040    TsQuery,
1041}
1042
1043impl fmt::Display for ColumnTypeName {
1044    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1045        match self {
1046            Self::SmallInt => f.write_str("SMALLINT"),
1047            Self::Int => f.write_str("INT"),
1048            Self::BigInt => f.write_str("BIGINT"),
1049            Self::Float => f.write_str("FLOAT"),
1050            Self::Text => f.write_str("TEXT"),
1051            Self::Varchar(n) => write!(f, "VARCHAR({n})"),
1052            Self::Char(n) => write!(f, "CHAR({n})"),
1053            Self::Bool => f.write_str("BOOL"),
1054            Self::Vector { dim, encoding } => match encoding {
1055                VecEncoding::F32 => write!(f, "VECTOR({dim})"),
1056                VecEncoding::Sq8 => write!(f, "VECTOR({dim}) USING SQ8"),
1057                VecEncoding::F16 => write!(f, "VECTOR({dim}) USING HALF"),
1058            },
1059            Self::Json => f.write_str("JSON"),
1060            Self::Jsonb => f.write_str("JSONB"),
1061            Self::Bytes => f.write_str("BYTEA"),
1062            Self::TextArray => f.write_str("TEXT[]"),
1063            Self::IntArray => f.write_str("INT[]"),
1064            Self::BigIntArray => f.write_str("BIGINT[]"),
1065            Self::TsVector => f.write_str("TSVECTOR"),
1066            Self::TsQuery => f.write_str("TSQUERY"),
1067            Self::Numeric(p, s) => {
1068                if *s == 0 {
1069                    write!(f, "NUMERIC({p})")
1070                } else {
1071                    write!(f, "NUMERIC({p}, {s})")
1072                }
1073            }
1074            Self::Date => f.write_str("DATE"),
1075            Self::Timestamp => f.write_str("TIMESTAMP"),
1076            Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
1077        }
1078    }
1079}
1080
1081/// `UPDATE <table> SET col = expr [, ...] [WHERE cond]`. v4.4 — the
1082/// engine evaluates `expr` per matched row in the table's row order
1083/// and rewrites cells in place. Indexed columns are dropped + re-
1084/// inserted into the affected B-tree on each row change.
1085#[derive(Debug, Clone, PartialEq)]
1086pub struct UpdateStatement {
1087    pub table: String,
1088    pub assignments: Vec<(String, Expr)>,
1089    pub where_: Option<Expr>,
1090    /// v7.9.4 — `RETURNING <projection>`. None = no RETURNING
1091    /// clause (legacy CommandComplete path). Some = engine
1092    /// evaluates the projection over each mutated row and
1093    /// streams the result as a Rows QueryResult.
1094    pub returning: Option<Vec<SelectItem>>,
1095}
1096
1097/// `DELETE FROM <table> [WHERE cond]`. v4.4 — removes matched rows
1098/// from the active catalog and prunes them from every index.
1099#[derive(Debug, Clone, PartialEq)]
1100pub struct DeleteStatement {
1101    pub table: String,
1102    pub where_: Option<Expr>,
1103    /// v7.9.4 — `RETURNING <projection>`.
1104    pub returning: Option<Vec<SelectItem>>,
1105}
1106
1107#[derive(Debug, Clone, PartialEq)]
1108pub struct InsertStatement {
1109    pub table: String,
1110    /// Optional column list — `INSERT INTO t (a, b) VALUES (...)`. When
1111    /// `None`, every tuple is positional and must match the table arity.
1112    /// When `Some`, the engine maps each tuple slot to the named column and
1113    /// fills the rest with NULL (must be nullable).
1114    pub columns: Option<Vec<String>>,
1115    /// One or more `(expr, expr, ...)` tuples — the multi-row VALUES form.
1116    /// v1.3+ accepts `INSERT INTO t VALUES (a), (b)`. Empty when
1117    /// `select_source` is `Some` (the engine builds rows from the
1118    /// inner SELECT result set instead).
1119    pub rows: Vec<Vec<Expr>>,
1120    /// v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
1121    /// round-5 G4). When present, `rows` is empty and the engine
1122    /// materialises the SELECT result, coerces each output tuple to
1123    /// the target column types, and inserts as a single batch.
1124    pub select_source: Option<Box<SelectStatement>>,
1125    /// v7.9.7 — `ON CONFLICT (cols) DO { NOTHING | UPDATE SET … }`
1126    /// upsert clause. None = legacy INSERT (conflict raises a
1127    /// DuplicateKey error). mailrs migration blocker #2.
1128    pub on_conflict: Option<OnConflictClause>,
1129    /// v7.9.4 — `RETURNING <projection>`.
1130    pub returning: Option<Vec<SelectItem>>,
1131}
1132
1133/// v7.9.7 — INSERT upsert clause: `ON CONFLICT (target) DO action`.
1134#[derive(Debug, Clone, PartialEq)]
1135pub struct OnConflictClause {
1136    /// Local columns that identify the conflict (must match a
1137    /// UNIQUE / PRIMARY KEY index on the target table). Empty
1138    /// list means the user wrote `ON CONFLICT DO …` without a
1139    /// target — engine picks the table's first BTree index by
1140    /// convention.
1141    pub target_columns: Vec<String>,
1142    /// The action on conflict.
1143    pub action: OnConflictAction,
1144}
1145
1146/// v7.9.7 — action on conflict.
1147#[derive(Debug, Clone, PartialEq)]
1148pub enum OnConflictAction {
1149    /// `DO NOTHING` — INSERT proceeds for non-conflicting rows,
1150    /// silently skips conflicting ones.
1151    Nothing,
1152    /// `DO UPDATE SET col = expr [, …] [WHERE cond]`. `assignments`
1153    /// may reference `EXCLUDED.col` to read the incoming row's
1154    /// value (engine wires `EXCLUDED` as a virtual table).
1155    Update {
1156        assignments: Vec<(String, Expr)>,
1157        where_: Option<Expr>,
1158    },
1159}
1160
1161#[derive(Debug, Clone, PartialEq)]
1162pub struct SelectStatement {
1163    /// v4.11: `WITH name AS (SELECT ...) [, ...]` common-table
1164    /// expressions, materialised once at query start before the
1165    /// body SELECT runs. Empty for a regular SELECT. Non-recursive
1166    /// only — no `WITH RECURSIVE` for v4.x.
1167    pub ctes: Vec<Cte>,
1168    pub distinct: bool,
1169    pub items: Vec<SelectItem>,
1170    pub from: Option<FromClause>,
1171    pub where_: Option<Expr>,
1172    pub group_by: Option<Vec<Expr>>,
1173    /// v6.4.1 — `GROUP BY ALL` shortcut: when true, the planner
1174    /// expands `group_by` to every non-aggregate SELECT-list item
1175    /// before the executor runs. Mutually exclusive with an
1176    /// explicit `group_by` list (the parser sets exactly one).
1177    pub group_by_all: bool,
1178    /// `HAVING <expr>` — filter applied *after* `GROUP BY` aggregation.
1179    /// Supports aggregate calls (e.g. `HAVING count(*) > 1`); the
1180    /// aggregate executor resolves them through the same synthetic
1181    /// schema used for the SELECT items.
1182    pub having: Option<Expr>,
1183    /// UNION / UNION ALL chain. Empty for a plain SELECT. Each peer is
1184    /// itself a `SelectStatement` with `order_by = None` and `limit =
1185    /// None` (the parser enforces that — ORDER BY / LIMIT belong to the
1186    /// top of the chain).
1187    pub unions: Vec<(UnionKind, SelectStatement)>,
1188    /// v6.4.0 — multi-key ORDER BY. Empty `Vec` means no ORDER BY.
1189    /// Keys are matched left-to-right: first key decides, ties break
1190    /// to the second, etc.
1191    pub order_by: Vec<OrderBy>,
1192    /// `LIMIT <n>` — bound on row output. `n` is an integer
1193    /// literal **or** (v7.9.24) a placeholder `$N` resolved
1194    /// against the prepared-statement Bind values. mailrs
1195    /// migration follow-up H2.
1196    pub limit: Option<LimitExpr>,
1197    /// `OFFSET <n>` — drop the first `n` rows after ORDER BY but
1198    /// before LIMIT (so `LIMIT 10 OFFSET 5` keeps rows 6..=15).
1199    pub offset: Option<LimitExpr>,
1200}
1201
1202/// v7.9.24 — LIMIT / OFFSET value. Integer literal at parse
1203/// time or a placeholder `$N` resolved during extended-query
1204/// Bind. mailrs migration follow-up H2.
1205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1206pub enum LimitExpr {
1207    /// `LIMIT 10` — value known at parse time.
1208    Literal(u32),
1209    /// `LIMIT $N` — the 1-based parameter index, resolved against
1210    /// the bind values when the prepared statement executes.
1211    Placeholder(u16),
1212}
1213
1214impl fmt::Display for LimitExpr {
1215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1216        match self {
1217            Self::Literal(n) => write!(f, "{n}"),
1218            Self::Placeholder(n) => write!(f, "${n}"),
1219        }
1220    }
1221}
1222
1223impl LimitExpr {
1224    /// Convenience for the simple-query path where no placeholders
1225    /// can possibly exist. Returns the literal value or `None` if
1226    /// this is a placeholder (caller must surface as Unsupported).
1227    pub fn as_literal(self) -> Option<u32> {
1228        match self {
1229            Self::Literal(n) => Some(n),
1230            Self::Placeholder(_) => None,
1231        }
1232    }
1233}
1234
1235/// v7.9.24 — extract LIMIT / OFFSET as a `u32` literal. After
1236/// the engine's `substitute_placeholders` pass these are
1237/// always Literal; in the simple-query path a Placeholder
1238/// shape returns None (executor surfaces as
1239/// "LIMIT/OFFSET ${n} requires prepared-statement binding").
1240impl SelectStatement {
1241    #[must_use]
1242    pub fn limit_literal(&self) -> Option<u32> {
1243        self.limit.and_then(LimitExpr::as_literal)
1244    }
1245    #[must_use]
1246    pub fn offset_literal(&self) -> Option<u32> {
1247        self.offset.and_then(LimitExpr::as_literal)
1248    }
1249}
1250
1251#[derive(Debug, Clone, PartialEq)]
1252pub struct Cte {
1253    pub name: String,
1254    pub body: SelectStatement,
1255    /// v4.22: `WITH RECURSIVE` — set when the WITH clause had the
1256    /// RECURSIVE keyword. Applies to every CTE in the clause per
1257    /// PG semantics. A non-recursive body in a RECURSIVE WITH is
1258    /// allowed; the engine just runs it once.
1259    pub recursive: bool,
1260    /// v4.22: optional `WITH name(a, b, c)` column-name list. When
1261    /// non-empty, these override the body's output column names
1262    /// position-by-position; the engine errors out if the count
1263    /// doesn't match the body's projection width.
1264    pub column_overrides: Vec<String>,
1265}
1266
1267#[derive(Debug, Clone, PartialEq)]
1268pub struct OrderBy {
1269    pub expr: Expr,
1270    /// `false` = ASC (default), `true` = DESC.
1271    pub desc: bool,
1272}
1273
1274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1275pub enum UnionKind {
1276    /// `UNION` — dedupes the combined set.
1277    Distinct,
1278    /// `UNION ALL` — concatenates without dedup.
1279    All,
1280}
1281
1282#[derive(Debug, Clone, PartialEq)]
1283pub enum SelectItem {
1284    Wildcard,
1285    Expr { expr: Expr, alias: Option<String> },
1286}
1287
1288#[derive(Debug, Clone, PartialEq)]
1289pub struct TableRef {
1290    pub name: String,
1291    pub alias: Option<String>,
1292    /// v6.10.2 — `AS OF SEGMENT '<id>'` cold-tier time-travel.
1293    /// When `Some(id)`, the scan restricts to rows that live in
1294    /// segment `<id>` only — useful for forensic inspection of a
1295    /// specific freezer-emitted segment without exposing the hot
1296    /// tier. `AS OF TIMESTAMP <ts>` (PG-flavoured time travel)
1297    /// is STABILITY carve-out for v6.10 — needs the freezer to
1298    /// stamp each segment with a wall-clock at creation time.
1299    pub as_of_segment: Option<u32>,
1300    /// v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
1301    /// source. When `Some`, `name` is the alias (defaulting to
1302    /// `"unnest"` when no `AS` is given) and the engine builds a
1303    /// synthetic single-column table by evaluating the expression
1304    /// once at SELECT entry. Each TEXT[] element becomes one row;
1305    /// NULL elements become NULL cells. v7.11 supported
1306    /// uncorrelated UNNEST only as the FROM primary; v7.13.2
1307    /// (mailrs round-6 S5) widens to UNNEST in any FROM-list
1308    /// position (cross-join with regular tables).
1309    pub unnest_expr: Option<Box<Expr>>,
1310    /// v7.13.2 — mailrs round-6 S5. PG-standard
1311    /// `UNNEST(<arr>) AS alias(col_name)` column-list aliasing:
1312    /// when non-empty, the first entry overrides the projected
1313    /// column name for the unnested column. Empty = fall back to
1314    /// the table alias (pre-v7.13.2 behaviour).
1315    pub unnest_column_aliases: Vec<String>,
1316}
1317
1318/// FROM clause shape. v1.10 accepts a primary table plus a flat list of
1319/// joined peers — `FROM a [, b]* [INNER|LEFT] JOIN c ON expr ...`. The
1320/// joins evaluate left-associatively in nested-loop order.
1321#[derive(Debug, Clone, PartialEq)]
1322pub struct FromClause {
1323    pub primary: TableRef,
1324    pub joins: Vec<FromJoin>,
1325}
1326
1327#[derive(Debug, Clone, PartialEq)]
1328pub struct FromJoin {
1329    pub kind: JoinKind,
1330    pub table: TableRef,
1331    /// Required for INNER/LEFT; must be `None` for CROSS / comma-list.
1332    pub on: Option<Expr>,
1333}
1334
1335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1336pub enum JoinKind {
1337    Inner,
1338    Left,
1339    Cross,
1340}
1341
1342#[derive(Debug, Clone, PartialEq)]
1343pub enum Expr {
1344    Literal(Literal),
1345    Column(ColumnName),
1346    /// v6.1.1 — `$N` parameter placeholder for the extended query
1347    /// protocol. The number is 1-based per PostgreSQL convention.
1348    /// Evaluation looks up `params[N-1]` from the prepared-statement
1349    /// bind buffer; out-of-range indices raise a runtime error
1350    /// (same shape as a column-not-found miss).
1351    Placeholder(u16),
1352    Binary {
1353        lhs: Box<Expr>,
1354        op: BinOp,
1355        rhs: Box<Expr>,
1356    },
1357    Unary {
1358        op: UnOp,
1359        expr: Box<Expr>,
1360    },
1361    /// PG-style `expr::TYPE` cast. v1.3 supports VECTOR, INT, BIGINT, FLOAT,
1362    /// TEXT, BOOL targets; engine coerces at evaluation time.
1363    Cast {
1364        expr: Box<Expr>,
1365        target: CastTarget,
1366    },
1367    /// Postfix `IS NULL` / `IS NOT NULL`. Returns BOOL.
1368    IsNull {
1369        expr: Box<Expr>,
1370        negated: bool,
1371    },
1372    /// Function call `name(args...)`. v1.4 supports a small built-in set
1373    /// (length, upper, lower, abs, coalesce); unknown names error at eval
1374    /// time so the parser stays open for v1.5 aggregates.
1375    FunctionCall {
1376        name: String,
1377        args: Vec<Expr>,
1378    },
1379    /// SQL `LIKE` predicate. `pattern` evaluates to text at runtime;
1380    /// wildcards are `%` (any run) and `_` (one char), backslash escapes
1381    /// the next char (so `\%` matches a literal `%`).
1382    Like {
1383        expr: Box<Expr>,
1384        pattern: Box<Expr>,
1385        negated: bool,
1386    },
1387    /// v4.12 window function call: `name(args) OVER (PARTITION BY
1388    /// ... ORDER BY ...)`. Supports `ROW_NUMBER` / `RANK` /
1389    /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
1390    /// `AVG` / `COUNT` / `MIN` / `MAX`. The window frame defaults to "entire partition" for
1391    /// unordered windows and "from start of partition through
1392    /// current row" for ordered windows — no explicit ROWS /
1393    /// RANGE clause in v4.12 MVP.
1394    WindowFunction {
1395        name: String,
1396        args: Vec<Expr>,
1397        partition_by: Vec<Expr>,
1398        order_by: Vec<(Expr, bool /* desc */)>,
1399        /// v4.20 explicit frame. `None` means "use the default":
1400        /// whole-partition when unordered, running aggregate from
1401        /// partition start through current row when ordered.
1402        frame: Option<WindowFrame>,
1403        /// v6.4.2 — `IGNORE NULLS` / `RESPECT NULLS` modifier on
1404        /// LAG / LEAD / FIRST_VALUE / LAST_VALUE. Default is
1405        /// `Respect` (PG / ANSI default — NULLs participate). Other
1406        /// window functions ignore this flag.
1407        null_treatment: NullTreatment,
1408    },
1409    /// v4.10 scalar subquery — `(SELECT ...)` used in expression
1410    /// position. Must return exactly one row × one column at eval
1411    /// time; the engine errors out otherwise. Uncorrelated only —
1412    /// the inner SELECT cannot reference outer columns.
1413    ScalarSubquery(Box<SelectStatement>),
1414    /// v4.10 `[NOT] EXISTS (SELECT ...)`. Returns Bool. Inner
1415    /// projection is ignored; only row-count matters.
1416    Exists {
1417        subquery: Box<SelectStatement>,
1418        negated: bool,
1419    },
1420    /// v4.10 `expr [NOT] IN (SELECT ...)`. Inner SELECT must
1421    /// project exactly one column; membership is tested by Eq
1422    /// against each row's value (NULL handling follows ANSI:
1423    /// NULL ∈ list ⇒ NULL ; otherwise present ⇒ true).
1424    InSubquery {
1425        expr: Box<Expr>,
1426        subquery: Box<SelectStatement>,
1427        negated: bool,
1428    },
1429    /// `EXTRACT(<field> FROM <source>)` — pull an integer component
1430    /// out of a `DATE` or `TIMESTAMP`. Parsed as its own AST node
1431    /// because the `FROM` keyword is what separates the two halves,
1432    /// not a comma.
1433    Extract {
1434        field: ExtractField,
1435        source: Box<Expr>,
1436    },
1437    /// v7.10.10 — `ARRAY[expr, expr, …]` array constructor. Each
1438    /// element is evaluated independently; NULLs are allowed.
1439    /// v7.10 supports only single-dimension TEXT[] semantically;
1440    /// non-text elements coerce at engine evaluation time when
1441    /// the surrounding context (column type / cast) makes the
1442    /// target clear.
1443    Array(Vec<Expr>),
1444    /// v7.10.10 — array subscript `arr[i]`. PG 1-based; the
1445    /// engine returns NULL for out-of-range indices.
1446    ArraySubscript {
1447        target: Box<Expr>,
1448        index: Box<Expr>,
1449    },
1450    /// v7.10.12 — `expr op ANY(arr)` and `expr op ALL(arr)`. The
1451    /// operator is the comparison binary op (Eq / Ne / Lt / …);
1452    /// the engine desugars: `ANY` returns true if any element
1453    /// satisfies; `ALL` returns true only if every element does.
1454    /// NULL handling follows PG's three-valued logic.
1455    AnyAll {
1456        expr: Box<Expr>,
1457        op: BinOp,
1458        array: Box<Expr>,
1459        /// `true` = ANY, `false` = ALL.
1460        is_any: bool,
1461    },
1462    /// v7.13.0 — `CASE WHEN <cond> THEN <val> ... ELSE <val> END`
1463    /// (searched form, `operand` is None) and
1464    /// `CASE <expr> WHEN <val> THEN <val> ... END` (simple form,
1465    /// `operand` is the lead expression compared against each
1466    /// branch's match). Each `(when_expr, then_expr)` branch
1467    /// stays as written; engine short-circuits on the first match.
1468    /// `else_branch` is `None` when no ELSE; evaluates to NULL.
1469    /// mailrs round-5 G9.
1470    Case {
1471        operand: Option<Box<Expr>>,
1472        branches: Vec<(Expr, Expr)>,
1473        else_branch: Option<Box<Expr>>,
1474    },
1475}
1476
1477/// v6.4.2 — null treatment on `LAG` / `LEAD` / `FIRST_VALUE` /
1478/// `LAST_VALUE`. PG / ANSI default is `Respect` — NULLs participate
1479/// in the offset walk. `Ignore` causes the function to skip NULL
1480/// values in the argument expression, returning the next non-NULL.
1481#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1482pub enum NullTreatment {
1483    #[default]
1484    Respect,
1485    Ignore,
1486}
1487
1488/// v4.20 explicit window frame: `ROWS|RANGE BETWEEN <bound> AND
1489/// <bound>`. `end` is `None` for the shorthand "ROWS <bound>"
1490/// where end implicitly = CURRENT ROW.
1491#[derive(Debug, Clone, PartialEq, Eq)]
1492pub struct WindowFrame {
1493    pub kind: FrameKind,
1494    pub start: FrameBound,
1495    pub end: Option<FrameBound>,
1496}
1497
1498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1499pub enum FrameKind {
1500    Rows,
1501    Range,
1502}
1503
1504#[derive(Debug, Clone, PartialEq, Eq)]
1505pub enum FrameBound {
1506    UnboundedPreceding,
1507    OffsetPreceding(u64),
1508    CurrentRow,
1509    OffsetFollowing(u64),
1510    UnboundedFollowing,
1511}
1512
1513impl fmt::Display for FrameBound {
1514    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1515        match self {
1516            Self::UnboundedPreceding => f.write_str("UNBOUNDED PRECEDING"),
1517            Self::OffsetPreceding(n) => write!(f, "{n} PRECEDING"),
1518            Self::CurrentRow => f.write_str("CURRENT ROW"),
1519            Self::OffsetFollowing(n) => write!(f, "{n} FOLLOWING"),
1520            Self::UnboundedFollowing => f.write_str("UNBOUNDED FOLLOWING"),
1521        }
1522    }
1523}
1524
1525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1526pub enum ExtractField {
1527    Year,
1528    Month,
1529    Day,
1530    Hour,
1531    Minute,
1532    Second,
1533    Microsecond,
1534}
1535
1536impl fmt::Display for ExtractField {
1537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1538        f.write_str(match self {
1539            Self::Year => "YEAR",
1540            Self::Month => "MONTH",
1541            Self::Day => "DAY",
1542            Self::Hour => "HOUR",
1543            Self::Minute => "MINUTE",
1544            Self::Second => "SECOND",
1545            Self::Microsecond => "MICROSECOND",
1546        })
1547    }
1548}
1549
1550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1551pub enum CastTarget {
1552    Int,
1553    BigInt,
1554    Float,
1555    Text,
1556    Bool,
1557    Vector,
1558    Date,
1559    Timestamp,
1560    /// v7.9.25 — `::INTERVAL` and `::TIMESTAMPTZ`. mailrs follow-up
1561    /// H3a. Engine reuses the existing runtime-interval / timestamp
1562    /// paths (parse the text input, return the matching Value).
1563    Interval,
1564    Timestamptz,
1565    /// v7.9.25 — `::JSON` and `::JSONB`. SPG already has both
1566    /// types (v7.9.0); the cast just routes Text→Json with the
1567    /// requested OID for the wire layer.
1568    Json,
1569    Jsonb,
1570    /// v7.9.26 — `::regtype` / `::regclass`. Parsed for PG dump
1571    /// compatibility; engine surfaces as Unsupported with a
1572    /// hint to use `SHOW TABLES` or `spg_table_ddl`. mailrs F3b.
1573    RegType,
1574    RegClass,
1575    /// v7.10.11 — `::TEXT[]`. Engine decodes the LHS Text into
1576    /// the PG external array form `{a,b,NULL}`.
1577    TextArray,
1578    /// v7.11.13 — `::INT[]` / `::BIGINT[]`. Decodes PG external
1579    /// `{1,2,3}` or widens a `TextArray` whose elements are
1580    /// integer-shaped.
1581    IntArray,
1582    BigIntArray,
1583    /// v7.12.0 — `::tsvector` / `::tsquery`. Decodes the PG
1584    /// external form text representation. Used by pg_dump output
1585    /// and by `WHERE col @@ 'term'::tsquery` literal patterns.
1586    TsVector,
1587    TsQuery,
1588}
1589
1590impl fmt::Display for CastTarget {
1591    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1592        f.write_str(match self {
1593            Self::Int => "int",
1594            Self::BigInt => "bigint",
1595            Self::Float => "float",
1596            Self::Text => "text",
1597            Self::Bool => "bool",
1598            Self::Vector => "vector",
1599            Self::Interval => "interval",
1600            Self::Timestamptz => "timestamptz",
1601            Self::Json => "json",
1602            Self::Jsonb => "jsonb",
1603            Self::RegType => "regtype",
1604            Self::RegClass => "regclass",
1605            Self::Date => "date",
1606            Self::Timestamp => "timestamp",
1607            Self::TextArray => "TEXT[]",
1608            Self::IntArray => "INT[]",
1609            Self::BigIntArray => "BIGINT[]",
1610            Self::TsVector => "tsvector",
1611            Self::TsQuery => "tsquery",
1612        })
1613    }
1614}
1615
1616#[derive(Debug, Clone, PartialEq)]
1617pub enum Literal {
1618    Integer(i64),
1619    Float(f64),
1620    String(String),
1621    Bool(bool),
1622    Null,
1623    /// pgvector-style array literal, e.g. `[1, 2.5, -3]`.
1624    Vector(Vec<f32>),
1625    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — calendar-aware span.
1626    /// Split into a months part (because a month is not a fixed number of
1627    /// days) and a microseconds part (everything sub-month). `text` keeps
1628    /// the original spelling so Display round-trips byte-for-byte.
1629    Interval {
1630        months: i32,
1631        micros: i64,
1632        text: String,
1633    },
1634}
1635
1636#[derive(Debug, Clone, PartialEq, Eq)]
1637pub struct ColumnName {
1638    pub qualifier: Option<String>,
1639    pub name: String,
1640}
1641
1642#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1643pub enum BinOp {
1644    Or,
1645    And,
1646    Eq,
1647    NotEq,
1648    /// v7.9.27b — PG `a IS DISTINCT FROM b` / `a IS NOT DISTINCT
1649    /// FROM b`. NULL-safe equality: NULL IS NOT DISTINCT FROM
1650    /// NULL → true, NULL IS DISTINCT FROM NULL → false. The
1651    /// non-NULL behaviour matches `<>` / `=` exactly. Common in
1652    /// PG-style JOIN ON predicates and pg_dump output.
1653    IsDistinctFrom,
1654    IsNotDistinctFrom,
1655    Lt,
1656    LtEq,
1657    Gt,
1658    GtEq,
1659    Add,
1660    Sub,
1661    Mul,
1662    Div,
1663    /// pgvector L2 (Euclidean) distance `<->`. Defined for two vector
1664    /// operands of equal dimension; engine returns `Value::Float(d)`.
1665    L2Distance,
1666    /// pgvector inner-product `<#>` — returns `-Σ aᵢ bᵢ` so "smaller =
1667    /// more similar" remains true (matches pgvector's published convention).
1668    InnerProduct,
1669    /// pgvector cosine distance `<=>` — `1 - (a·b)/(|a| |b|)`.
1670    CosineDistance,
1671    /// SQL string concatenation `||`. NULL propagates.
1672    Concat,
1673    /// v4.14 `json -> key` — element access by string key (object)
1674    /// or integer index (array). Returns a JSON value.
1675    JsonGet,
1676    /// v4.14 `json ->> key` — same access, returns the result as
1677    /// TEXT (unwraps a top-level JSON string; renders other scalars
1678    /// as their canonical text).
1679    JsonGetText,
1680    /// v6.4.5 `json #> path_text` — walk the path encoded as a PG
1681    /// text array literal like `'{a,0,b}'`. Returns JSON.
1682    JsonGetPath,
1683    /// v6.4.5 `json #>> path_text` — same walk, returns TEXT.
1684    JsonGetPathText,
1685    /// v6.4.5 `json @> sub_json` — containment. Returns BOOL; true
1686    /// when every key/value in `sub_json` is structurally present in
1687    /// the left side. Matches PG semantics (top-level + recursive).
1688    JsonContains,
1689    /// v7.12.2 `tsvector @@ tsquery` — FTS match. Returns BOOL;
1690    /// 3VL on NULL. Symmetric: PG also accepts `tsquery @@
1691    /// tsvector` and engine eval normalises either ordering.
1692    TsMatch,
1693}
1694
1695#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1696pub enum UnOp {
1697    Not,
1698    Neg,
1699}
1700
1701// --- Display impls (round-trip-safe) --------------------------------------
1702
1703impl fmt::Display for Statement {
1704    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1705        match self {
1706            Self::Empty => Ok(()),
1707            Self::DropTable { names, if_exists } => {
1708                f.write_str("DROP TABLE ")?;
1709                if *if_exists {
1710                    f.write_str("IF EXISTS ")?;
1711                }
1712                for (i, n) in names.iter().enumerate() {
1713                    if i > 0 {
1714                        f.write_str(", ")?;
1715                    }
1716                    write!(f, "{}", quote_ident(n))?;
1717                }
1718                Ok(())
1719            }
1720            Self::DropIndex { name, if_exists } => {
1721                f.write_str("DROP INDEX ")?;
1722                if *if_exists {
1723                    f.write_str("IF EXISTS ")?;
1724                }
1725                write!(f, "{}", quote_ident(name))
1726            }
1727            Self::Select(s) => s.fmt(f),
1728            Self::CreateTable(s) => s.fmt(f),
1729            Self::CreateIndex(s) => s.fmt(f),
1730            Self::Insert(s) => s.fmt(f),
1731            Self::Update(s) => s.fmt(f),
1732            Self::Delete(s) => s.fmt(f),
1733            Self::Begin => f.write_str("BEGIN"),
1734            Self::Commit => f.write_str("COMMIT"),
1735            Self::Rollback => f.write_str("ROLLBACK"),
1736            Self::Savepoint(n) => write!(f, "SAVEPOINT {}", quote_ident(n)),
1737            Self::RollbackToSavepoint(n) => write!(f, "ROLLBACK TO SAVEPOINT {}", quote_ident(n)),
1738            Self::ReleaseSavepoint(n) => write!(f, "RELEASE SAVEPOINT {}", quote_ident(n)),
1739            Self::ShowTables => f.write_str("SHOW TABLES"),
1740            Self::ShowColumns(t) => write!(f, "SHOW COLUMNS FROM {}", quote_ident(t)),
1741            Self::CreateUser(s) => write!(
1742                f,
1743                "CREATE USER {} WITH PASSWORD '<redacted>' ROLE '{}'",
1744                quote_ident(&s.name),
1745                s.role
1746            ),
1747            Self::DropUser(n) => write!(f, "DROP USER {}", quote_ident(n)),
1748            Self::ShowUsers => f.write_str("SHOW USERS"),
1749            Self::ShowPublications => f.write_str("SHOW PUBLICATIONS"),
1750            Self::ShowSubscriptions => f.write_str("SHOW SUBSCRIPTIONS"),
1751            Self::CreateSubscription(s) => {
1752                write!(
1753                    f,
1754                    "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION ",
1755                    quote_ident(&s.name),
1756                    s.conn_str.replace('\'', "''")
1757                )?;
1758                for (i, p) in s.publications.iter().enumerate() {
1759                    if i > 0 {
1760                        f.write_str(", ")?;
1761                    }
1762                    write!(f, "{}", quote_ident(p))?;
1763                }
1764                Ok(())
1765            }
1766            Self::DropSubscription(name) => {
1767                write!(f, "DROP SUBSCRIPTION {}", quote_ident(name))
1768            }
1769            Self::WaitForWalPosition { pos, timeout_ms } => {
1770                write!(f, "WAIT FOR WAL POSITION {pos}")?;
1771                if let Some(ms) = timeout_ms {
1772                    write!(f, " WITH TIMEOUT {ms}")?;
1773                }
1774                Ok(())
1775            }
1776            Self::Analyze(None) => f.write_str("ANALYZE"),
1777            Self::Analyze(Some(t)) => write!(f, "ANALYZE {}", quote_ident(t)),
1778            Self::CompactColdSegments => f.write_str("COMPACT COLD SEGMENTS"),
1779            Self::Explain(e) => {
1780                if e.suggest {
1781                    write!(f, "EXPLAIN (SUGGEST) {}", e.inner)
1782                } else if e.analyze {
1783                    write!(f, "EXPLAIN ANALYZE {}", e.inner)
1784                } else {
1785                    write!(f, "EXPLAIN {}", e.inner)
1786                }
1787            }
1788            Self::AlterIndex(a) => {
1789                write!(f, "ALTER INDEX ")?;
1790                match &a.target {
1791                    AlterIndexTarget::Rebuild { encoding } => {
1792                        write!(f, "{} REBUILD", quote_ident(&a.name))?;
1793                        if let Some(enc) = encoding {
1794                            write!(f, " WITH (encoding = {enc})")?;
1795                        }
1796                        Ok(())
1797                    }
1798                    AlterIndexTarget::Rename { new, if_exists } => {
1799                        if *if_exists {
1800                            f.write_str("IF EXISTS ")?;
1801                        }
1802                        write!(f, "{} RENAME TO {}", quote_ident(&a.name), quote_ident(new))
1803                    }
1804                }
1805            }
1806            Self::AlterTable(a) => {
1807                write!(f, "ALTER TABLE {} ", quote_ident(&a.name))?;
1808                for (i, t) in a.targets.iter().enumerate() {
1809                    if i > 0 {
1810                        f.write_str(", ")?;
1811                    }
1812                    fmt_alter_target(f, t)?;
1813                }
1814                Ok(())
1815            }
1816            Self::CreatePublication(p) => {
1817                write!(f, "CREATE PUBLICATION {}", quote_ident(&p.name))?;
1818                match &p.scope {
1819                    PublicationScope::AllTables => f.write_str(" FOR ALL TABLES"),
1820                    PublicationScope::ForTables(ts) => {
1821                        f.write_str(" FOR TABLE ")?;
1822                        for (i, t) in ts.iter().enumerate() {
1823                            if i > 0 {
1824                                f.write_str(", ")?;
1825                            }
1826                            write!(f, "{}", quote_ident(t))?;
1827                        }
1828                        Ok(())
1829                    }
1830                    PublicationScope::AllTablesExcept(ts) => {
1831                        f.write_str(" FOR ALL TABLES EXCEPT ")?;
1832                        for (i, t) in ts.iter().enumerate() {
1833                            if i > 0 {
1834                                f.write_str(", ")?;
1835                            }
1836                            write!(f, "{}", quote_ident(t))?;
1837                        }
1838                        Ok(())
1839                    }
1840                }
1841            }
1842            Self::CreateExtension(name) => {
1843                write!(f, "CREATE EXTENSION IF NOT EXISTS {}", quote_ident(name))
1844            }
1845            Self::DoBlock(body) => write!(f, "DO $$ {body} $$"),
1846            Self::DropPublication(name) => {
1847                write!(f, "DROP PUBLICATION {}", quote_ident(name))
1848            }
1849            Self::SetParameter { name, value } => {
1850                write!(f, "SET {name} = ")?;
1851                match value {
1852                    SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
1853                    SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s),
1854                    SetValue::Default => f.write_str("DEFAULT"),
1855                }
1856            }
1857            Self::SetParameterList(pairs) => {
1858                f.write_str("SET ")?;
1859                for (i, (name, value)) in pairs.iter().enumerate() {
1860                    if i > 0 {
1861                        f.write_str(", ")?;
1862                    }
1863                    write!(f, "{name} = ")?;
1864                    match value {
1865                        SetValue::String(s) => write!(f, "'{}'", s.replace('\'', "''"))?,
1866                        SetValue::Ident(s) | SetValue::Number(s) => f.write_str(s)?,
1867                        SetValue::Default => f.write_str("DEFAULT")?,
1868                    }
1869                }
1870                Ok(())
1871            }
1872            Self::ResetParameter(None) => f.write_str("RESET ALL"),
1873            Self::ResetParameter(Some(name)) => write!(f, "RESET {name}"),
1874            Self::CreateFunction(s) => s.fmt(f),
1875            Self::CreateTrigger(s) => s.fmt(f),
1876            Self::DropTrigger {
1877                name,
1878                table,
1879                if_exists,
1880            } => {
1881                f.write_str("DROP TRIGGER ")?;
1882                if *if_exists {
1883                    f.write_str("IF EXISTS ")?;
1884                }
1885                write!(f, "{} ON {}", quote_ident(name), quote_ident(table))
1886            }
1887            Self::DropFunction { name, if_exists } => {
1888                f.write_str("DROP FUNCTION ")?;
1889                if *if_exists {
1890                    f.write_str("IF EXISTS ")?;
1891                }
1892                write!(f, "{}", quote_ident(name))
1893            }
1894        }
1895    }
1896}
1897
1898impl fmt::Display for CreateFunctionStatement {
1899    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1900        f.write_str("CREATE ")?;
1901        if self.or_replace {
1902            f.write_str("OR REPLACE ")?;
1903        }
1904        write!(f, "FUNCTION {}(", quote_ident(&self.name))?;
1905        for (i, arg) in self.args.iter().enumerate() {
1906            if i > 0 {
1907                f.write_str(", ")?;
1908            }
1909            match arg.mode {
1910                FunctionArgMode::In => {}
1911                FunctionArgMode::Out => f.write_str("OUT ")?,
1912                FunctionArgMode::InOut => f.write_str("INOUT ")?,
1913            }
1914            if let Some(name) = &arg.name {
1915                write!(f, "{} ", quote_ident(name))?;
1916            }
1917            match &arg.ty {
1918                FunctionArgType::Typed(t) => write!(f, "{t}")?,
1919                FunctionArgType::Raw(s) => f.write_str(s)?,
1920            }
1921        }
1922        f.write_str(") RETURNS ")?;
1923        match &self.returns {
1924            FunctionReturn::Trigger => f.write_str("TRIGGER")?,
1925            FunctionReturn::Void => f.write_str("VOID")?,
1926            FunctionReturn::Type(t) => write!(f, "{t}")?,
1927            FunctionReturn::Other(s) => f.write_str(s)?,
1928        }
1929        write!(f, " LANGUAGE {} AS $$", self.language)?;
1930        match &self.body {
1931            FunctionBody::PlPgSql(b) => write!(f, "\n{b}\n")?,
1932            FunctionBody::Raw(s) => f.write_str(s)?,
1933        }
1934        f.write_str("$$")
1935    }
1936}
1937
1938impl fmt::Display for PlPgSqlBlock {
1939    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1940        if !self.declarations.is_empty() {
1941            f.write_str("DECLARE\n")?;
1942            for d in &self.declarations {
1943                write!(f, "  {} ", quote_ident(&d.name))?;
1944                match &d.ty {
1945                    FunctionArgType::Typed(t) => write!(f, "{t}")?,
1946                    FunctionArgType::Raw(s) => f.write_str(s)?,
1947                }
1948                if let Some(e) = &d.default {
1949                    write!(f, " := {e}")?;
1950                }
1951                f.write_str(";\n")?;
1952            }
1953        }
1954        f.write_str("BEGIN\n")?;
1955        for stmt in &self.statements {
1956            writeln!(f, "  {stmt};")?;
1957        }
1958        f.write_str("END")
1959    }
1960}
1961
1962impl fmt::Display for PlPgSqlStmt {
1963    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1964        match self {
1965            Self::Assign { target, value } => write!(f, "{target} := {value}"),
1966            Self::SelectInto { var, body } => write!(f, "{body} INTO {var}"),
1967            Self::Return(t) => match t {
1968                ReturnTarget::New => f.write_str("RETURN NEW"),
1969                ReturnTarget::Old => f.write_str("RETURN OLD"),
1970                ReturnTarget::Null => f.write_str("RETURN NULL"),
1971                ReturnTarget::Expr(e) => write!(f, "RETURN {e}"),
1972            },
1973            Self::If {
1974                branches,
1975                else_branch,
1976            } => {
1977                for (i, (cond, body)) in branches.iter().enumerate() {
1978                    if i == 0 {
1979                        write!(f, "IF {cond} THEN ")?;
1980                    } else {
1981                        write!(f, " ELSIF {cond} THEN ")?;
1982                    }
1983                    for (j, s) in body.iter().enumerate() {
1984                        if j > 0 {
1985                            f.write_str("; ")?;
1986                        }
1987                        write!(f, "{s}")?;
1988                    }
1989                }
1990                if !else_branch.is_empty() {
1991                    f.write_str(" ELSE ")?;
1992                    for (j, s) in else_branch.iter().enumerate() {
1993                        if j > 0 {
1994                            f.write_str("; ")?;
1995                        }
1996                        write!(f, "{s}")?;
1997                    }
1998                }
1999                f.write_str(" END IF")
2000            }
2001            Self::Raise {
2002                level,
2003                message,
2004                args,
2005            } => {
2006                let lvl = match level {
2007                    RaiseLevel::Notice => "NOTICE",
2008                    RaiseLevel::Warning => "WARNING",
2009                    RaiseLevel::Info => "INFO",
2010                    RaiseLevel::Log => "LOG",
2011                    RaiseLevel::Debug => "DEBUG",
2012                    RaiseLevel::Exception => "EXCEPTION",
2013                };
2014                write!(f, "RAISE {lvl} '{}'", message.replace('\'', "''"))?;
2015                for a in args {
2016                    write!(f, ", {a}")?;
2017                }
2018                Ok(())
2019            }
2020            Self::EmbeddedSql(s) => write!(f, "{s}"),
2021        }
2022    }
2023}
2024
2025impl fmt::Display for AssignTarget {
2026    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2027        match self {
2028            Self::NewColumn(c) => write!(f, "NEW.{}", quote_ident(c)),
2029            Self::OldColumn(c) => write!(f, "OLD.{}", quote_ident(c)),
2030            Self::Local(n) => f.write_str(n),
2031        }
2032    }
2033}
2034
2035impl fmt::Display for CreateTriggerStatement {
2036    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2037        f.write_str("CREATE ")?;
2038        if self.or_replace {
2039            f.write_str("OR REPLACE ")?;
2040        }
2041        write!(f, "TRIGGER {} ", quote_ident(&self.name))?;
2042        match self.timing {
2043            TriggerTiming::Before => f.write_str("BEFORE")?,
2044            TriggerTiming::After => f.write_str("AFTER")?,
2045            TriggerTiming::InsteadOf => f.write_str("INSTEAD OF")?,
2046        }
2047        for (i, e) in self.events.iter().enumerate() {
2048            if i == 0 {
2049                f.write_str(" ")?;
2050            } else {
2051                f.write_str(" OR ")?;
2052            }
2053            match e {
2054                TriggerEvent::Insert => f.write_str("INSERT")?,
2055                TriggerEvent::Update => {
2056                    f.write_str("UPDATE")?;
2057                    if !self.update_columns.is_empty() {
2058                        f.write_str(" OF ")?;
2059                        for (j, col) in self.update_columns.iter().enumerate() {
2060                            if j > 0 {
2061                                f.write_str(", ")?;
2062                            }
2063                            f.write_str(&quote_ident(col))?;
2064                        }
2065                    }
2066                }
2067                TriggerEvent::Delete => f.write_str("DELETE")?,
2068                TriggerEvent::Truncate => f.write_str("TRUNCATE")?,
2069            }
2070        }
2071        write!(f, " ON {} FOR EACH ", quote_ident(&self.table))?;
2072        match self.for_each {
2073            TriggerForEach::Row => f.write_str("ROW")?,
2074            TriggerForEach::Statement => f.write_str("STATEMENT")?,
2075        }
2076        write!(f, " EXECUTE FUNCTION {}()", quote_ident(&self.function))
2077    }
2078}
2079
2080impl fmt::Display for CreateIndexStatement {
2081    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2082        if self.is_unique {
2083            f.write_str("CREATE UNIQUE INDEX ")?;
2084        } else {
2085            f.write_str("CREATE INDEX ")?;
2086        }
2087        if self.if_not_exists {
2088            f.write_str("IF NOT EXISTS ")?;
2089        }
2090        write!(
2091            f,
2092            "{} ON {} ",
2093            quote_ident(&self.name),
2094            quote_ident(&self.table)
2095        )?;
2096        match self.method {
2097            IndexMethod::Hnsw => f.write_str("USING hnsw ")?,
2098            IndexMethod::Brin => f.write_str("USING brin ")?,
2099            IndexMethod::Gin => f.write_str("USING gin ")?,
2100            IndexMethod::BTree => {}
2101        }
2102        if let Some(expr) = &self.expression {
2103            write!(f, "({})", expr)?;
2104        } else if self.extra_columns.is_empty() {
2105            // v7.15.0 — preserve operator class on round-trip
2106            // (`(col opclass)`) so WAL replay reconstructs the
2107            // engine-routing intent (e.g. `gin_trgm_ops` →
2108            // trigram-GIN build path).
2109            if let Some(op) = &self.opclass {
2110                write!(f, "({} {})", quote_ident(&self.column), op)?;
2111            } else {
2112                write!(f, "({})", quote_ident(&self.column))?;
2113            }
2114        } else {
2115            // v7.9.14 — multi-column key. Emit each column quoted
2116            // so the round-tripped form re-parses to identical AST.
2117            f.write_str("(")?;
2118            write!(f, "{}", quote_ident(&self.column))?;
2119            for c in &self.extra_columns {
2120                write!(f, ", {}", quote_ident(c))?;
2121            }
2122            f.write_str(")")?;
2123        }
2124        if !self.included_columns.is_empty() {
2125            f.write_str(" INCLUDE (")?;
2126            for (i, c) in self.included_columns.iter().enumerate() {
2127                if i > 0 {
2128                    f.write_str(", ")?;
2129                }
2130                write!(f, "{}", quote_ident(c))?;
2131            }
2132            f.write_str(")")?;
2133        }
2134        if let Some(pred) = &self.partial_predicate {
2135            write!(f, " WHERE {}", pred)?;
2136        }
2137        Ok(())
2138    }
2139}
2140
2141impl fmt::Display for CreateTableStatement {
2142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2143        f.write_str("CREATE TABLE ")?;
2144        if self.if_not_exists {
2145            f.write_str("IF NOT EXISTS ")?;
2146        }
2147        write!(f, "{} (", quote_ident(&self.name))?;
2148        for (i, col) in self.columns.iter().enumerate() {
2149            if i > 0 {
2150                f.write_str(", ")?;
2151            }
2152            write!(f, "{col}")?;
2153        }
2154        // v7.6.0 — render FK constraints in table-level form, after
2155        // the column list. WAL replay round-trips through Display, so
2156        // every FK must serialise here for replay to reconstruct the
2157        // schema bit-for-bit.
2158        for fk in &self.foreign_keys {
2159            f.write_str(", ")?;
2160            write!(f, "{fk}")?;
2161        }
2162        // v7.13.0 — render table-level constraints (PRIMARY KEY /
2163        // UNIQUE / CHECK) so WAL replay reconstructs them. Inline
2164        // column-level UNIQUE / CHECK get lifted to this list at
2165        // parse time, so emitting only here avoids double-counting.
2166        for tc in &self.table_constraints {
2167            f.write_str(", ")?;
2168            write!(f, "{tc}")?;
2169        }
2170        f.write_str(")")
2171    }
2172}
2173
2174fn fmt_alter_target(f: &mut fmt::Formatter<'_>, t: &AlterTableTarget) -> fmt::Result {
2175    match t {
2176        AlterTableTarget::SetHotTierBytes(n) => {
2177            write!(f, "SET hot_tier_bytes = {n}")
2178        }
2179        AlterTableTarget::AddForeignKey(fk) => write!(f, "ADD {fk}"),
2180        AlterTableTarget::DropForeignKey { name, if_exists } => {
2181            f.write_str("DROP CONSTRAINT ")?;
2182            if *if_exists {
2183                f.write_str("IF EXISTS ")?;
2184            }
2185            write!(f, "{}", quote_ident(name))
2186        }
2187        AlterTableTarget::AddColumn {
2188            column,
2189            if_not_exists,
2190        } => {
2191            f.write_str("ADD COLUMN ")?;
2192            if *if_not_exists {
2193                f.write_str("IF NOT EXISTS ")?;
2194            }
2195            write!(f, "{} {}", quote_ident(&column.name), column.ty)?;
2196            if !column.nullable {
2197                f.write_str(" NOT NULL")?;
2198            }
2199            if let Some(d) = &column.default {
2200                write!(f, " DEFAULT {d}")?;
2201            }
2202            if column.auto_increment {
2203                f.write_str(" AUTO_INCREMENT")?;
2204            }
2205            if column.is_primary_key {
2206                f.write_str(" PRIMARY KEY")?;
2207            }
2208            Ok(())
2209        }
2210        AlterTableTarget::AlterColumnType {
2211            column,
2212            new_type,
2213            using,
2214        } => {
2215            write!(f, "ALTER COLUMN {} TYPE {new_type}", quote_ident(column))?;
2216            if let Some(u) = using {
2217                write!(f, " USING {u}")?;
2218            }
2219            Ok(())
2220        }
2221        AlterTableTarget::DropColumn {
2222            column,
2223            if_exists,
2224            cascade,
2225        } => {
2226            f.write_str("DROP COLUMN ")?;
2227            if *if_exists {
2228                f.write_str("IF EXISTS ")?;
2229            }
2230            write!(f, "{}", quote_ident(column))?;
2231            if *cascade {
2232                f.write_str(" CASCADE")?;
2233            }
2234            Ok(())
2235        }
2236        AlterTableTarget::AddTableConstraint(tc) => {
2237            write!(f, "ADD {tc}")
2238        }
2239        AlterTableTarget::RenameColumn { old, new } => {
2240            write!(
2241                f,
2242                "RENAME COLUMN {} TO {}",
2243                quote_ident(old),
2244                quote_ident(new)
2245            )
2246        }
2247        AlterTableTarget::RenameTable { new } => {
2248            write!(f, "RENAME TO {}", quote_ident(new))
2249        }
2250        AlterTableTarget::SetTriggerEnabled { which, enabled } => {
2251            f.write_str(if *enabled {
2252                "ENABLE TRIGGER "
2253            } else {
2254                "DISABLE TRIGGER "
2255            })?;
2256            match which {
2257                TriggerSelector::All => f.write_str("ALL"),
2258                TriggerSelector::Named(n) => f.write_str(&quote_ident(n)),
2259            }
2260        }
2261    }
2262}
2263
2264impl fmt::Display for TableConstraint {
2265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2266        match self {
2267            Self::PrimaryKey { name, columns } => {
2268                if let Some(n) = name {
2269                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
2270                }
2271                f.write_str("PRIMARY KEY (")?;
2272                for (i, c) in columns.iter().enumerate() {
2273                    if i > 0 {
2274                        f.write_str(", ")?;
2275                    }
2276                    f.write_str(&quote_ident(c))?;
2277                }
2278                f.write_str(")")
2279            }
2280            Self::Unique {
2281                name,
2282                columns,
2283                nulls_not_distinct,
2284            } => {
2285                if let Some(n) = name {
2286                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
2287                }
2288                f.write_str("UNIQUE ")?;
2289                if *nulls_not_distinct {
2290                    f.write_str("NULLS NOT DISTINCT ")?;
2291                }
2292                f.write_str("(")?;
2293                for (i, c) in columns.iter().enumerate() {
2294                    if i > 0 {
2295                        f.write_str(", ")?;
2296                    }
2297                    f.write_str(&quote_ident(c))?;
2298                }
2299                f.write_str(")")
2300            }
2301            Self::Check { name, expr } => {
2302                if let Some(n) = name {
2303                    write!(f, "CONSTRAINT {} ", quote_ident(n))?;
2304                }
2305                write!(f, "CHECK ({expr})")
2306            }
2307            Self::Index { name, columns } => {
2308                f.write_str("KEY ")?;
2309                if let Some(n) = name {
2310                    write!(f, "{} ", quote_ident(n))?;
2311                }
2312                f.write_str("(")?;
2313                for (i, c) in columns.iter().enumerate() {
2314                    if i > 0 {
2315                        f.write_str(", ")?;
2316                    }
2317                    f.write_str(&quote_ident(c))?;
2318                }
2319                f.write_str(")")
2320            }
2321        }
2322    }
2323}
2324
2325impl fmt::Display for ForeignKeyConstraint {
2326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2327        if let Some(name) = &self.name {
2328            write!(f, "CONSTRAINT {} ", quote_ident(name))?;
2329        }
2330        f.write_str("FOREIGN KEY (")?;
2331        for (i, c) in self.columns.iter().enumerate() {
2332            if i > 0 {
2333                f.write_str(", ")?;
2334            }
2335            f.write_str(&quote_ident(c))?;
2336        }
2337        write!(f, ") REFERENCES {}", quote_ident(&self.parent_table))?;
2338        if !self.parent_columns.is_empty() {
2339            f.write_str(" (")?;
2340            for (i, c) in self.parent_columns.iter().enumerate() {
2341                if i > 0 {
2342                    f.write_str(", ")?;
2343                }
2344                f.write_str(&quote_ident(c))?;
2345            }
2346            f.write_str(")")?;
2347        }
2348        // Only render non-default actions to keep Display output
2349        // close to user input. SPG's default is RESTRICT (matches
2350        // SQL spec).
2351        if self.on_delete != FkAction::Restrict {
2352            write!(f, " ON DELETE {}", self.on_delete)?;
2353        }
2354        if self.on_update != FkAction::Restrict {
2355            write!(f, " ON UPDATE {}", self.on_update)?;
2356        }
2357        Ok(())
2358    }
2359}
2360
2361impl fmt::Display for FkAction {
2362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2363        match self {
2364            Self::Restrict => f.write_str("RESTRICT"),
2365            Self::Cascade => f.write_str("CASCADE"),
2366            Self::SetNull => f.write_str("SET NULL"),
2367            Self::SetDefault => f.write_str("SET DEFAULT"),
2368            Self::NoAction => f.write_str("NO ACTION"),
2369        }
2370    }
2371}
2372
2373impl fmt::Display for ColumnDef {
2374    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2375        write!(f, "{} {}", quote_ident(&self.name), self.ty)?;
2376        if let Some(d) = &self.default {
2377            write!(f, " DEFAULT {d}")?;
2378        }
2379        if self.auto_increment {
2380            f.write_str(" AUTO_INCREMENT")?;
2381        }
2382        if !self.nullable {
2383            f.write_str(" NOT NULL")?;
2384        }
2385        Ok(())
2386    }
2387}
2388
2389impl fmt::Display for InsertStatement {
2390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2391        write!(f, "INSERT INTO {}", quote_ident(&self.table))?;
2392        if let Some(cols) = &self.columns {
2393            f.write_str(" (")?;
2394            for (i, c) in cols.iter().enumerate() {
2395                if i > 0 {
2396                    f.write_str(", ")?;
2397                }
2398                f.write_str(&quote_ident(c))?;
2399            }
2400            f.write_str(")")?;
2401        }
2402        // v7.13.0 — INSERT…SELECT renders as `... SELECT …`,
2403        // skipping the VALUES list (mailrs round-5 G4).
2404        if let Some(sel) = &self.select_source {
2405            write!(f, " {sel}")?;
2406        } else {
2407            f.write_str(" VALUES ")?;
2408            for (ri, row) in self.rows.iter().enumerate() {
2409                if ri > 0 {
2410                    f.write_str(", ")?;
2411                }
2412                f.write_str("(")?;
2413                for (i, v) in row.iter().enumerate() {
2414                    if i > 0 {
2415                        f.write_str(", ")?;
2416                    }
2417                    write!(f, "{v}")?;
2418                }
2419                f.write_str(")")?;
2420            }
2421        }
2422        Ok(())
2423    }
2424}
2425
2426impl fmt::Display for UpdateStatement {
2427    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2428        write!(f, "UPDATE {} SET ", quote_ident(&self.table))?;
2429        for (i, (col, expr)) in self.assignments.iter().enumerate() {
2430            if i > 0 {
2431                f.write_str(", ")?;
2432            }
2433            write!(f, "{} = {expr}", quote_ident(col))?;
2434        }
2435        if let Some(w) = &self.where_ {
2436            write!(f, " WHERE {w}")?;
2437        }
2438        Ok(())
2439    }
2440}
2441
2442impl fmt::Display for DeleteStatement {
2443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2444        write!(f, "DELETE FROM {}", quote_ident(&self.table))?;
2445        if let Some(w) = &self.where_ {
2446            write!(f, " WHERE {w}")?;
2447        }
2448        Ok(())
2449    }
2450}
2451
2452impl fmt::Display for SelectStatement {
2453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2454        write_bare_select(self, f)?;
2455        for (kind, peer) in &self.unions {
2456            f.write_str(match kind {
2457                UnionKind::Distinct => " UNION ",
2458                UnionKind::All => " UNION ALL ",
2459            })?;
2460            write_bare_select(peer, f)?;
2461        }
2462        if !self.order_by.is_empty() {
2463            f.write_str(" ORDER BY ")?;
2464            for (i, o) in self.order_by.iter().enumerate() {
2465                if i > 0 {
2466                    f.write_str(", ")?;
2467                }
2468                write!(f, "{}", o.expr)?;
2469                if o.desc {
2470                    f.write_str(" DESC")?;
2471                }
2472            }
2473        }
2474        if let Some(n) = &self.limit {
2475            write!(f, " LIMIT {n}")?;
2476        }
2477        if let Some(o) = &self.offset {
2478            write!(f, " OFFSET {o}")?;
2479        }
2480        Ok(())
2481    }
2482}
2483
2484fn write_bare_select(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2485    f.write_str("SELECT ")?;
2486    if s.distinct {
2487        f.write_str("DISTINCT ")?;
2488    }
2489    write_bare_select_body(s, f)
2490}
2491
2492fn write_bare_select_body(s: &SelectStatement, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2493    for (i, item) in s.items.iter().enumerate() {
2494        if i > 0 {
2495            f.write_str(", ")?;
2496        }
2497        write!(f, "{item}")?;
2498    }
2499    if let Some(t) = &s.from {
2500        write!(f, " FROM {t}")?;
2501    }
2502    if let Some(e) = &s.where_ {
2503        write!(f, " WHERE {e}")?;
2504    }
2505    if let Some(gs) = &s.group_by {
2506        f.write_str(" GROUP BY ")?;
2507        for (i, g) in gs.iter().enumerate() {
2508            if i > 0 {
2509                f.write_str(", ")?;
2510            }
2511            write!(f, "{g}")?;
2512        }
2513    }
2514    if let Some(h) = &s.having {
2515        write!(f, " HAVING {h}")?;
2516    }
2517    Ok(())
2518}
2519
2520impl fmt::Display for SelectItem {
2521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2522        match self {
2523            Self::Wildcard => f.write_str("*"),
2524            Self::Expr { expr, alias } => {
2525                write!(f, "{expr}")?;
2526                if let Some(a) = alias {
2527                    write!(f, " AS {}", quote_ident(a))?;
2528                }
2529                Ok(())
2530            }
2531        }
2532    }
2533}
2534
2535impl fmt::Display for FromClause {
2536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2537        write!(f, "{}", self.primary)?;
2538        for j in &self.joins {
2539            match j.kind {
2540                JoinKind::Inner => write!(f, " INNER JOIN {}", j.table)?,
2541                JoinKind::Left => write!(f, " LEFT JOIN {}", j.table)?,
2542                JoinKind::Cross => write!(f, " CROSS JOIN {}", j.table)?,
2543            }
2544            if let Some(on) = &j.on {
2545                write!(f, " ON {on}")?;
2546            }
2547        }
2548        Ok(())
2549    }
2550}
2551
2552impl fmt::Display for TableRef {
2553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2554        write!(f, "{}", quote_ident(&self.name))?;
2555        if let Some(a) = &self.alias {
2556            write!(f, " AS {}", quote_ident(a))?;
2557        }
2558        Ok(())
2559    }
2560}
2561
2562impl fmt::Display for ColumnName {
2563    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2564        if let Some(q) = &self.qualifier {
2565            write!(f, "{}.{}", quote_ident(q), quote_ident(&self.name))
2566        } else {
2567            write!(f, "{}", quote_ident(&self.name))
2568        }
2569    }
2570}
2571
2572impl fmt::Display for Expr {
2573    #[allow(clippy::too_many_lines)]
2574    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2575        match self {
2576            Self::Literal(l) => write!(f, "{l}"),
2577            Self::Column(c) => write!(f, "{c}"),
2578            Self::Placeholder(n) => write!(f, "${n}"),
2579            Self::Binary { lhs, op, rhs } => write!(f, "({lhs} {op} {rhs})"),
2580            Self::Unary { op, expr } => match op {
2581                UnOp::Not => write!(f, "(NOT {expr})"),
2582                UnOp::Neg => write!(f, "(-{expr})"),
2583            },
2584            Self::Cast { expr, target } => write!(f, "({expr}::{target})"),
2585            Self::IsNull { expr, negated } => {
2586                if *negated {
2587                    write!(f, "({expr} IS NOT NULL)")
2588                } else {
2589                    write!(f, "({expr} IS NULL)")
2590                }
2591            }
2592            Self::FunctionCall { name, args } => {
2593                write!(f, "{name}(")?;
2594                for (i, a) in args.iter().enumerate() {
2595                    if i > 0 {
2596                        f.write_str(", ")?;
2597                    }
2598                    write!(f, "{a}")?;
2599                }
2600                f.write_str(")")
2601            }
2602            Self::Like {
2603                expr,
2604                pattern,
2605                negated,
2606            } => {
2607                if *negated {
2608                    write!(f, "({expr} NOT LIKE {pattern})")
2609                } else {
2610                    write!(f, "({expr} LIKE {pattern})")
2611                }
2612            }
2613            Self::Extract { field, source } => write!(f, "EXTRACT({field} FROM {source})"),
2614            Self::WindowFunction {
2615                name,
2616                args,
2617                partition_by,
2618                order_by,
2619                frame,
2620                null_treatment: _,
2621            } => {
2622                write!(f, "{name}(")?;
2623                for (i, a) in args.iter().enumerate() {
2624                    if i > 0 {
2625                        f.write_str(", ")?;
2626                    }
2627                    write!(f, "{a}")?;
2628                }
2629                f.write_str(") OVER (")?;
2630                if !partition_by.is_empty() {
2631                    f.write_str("PARTITION BY ")?;
2632                    for (i, p) in partition_by.iter().enumerate() {
2633                        if i > 0 {
2634                            f.write_str(", ")?;
2635                        }
2636                        write!(f, "{p}")?;
2637                    }
2638                }
2639                if !order_by.is_empty() {
2640                    if !partition_by.is_empty() {
2641                        f.write_str(" ")?;
2642                    }
2643                    f.write_str("ORDER BY ")?;
2644                    for (i, (e, desc)) in order_by.iter().enumerate() {
2645                        if i > 0 {
2646                            f.write_str(", ")?;
2647                        }
2648                        write!(f, "{e}")?;
2649                        if *desc {
2650                            f.write_str(" DESC")?;
2651                        }
2652                    }
2653                }
2654                if let Some(fr) = frame {
2655                    if !partition_by.is_empty() || !order_by.is_empty() {
2656                        f.write_str(" ")?;
2657                    }
2658                    let k = match fr.kind {
2659                        FrameKind::Rows => "ROWS",
2660                        FrameKind::Range => "RANGE",
2661                    };
2662                    if let Some(end) = &fr.end {
2663                        write!(f, "{k} BETWEEN {} AND {}", fr.start, end)?;
2664                    } else {
2665                        write!(f, "{k} {}", fr.start)?;
2666                    }
2667                }
2668                f.write_str(")")
2669            }
2670            Self::ScalarSubquery(s) => write!(f, "({s})"),
2671            Self::Exists { subquery, negated } => {
2672                if *negated {
2673                    write!(f, "NOT EXISTS ({subquery})")
2674                } else {
2675                    write!(f, "EXISTS ({subquery})")
2676                }
2677            }
2678            Self::InSubquery {
2679                expr,
2680                subquery,
2681                negated,
2682            } => {
2683                if *negated {
2684                    write!(f, "({expr} NOT IN ({subquery}))")
2685                } else {
2686                    write!(f, "({expr} IN ({subquery}))")
2687                }
2688            }
2689            Self::Array(items) => {
2690                f.write_str("ARRAY[")?;
2691                for (i, e) in items.iter().enumerate() {
2692                    if i > 0 {
2693                        f.write_str(", ")?;
2694                    }
2695                    write!(f, "{e}")?;
2696                }
2697                f.write_str("]")
2698            }
2699            Self::ArraySubscript { target, index } => write!(f, "({target}[{index}])"),
2700            Self::AnyAll {
2701                expr,
2702                op,
2703                array,
2704                is_any,
2705            } => {
2706                let kw = if *is_any { "ANY" } else { "ALL" };
2707                write!(f, "({expr} {op} {kw}({array}))")
2708            }
2709            Self::Case {
2710                operand,
2711                branches,
2712                else_branch,
2713            } => {
2714                f.write_str("CASE")?;
2715                if let Some(op) = operand {
2716                    write!(f, " {op}")?;
2717                }
2718                for (w, t) in branches {
2719                    write!(f, " WHEN {w} THEN {t}")?;
2720                }
2721                if let Some(e) = else_branch {
2722                    write!(f, " ELSE {e}")?;
2723                }
2724                f.write_str(" END")
2725            }
2726        }
2727    }
2728}
2729
2730impl fmt::Display for Literal {
2731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2732        match self {
2733            Self::Integer(n) => write!(f, "{n}"),
2734            Self::Float(x) => {
2735                let s = format!("{x}");
2736                // Default Display for an integral f64 (e.g. 1.0) emits "1",
2737                // which would round-trip back to Integer. Force a dot.
2738                if s.contains('.') || s.contains('e') || s.contains('E') {
2739                    f.write_str(&s)
2740                } else {
2741                    write!(f, "{s}.0")
2742                }
2743            }
2744            Self::String(s) => {
2745                f.write_str("'")?;
2746                for c in s.chars() {
2747                    if c == '\'' {
2748                        f.write_str("''")?;
2749                    } else {
2750                        write!(f, "{c}")?;
2751                    }
2752                }
2753                f.write_str("'")
2754            }
2755            Self::Bool(b) => f.write_str(if *b { "TRUE" } else { "FALSE" }),
2756            Self::Null => f.write_str("NULL"),
2757            Self::Vector(v) => {
2758                f.write_str("[")?;
2759                for (i, x) in v.iter().enumerate() {
2760                    if i > 0 {
2761                        f.write_str(", ")?;
2762                    }
2763                    let s = format!("{x}");
2764                    // Mirror Float Display: force a dot so re-parse stays
2765                    // numerically literal.
2766                    if s.contains('.') || s.contains('e') || s.contains('E') {
2767                        f.write_str(&s)?;
2768                    } else {
2769                        write!(f, "{s}.0")?;
2770                    }
2771                }
2772                f.write_str("]")
2773            }
2774            Self::Interval { text, .. } => {
2775                f.write_str("INTERVAL '")?;
2776                for c in text.chars() {
2777                    if c == '\'' {
2778                        f.write_str("''")?;
2779                    } else {
2780                        write!(f, "{c}")?;
2781                    }
2782                }
2783                f.write_str("'")
2784            }
2785        }
2786    }
2787}
2788
2789impl fmt::Display for BinOp {
2790    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2791        f.write_str(match self {
2792            Self::Or => "OR",
2793            Self::And => "AND",
2794            Self::Eq => "=",
2795            Self::NotEq => "<>",
2796            Self::IsDistinctFrom => "IS DISTINCT FROM",
2797            Self::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
2798            Self::Lt => "<",
2799            Self::LtEq => "<=",
2800            Self::Gt => ">",
2801            Self::GtEq => ">=",
2802            Self::Add => "+",
2803            Self::Sub => "-",
2804            Self::Mul => "*",
2805            Self::Div => "/",
2806            Self::L2Distance => "<->",
2807            Self::InnerProduct => "<#>",
2808            Self::CosineDistance => "<=>",
2809            Self::Concat => "||",
2810            Self::JsonGet => "->",
2811            Self::JsonGetText => "->>",
2812            Self::JsonGetPath => "#>",
2813            Self::JsonGetPathText => "#>>",
2814            Self::JsonContains => "@>",
2815            Self::TsMatch => "@@",
2816        })
2817    }
2818}
2819
2820/// Quote `s` as a PG double-quoted identifier when required (keyword,
2821/// non-folded case, leading digit, embedded non-`[A-Za-z0-9_]`, empty).
2822/// Otherwise return it as-is. Returns an owned `String` to keep the call site
2823/// uniform.
2824fn quote_ident(s: &str) -> String {
2825    let needs_quote = match s.chars().next() {
2826        None => true,
2827        Some(c) if !c.is_ascii_alphabetic() && c != '_' => true,
2828        _ => {
2829            s.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_'))
2830                || s.chars().any(|c| c.is_ascii_uppercase())
2831                || is_keyword(s)
2832        }
2833    };
2834    if !needs_quote {
2835        return s.to_string();
2836    }
2837    let mut out = String::with_capacity(s.len() + 2);
2838    out.push('"');
2839    for c in s.chars() {
2840        if c == '"' {
2841            out.push_str("\"\"");
2842        } else {
2843            out.push(c);
2844        }
2845    }
2846    out.push('"');
2847    out
2848}
2849
2850fn is_keyword(s: &str) -> bool {
2851    matches!(
2852        &*s.to_ascii_lowercase(),
2853        "select"
2854            | "from"
2855            | "where"
2856            | "as"
2857            | "null"
2858            | "true"
2859            | "false"
2860            | "and"
2861            | "or"
2862            | "not"
2863            | "create"
2864            | "table"
2865            | "insert"
2866            | "into"
2867            | "values"
2868            | "index"
2869            | "on"
2870            | "begin"
2871            | "commit"
2872            | "rollback"
2873            | "is"
2874            | "between"
2875            | "in"
2876            | "like"
2877            | "group"
2878            | "distinct"
2879            | "union"
2880            | "all"
2881            | "join"
2882            | "inner"
2883            | "left"
2884            | "cross"
2885            | "outer"
2886            | "default"
2887            | "savepoint"
2888            | "release"
2889            | "to"
2890            | "having"
2891            | "show"
2892            | "extract"
2893            | "offset"
2894            | "asc"
2895            | "desc"
2896            | "interval"
2897    )
2898}
2899
2900#[cfg(test)]
2901mod tests {
2902    use super::*;
2903    use alloc::vec;
2904
2905    #[test]
2906    fn integer_literal_renders_without_dot() {
2907        assert_eq!(Literal::Integer(42).to_string(), "42");
2908    }
2909
2910    #[test]
2911    fn integral_float_keeps_dot() {
2912        assert_eq!(Literal::Float(1.0).to_string(), "1.0");
2913        assert_eq!(Literal::Float(1.5).to_string(), "1.5");
2914        assert_eq!(Literal::Float(2.5e-3).to_string(), "0.0025");
2915    }
2916
2917    #[test]
2918    fn string_literal_doubles_quote() {
2919        assert_eq!(Literal::String("it's".into()).to_string(), "'it''s'");
2920    }
2921
2922    #[test]
2923    fn bool_and_null_render_uppercase() {
2924        assert_eq!(Literal::Bool(true).to_string(), "TRUE");
2925        assert_eq!(Literal::Bool(false).to_string(), "FALSE");
2926        assert_eq!(Literal::Null.to_string(), "NULL");
2927    }
2928
2929    #[test]
2930    fn binary_op_always_parenthesised() {
2931        let e = Expr::Binary {
2932            lhs: Box::new(Expr::Literal(Literal::Integer(1))),
2933            op: BinOp::Add,
2934            rhs: Box::new(Expr::Literal(Literal::Integer(2))),
2935        };
2936        assert_eq!(e.to_string(), "(1 + 2)");
2937    }
2938
2939    #[test]
2940    fn select_star_from_table() {
2941        let s = SelectStatement {
2942            items: vec![SelectItem::Wildcard],
2943            from: Some(FromClause {
2944                primary: TableRef {
2945                    name: "users".into(),
2946                    alias: None,
2947                    as_of_segment: None,
2948                    unnest_expr: None,
2949                    unnest_column_aliases: Vec::new(),
2950                },
2951                joins: vec![],
2952            }),
2953            where_: None,
2954            group_by: None,
2955            group_by_all: false,
2956            having: None,
2957            unions: vec![],
2958            order_by: Vec::new(),
2959            limit: None,
2960            offset: None,
2961            distinct: false,
2962            ctes: vec![],
2963        };
2964        assert_eq!(s.to_string(), "SELECT * FROM users");
2965    }
2966
2967    #[test]
2968    fn quote_ident_for_uppercase_and_keyword() {
2969        assert_eq!(quote_ident("foo"), "foo");
2970        assert_eq!(quote_ident("Foo"), "\"Foo\"");
2971        assert_eq!(quote_ident("select"), "\"select\"");
2972        assert_eq!(quote_ident(""), "\"\"");
2973        assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
2974    }
2975}