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("e_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("e_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("e_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("e_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("e_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("e_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("e_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("e_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}