spg_engine/lib.rs
1//! SPG execution engine — v0.3 wires the SQL front-end to the in-memory
2//! storage layer. Implements `CREATE TABLE`, single-row `INSERT VALUES`, and
3//! `SELECT * FROM <table>` (no WHERE yet — that lands in v0.4 alongside
4//! expression evaluation against rows).
5#![no_std]
6
7extern crate alloc;
8
9// v7.37.9 T3 — `bump_counter!(C)` / `bump_counter!(C, N)` macros for the
10// Step VM + aggregate hot-path diagnostic counters. Gated on the
11// `perf-counters` feature so release builds pay zero cost; the
12// `xtests/dogfood_replay/spg-counter-dump` binary turns the feature on
13// to attribute Class A / B / C cascade cost.
14#[cfg(not(feature = "perf-counters"))]
15#[macro_export]
16macro_rules! bump_counter {
17 ($c:path) => {{
18 let _ = &$c;
19 }};
20 ($c:path, $n:expr) => {{
21 let _ = &$c;
22 let _ = &$n;
23 }};
24}
25
26#[cfg(feature = "perf-counters")]
27#[macro_export]
28macro_rules! bump_counter {
29 ($c:path) => {{
30 $c.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
31 }};
32 ($c:path, $n:expr) => {{
33 $c.fetch_add($n, core::sync::atomic::Ordering::Relaxed);
34 }};
35}
36
37mod acl;
38pub mod aggregate;
39pub(crate) mod amcheck;
40mod bytebudget;
41mod cancel;
42mod clock;
43mod collate;
44mod collate_derive;
45mod constfold;
46mod constraints;
47mod conversions;
48pub mod copy;
49mod cursor;
50mod ddl;
51pub mod describe;
52mod distinct;
53mod dml;
54mod envelope;
55pub mod eval;
56mod execute;
57mod explain;
58mod expr_analysis;
59pub(crate) mod extsort;
60pub mod fts;
61mod guc_catalog;
62mod immutable_fn;
63mod index_access;
64mod join;
65mod join_using;
66mod joinfold;
67pub mod json;
68pub mod largeobject;
69mod limit_expr;
70pub mod locks;
71mod maintenance;
72pub mod memoize;
73mod notify;
74mod numeric;
75mod opclass;
76mod orderby;
77mod partition;
78pub(crate) mod partition_walks;
79pub mod plan_cache;
80mod plpgsql;
81pub mod publications;
82pub mod query_stats;
83mod readonly;
84pub mod reorder;
85mod rls;
86mod rules;
87pub mod scalarsq_streaming;
88mod select;
89pub mod selectivity;
90mod sequence;
91mod session;
92mod show;
93mod spg_admin;
94pub mod statistics;
95pub mod subquery;
96pub mod subscriptions;
97mod substitute;
98mod system_catalog;
99mod table_access;
100pub mod tempstore;
101pub mod testkit;
102mod transaction;
103pub(crate) use transaction::{TxStmtClass, classify_stmt_for_tx};
104pub mod triggers;
105pub mod users;
106mod window;
107
108pub use crate::users::{Role, ScramSecrets, UserError, UserStore};
109pub use cancel::{CancelToken, MonotonicNowFn};
110pub use execute::{RowCells, StreamItem};
111
112use bytebudget::*;
113pub(crate) use clock::{rewrite_clock_calls, value_to_literal};
114use constraints::*;
115pub use constraints::{UNIQ_FOLD_CHOSEN, UNIQ_PROBE_CALLS, UNIQ_PROBE_LOCATORS};
116use conversions::*;
117pub use conversions::{
118 format_bigint_2d_text_pub, format_bit_string, format_circle, format_hstore_text, format_inet,
119 format_int_2d_text_pub, format_line, format_lseg, format_macaddr, format_macaddr8,
120 format_multirange, format_path, format_pg_box, format_pg_lsn, format_point, format_polygon,
121 format_range_text, format_text_2d_text_pub,
122};
123pub(crate) use ddl::{
124 canonicalize_set_value, enforce_enum_label, eval_runtime_default_free,
125 resolve_column_default_free,
126};
127pub(crate) use envelope::{EnvelopeParse, build_envelope, split_envelope};
128use expr_analysis::*;
129use index_access::*;
130pub use join::{ANTI_JOIN_FAST_PATH_FIRED, ANTI_JOIN_FAST_PATH_TRIED};
131pub(crate) use orderby::{
132 OrderKey, apply_offset_and_limit, apply_offset_and_limit_tagged, build_order_keys,
133 canonical_value_repr, cmp_multi_key, expand_group_by_all, order_by_value_cmp,
134 order_by_value_cmp_in, render_histogram_bounds, resolve_order_by_position, sort_by_keys,
135 sort_values_for_histogram, topk_trim, value_cmp, value_to_f64,
136};
137pub use select::{DISTINCT_DUP_DROPPED, PROJ_DIRECT_FIRE, PROJ_ROW_BUILT, SCAN_PATH_ENTERED};
138pub(crate) use select::{build_projection, infer_column_types, value_to_order_key};
139pub use sequence::MUTATING_CALL_NEEDLES;
140pub(crate) use show::render_create_table;
141pub use subquery::{
142 BATCHED_SCALAR_FALL_THROUGH_COUNT, BATCHED_SCALAR_KEYED_FIRE_COUNT,
143 BATCHED_SCALAR_KEYED_PROBE_COUNT, EXISTS_BATCH_FALL_THROUGH_COUNT, EXISTS_BATCH_FIRE_COUNT,
144 EXISTS_PULLUP_BAIL_INNER_FROM, EXISTS_PULLUP_BAIL_INNER_SHAPE,
145 EXISTS_PULLUP_BAIL_MULTICOL_DISABLED, EXISTS_PULLUP_BAIL_NO_CORR, EXISTS_PULLUP_BAIL_NO_WHERE,
146 EXISTS_PULLUP_BAIL_RESIDUAL_NOT_INNER, EXISTS_PULLUP_BAIL_UNIQUE_KEY_MISSING,
147 EXISTS_PULLUP_CANDIDATE_COUNT, EXISTS_PULLUP_FIRE_COUNT, EXISTS_PULLUP_MULTICOL_DISABLE,
148 PULLUP_LIMIT1_FIRE_COUNT, SCALARSQ_PK_PROBE_FIRED, ScalarPkProbeFastPath,
149 expr_tree_has_subquery,
150};
151pub(crate) use subquery::{build_in_list_set, collect_scalar_subqueries, expr_has_subquery};
152pub use substitute::substitute_placeholders;
153use substitute::*;
154use system_catalog::*;
155use window::*;
156
157use alloc::collections::{BTreeMap, BTreeSet};
158use alloc::string::String;
159use alloc::vec::Vec;
160use core::fmt;
161
162// v7.16.0 — re-export the parsed-statement AST so downstream
163// crates (spg-embedded → spg-sqlx) don't need a direct dep on
164// spg-sql for the prepare/bind handle.
165pub use spg_sql::ast::{SelectStatement, Statement as ParsedStatement};
166// v7.37.15 Phase B — re-export the visibility primitives for engine
167// callers so the per-row MVCC types live behind one stable name
168// (`spg_engine::Snapshot` / `spg_engine::RowHeader`) instead of every
169// caller threading through `spg_storage::snapshot::*` directly.
170pub use spg_storage::RowChange;
171pub use spg_storage::row_header::{RowHeader, XMAX_ALIVE, XMIN_FROZEN};
172pub use spg_storage::snapshot::{
173 AllCommitted, InProgressSet, Snapshot, XactStatus, XactStatusOracle,
174};
175
176/// v7.37.15 (Phase C.2) — the engine is its own visibility oracle.
177/// Scans hold `&Engine` while reading, so a scan site can pass `self`
178/// as the [`XactStatusOracle`] alongside its [`Snapshot`] when the
179/// visibility gate migrates from `visible` to `visible_with_status`
180/// (next Phase C step). Delegates to [`Engine::xact_status`].
181impl XactStatusOracle for Engine {
182 fn status(&self, version: u64) -> XactStatus {
183 self.xact_status(version)
184 }
185}
186// v7.37.14 (A2.5-stub) — re-export the silent-FOR-UPDATE telemetry
187// helper through the engine surface so downstream wrappers
188// (spg-embedded / spg-embedded-tokio / spgctl) and their tests
189// don't need a direct `spg-sql` dep.
190use spg_sql::parser::ParseError;
191pub use spg_sql::silent_for_update_count;
192use spg_storage::{Catalog, ColumnSchema, Row, StorageError};
193
194use crate::eval::EvalError;
195
196/// Result of executing one statement.
197#[derive(Debug, Clone, PartialEq)]
198#[non_exhaustive]
199pub enum QueryResult {
200 /// DDL or DML succeeded.
201 ///
202 /// `affected` is the row count for `INSERT` and 0 elsewhere.
203 /// `modified_catalog` tells the server whether this statement
204 /// caused the *committed* catalog to change — it's the signal to
205 /// snapshot/audit. False for `BEGIN`/`ROLLBACK`, false for writeful
206 /// statements executed inside a transaction (those only touch the
207 /// shadow), and true for `COMMIT` and for writes outside a TX.
208 CommandOk {
209 affected: usize,
210 modified_catalog: bool,
211 },
212 /// `SELECT` returned a (possibly empty) row set.
213 Rows {
214 columns: Vec<ColumnSchema>,
215 rows: Vec<Row<'static>>,
216 },
217}
218
219/// All errors the engine can return.
220///
221/// Marked `#[non_exhaustive]` from v7.5.0 onward: external `match`
222/// must include a `_` arm so new variants in subsequent v7.x releases
223/// are not breaking changes.
224#[derive(Debug, Clone, PartialEq)]
225#[non_exhaustive]
226pub enum EngineError {
227 Parse(ParseError),
228 Storage(StorageError),
229 Eval(EvalError),
230 /// Front-end accepted a construct that the v0.x executor doesn't support.
231 Unsupported(String),
232 /// `BEGIN` while another transaction is already open.
233 TransactionAlreadyOpen,
234 /// `COMMIT` / `ROLLBACK` with no active transaction.
235 NoActiveTransaction,
236 /// v7.38 (read01 P3.26) — a statement other than COMMIT / ROLLBACK /
237 /// ROLLBACK TO SAVEPOINT was issued after an earlier statement in the
238 /// same transaction failed. PG aborts the whole transaction on the
239 /// first error and rejects everything until it is ended (SQLSTATE
240 /// 25P02); this mirrors that so partial work can't slip through.
241 InFailedTransaction,
242 /// v7.39 (round 299, E3 Phase 2) — a row lock is held by another
243 /// transaction and the policy is `Wait`.
244 ///
245 /// Its own variant, not an `Unsupported` string: the SERVER has to
246 /// recognise it to retry, and it cannot block inside the engine
247 /// write lock — doing so would stop the whole server, including the
248 /// transaction whose commit would release the lock.
249 LockWouldBlock,
250 /// v7.39 (round 299) — granting the wait would close a wait-for
251 /// cycle. PG's 40P01.
252 LockDeadlock,
253 /// v7.38 (read01 P4.02) — a scalar / row subquery used as an
254 /// expression returned more than one row. PG raises this as
255 /// SQLSTATE 21000 (CARDINALITY_VIOLATION) with a fixed message.
256 CardinalityViolation,
257 /// v7.37.17 (Phase E3) — a REPEATABLE READ / SERIALIZABLE commit
258 /// found a write-write conflict with a concurrently-committed
259 /// transaction (a row this tx wrote was deleted/updated by another
260 /// committed writer, or a unique key this tx inserted was taken).
261 /// PG raises SQLSTATE 40001; the client retries the transaction.
262 /// The failing COMMIT rolls the transaction back, like PG.
263 SerializationFailure(String),
264 /// v4.0 sentinel: `execute_readonly` got a statement that
265 /// mutates engine state (INSERT / CREATE / BEGIN / COMMIT / …).
266 /// The caller should retake the write lock and dispatch through
267 /// `execute(&mut self)` instead.
268 WriteRequired,
269 /// v4.2: a SELECT would have returned more rows than the
270 /// configured `max_query_rows` cap. Carries the cap.
271 RowLimitExceeded(usize),
272 /// v7.30.3 (mailrs round-26): a SELECT's join/filter
273 /// materialisation would have held more (approximate) heap
274 /// bytes than the configured `max_query_bytes` cap. The row
275 /// cap above counts rows; this counts bytes, because one row
276 /// can be a multi-MB mail body — 1000 fat rows pressure the
277 /// host long before any row ceiling trips. Carries the cap.
278 QueryBytesExceeded(usize),
279 /// v4.5: cooperative cancellation — the host (server's
280 /// per-query watchdog) set the cancel flag while a long-running
281 /// SELECT / UPDATE / DELETE was scanning rows. The partial work
282 /// is discarded; the caller should surface this as a timeout
283 /// to the client.
284 Cancelled,
285 /// v7.39 (round 318, V51) — MySQL `KILL <id>` naming an id no live
286 /// connection carries. MariaDB 11: `ERROR 1094 (HY000) Unknown thread
287 /// id: N`.
288 UnknownThreadId(u32),
289 /// v7.39 (round 318, V51) — MySQL `KILL <own id>`. The connection
290 /// really is killed; MariaDB reports it to the victim as
291 /// `ERROR 1927 (70100) Connection was killed` and closes.
292 ConnectionKilled,
293 /// v7.38 Epic P (panic isolation): a panic unwound out of
294 /// statement execution and was caught at the engine's
295 /// `execute_*` boundary (see `execute_in_with_cancel`). The
296 /// in-flight transaction's shadow was discarded (rollback) and
297 /// the engine left consistent; the caller sees this ordinary
298 /// error instead of a crashed process / poisoned lock. NOTE:
299 /// under the release `panic = "abort"` profile the process
300 /// aborts before any unwind, so this variant only ever surfaces
301 /// in dev/test (`panic = "unwind"`) — and in production once a
302 /// later slice flips the release profile to unwind.
303 Internal(String),
304}
305
306impl fmt::Display for EngineError {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 match self {
309 Self::Parse(e) => write!(f, "parse: {e}"),
310 Self::Storage(e) => write!(f, "storage: {e}"),
311 Self::Eval(e) => write!(f, "eval: {e}"),
312 Self::Unsupported(s) => write!(f, "unsupported: {s}"),
313 Self::TransactionAlreadyOpen => f.write_str("a transaction is already open"),
314 Self::NoActiveTransaction => f.write_str("no active transaction"),
315 Self::LockWouldBlock => f.write_str("row is locked by another transaction"),
316 Self::LockDeadlock => f.write_str("deadlock detected"),
317 Self::InFailedTransaction => f.write_str(
318 "current transaction is aborted, commands ignored until end of transaction block",
319 ),
320 Self::CardinalityViolation => {
321 f.write_str("more than one row returned by a subquery used as an expression")
322 }
323 Self::SerializationFailure(detail) => {
324 // v7.39 (round 552) — PG has TWO wordings under 40001 and
325 // they mean different things: "concurrent update" for a
326 // write-write conflict, "read/write dependencies among
327 // transactions" for the antidependency a SERIALIZABLE
328 // transaction hits. A detail that already carries PG's
329 // own sentence is passed through rather than nested
330 // inside the other one.
331 if detail.starts_with("could not serialize access") {
332 f.write_str(detail)
333 } else {
334 write!(
335 f,
336 "could not serialize access due to concurrent update: {detail}"
337 )
338 }
339 }
340 Self::WriteRequired => {
341 f.write_str("statement requires a write lock (use execute, not execute_readonly)")
342 }
343 Self::RowLimitExceeded(n) => {
344 write!(f, "query exceeded max_query_rows={n}")
345 }
346 Self::QueryBytesExceeded(n) => {
347 write!(
348 f,
349 "query materialisation exceeded max_query_bytes={n} (set SPG_MAX_QUERY_BYTES to raise, 0 to disable)"
350 )
351 }
352 Self::Cancelled => f.write_str("query cancelled (timeout or client request)"),
353 Self::UnknownThreadId(id) => write!(f, "Unknown thread id: {id}"),
354 Self::ConnectionKilled => f.write_str("Connection was killed"),
355 Self::Internal(s) => write!(f, "internal error: {s}"),
356 }
357 }
358}
359
360impl From<ParseError> for EngineError {
361 fn from(e: ParseError) -> Self {
362 Self::Parse(e)
363 }
364}
365impl From<StorageError> for EngineError {
366 fn from(e: StorageError) -> Self {
367 Self::Storage(e)
368 }
369}
370impl From<EvalError> for EngineError {
371 fn from(e: EvalError) -> Self {
372 Self::Eval(e)
373 }
374}
375
376/// The execution engine. Holds the catalog and (later) other server-scope
377/// state. `Engine::new()` is intentionally cheap so callers can construct one
378/// per database, per test.
379/// Function pointer that returns "now" as microseconds since Unix
380/// epoch. The engine is `no_std`, so it can't reach for `std::time`
381/// itself — callers (`spg-server`, the sqllogictest runner) inject a
382/// concrete implementation. `None` means `NOW()` / `CURRENT_*` raise
383/// `Unsupported`.
384pub type ClockFn = fn() -> i64;
385
386/// v7.39 (pg_stat knife A) — host-provided live connection count for
387/// `pg_stat_database.numbackends`.
388pub type BackendCountFn = fn() -> u32;
389
390/// v7.39 (read01 pgstatfuncs.c) — host-provided identity of the CALLING
391/// connection for `pg_backend_pid()` / the pg_stat_activity self-join.
392/// The host reads a connection-thread-local set at session start; the
393/// no_std engine just calls through. `None` (embedded) → pid 1.
394pub type BackendPidFn = fn() -> u32;
395
396/// v7.39 (round 476) — the WAL's current byte position, as a PG LSN.
397///
398/// `pg_current_wal_lsn()` answered the literal `0/0` forever, so every
399/// monitor watching WAL progress or replication lag saw an instance that
400/// had never written anything. SPG's WAL is a file and its length IS an
401/// LSN in every sense a monitor uses one: monotonic, byte-denominated, and
402/// comparable — `pg_wal_lsn_diff` over two samples gives real bytes.
403///
404/// `None` (embedded, or a server started without a WAL) keeps `0/0`, which
405/// is the honest answer there: nothing is being written.
406pub type WalLsnFn = fn() -> u64;
407
408/// v7.39 (round 318, V51) — host-provided connection control. `terminate`
409/// false = cancel the target's running statement (PG `pg_cancel_backend`,
410/// MySQL `KILL QUERY`); true = also close the connection (PG
411/// `pg_terminate_backend`, MySQL `KILL CONNECTION`). Returns whether a
412/// connection with that id exists — the engine has no registry of its own,
413/// so the answer has to come from the host that accepted the sockets.
414/// `None` (embedded, no connections) ⇒ nothing to signal.
415pub type BackendSignalFn = fn(pid: u32, terminate: bool) -> bool;
416
417pub use tempstore::{SpillStats, TempRun, TempRunFactory, TempStoreError};
418
419/// v7.39 (tz epic) — host-injected IANA timezone lookups (the no_std
420/// engine can't read the system zoneinfo directory; spg-tzif is the
421/// std-side implementation). All instants are MICROSECONDS.
422/// UTC offset (µs east) of a zone at a UTC instant; None = unknown zone.
423/// v7.39 (round 534) — the compiled-in default PG18 reports for a
424/// configuration parameter, for the wire's own SHOW shortcut.
425///
426/// The pgwire layer answers `SHOW <name>` from a small canned list
427/// before the statement ever reaches the engine, so it needs the same
428/// inventory the engine reads or the two disagree — which they did:
429/// `SHOW fsync` over the wire returned an empty row.
430#[must_use]
431pub fn pg_guc_boot_value(name: &str) -> Option<&'static str> {
432 crate::guc_catalog::guc_boot_value(name)
433}
434
435pub type TzOffsetFn = fn(&str, i64) -> Option<i64>;
436/// Local wall-clock µs -> UTC µs with PG's DST disambiguation.
437pub type TzLocalizeFn = fn(&str, i64) -> Option<i64>;
438/// Canonical zone spelling ("asia/tokyo" -> "Asia/Tokyo").
439pub type TzCanonFn = fn(&str) -> Option<alloc::string::String>;
440/// Zone designation ("JST", "EDT") at a UTC instant.
441pub type TzAbbrevFn = fn(&str, i64) -> Option<alloc::string::String>;
442/// v7.39 (round 502) — every zone the host knows at a UTC instant, as
443/// `(name, abbrev, utc_offset_secs, is_dst)`.
444///
445/// Backs `pg_timezone_names`. SPG resolved named zones correctly — round
446/// 502 measured DST boundaries byte-identical to PG18 — but could not
447/// LIST them, so a client populating a timezone picker got "relation
448/// pg_timezone_names does not exist". The data was there, only
449/// unlistable. No hook, or a host with no tzdata, yields an empty view
450/// rather than an error: that is what such a host honestly has.
451pub type TzAllFn =
452 fn(i64) -> alloc::vec::Vec<(alloc::string::String, alloc::string::String, i64, bool)>;
453
454/// v7.39 (tz epic) — per-statement snapshot of the session TimeZone,
455/// consumed per-VALUE by the timestamptz renderers (a DST zone's
456/// offset depends on the instant being rendered).
457#[derive(Debug, Clone)]
458pub enum SessionTz {
459 Utc,
460 /// Fixed offset, µs east.
461 Fixed(i64),
462 /// IANA zone + the host lookups.
463 Named(alloc::string::String, TzOffsetFn, TzAbbrevFn),
464}
465
466impl SessionTz {
467 #[must_use]
468 pub fn is_utc(&self) -> bool {
469 matches!(self, Self::Utc) || matches!(self, Self::Fixed(0))
470 }
471
472 /// Offset (µs east) at a UTC instant.
473 #[must_use]
474 pub fn offset_at(&self, utc_micros: i64) -> i64 {
475 match self {
476 Self::Utc => 0,
477 Self::Fixed(off) => *off,
478 Self::Named(zone, f, _) => f(zone, utc_micros).unwrap_or(0),
479 }
480 }
481
482 /// Designation for the non-ISO DateStyle suffix: a named zone's
483 /// abbreviation at the instant; None for UTC/fixed (callers spell
484 /// "UTC" / "+09" themselves).
485 #[must_use]
486 pub fn abbrev_at(&self, utc_micros: i64) -> Option<alloc::string::String> {
487 match self {
488 Self::Named(zone, _, f) => f(zone, utc_micros),
489 _ => None,
490 }
491 }
492}
493
494/// Function pointer that produces 16 cryptographically random bytes.
495/// Like `ClockFn`, the engine is `no_std` and can't reach for /dev/urandom
496/// itself — host (`spg-server`) injects an OS-backed source. `None`
497/// means SQL-driven `CREATE USER` falls back to a deterministic salt
498/// derived from the username (acceptable in tests; the server always
499/// installs a real RNG so production paths never see this).
500pub type SaltFn = fn() -> [u8; 16];
501
502/// v4.5 cooperative cancellation token. A long-running SELECT /
503/// UPDATE / DELETE checks `is_cancelled` at row-loop checkpoints
504/// and bails with `EngineError::Cancelled`. The host
505/// (`spg-server`) creates an `AtomicBool` per query, spawns a
506/// watchdog thread that sets it after `SPG_QUERY_TIMEOUT_MS`,
507/// and passes it via `execute_with_cancel` / `execute_readonly_with_cancel`.
508///
509/// `CancelToken::none()` is a no-op — used by the legacy `execute`
510/// and `execute_readonly` entry points so existing callers don't
511/// change.
512/// v4.41.1 opaque transaction handle. Returned by `Engine::alloc_tx_id`,
513/// threaded through `Engine::execute_in` so dispatch can identify which
514/// in-flight TX a statement belongs to. `IMPLICIT_TX` is the reserved
515/// slot every legacy caller — engine self-tests, spg-cli, spg-embedded,
516/// startup replay — implicitly uses through the unchanged
517/// `Engine::execute(sql)` API. v4.41.1 keeps at most one active slot at
518/// runtime (dispatch holds `engine.write()` across the wrap, same as
519/// v4.34); the map shape is here to let v4.42 turn on N in-flight
520/// implicit TXs without reshuffling the engine internals.
521#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
522pub struct TxId(pub u64);
523
524/// Reserved slot used by `Engine::execute(sql)` — the legacy single-
525/// global-shadow path. New `alloc_tx_id` handles start at 1.
526pub const IMPLICIT_TX: TxId = TxId(0);
527
528/// v6.7.3 — default segment-size threshold used by `COMPACT COLD
529/// SEGMENTS` when no explicit target is supplied. Segments whose
530/// `OwnedSegment::bytes().len()` is **strictly** less than this
531/// value are eligible to merge. spg-server reads
532/// `SPG_COMPACTION_TARGET_SEGMENT_BYTES` to override.
533pub const COMPACTION_TARGET_DEFAULT_BYTES: u64 = 4 * 1024 * 1024;
534
535/// Per-slot transaction state. Held inside `tx_catalogs[tx_id]` for the
536/// lifetime of a BEGIN..COMMIT (or BEGIN..ROLLBACK) window. Drops when
537/// the TX commits (its `catalog` is moved over `Engine.catalog`) or
538/// rolls back (slot removed, catalog discarded).
539#[derive(Debug, Default, Clone)]
540struct TxState {
541 /// The TX's shadow copy of the catalog. Started as a clone of
542 /// `Engine.catalog` at BEGIN time; writes flow into it; COMMIT
543 /// installs it over `Engine.catalog`. `Catalog::clone()` is O(1)
544 /// since v4.40 (`PersistentVec` rows + `PersistentBTreeMap` indices).
545 catalog: Catalog,
546 /// v7.37 (round 828) — the TX's shadow copy of the user store,
547 /// following exactly the catalog's model one field up: created
548 /// lazily by the first role DDL inside the TX (an ordinary TX
549 /// never pays the clone), written through for the rest of the TX,
550 /// installed over `Engine.users` at COMMIT, discarded on ROLLBACK.
551 /// PG treats roles as ordinary catalog rows — `BEGIN; CREATE ROLE
552 /// r; ROLLBACK` leaves no role — and SPG used to refuse the
553 /// statement instead, which no drop-in client expects.
554 ///
555 /// Other sessions and the auth path keep reading the committed
556 /// store, so an uncommitted role can neither log in nor be seen
557 /// elsewhere — the isolation PG gives via its catalog MVCC.
558 users: Option<crate::users::UserStore>,
559 /// Per-TX savepoint stack. Each entry pairs the savepoint name with
560 /// a clone of `catalog` (and of the role shadow, which subtransactions
561 /// roll back too) at the moment `SAVEPOINT <name>` fired.
562 /// `ROLLBACK TO <name>` restores from the entry and pops everything
563 /// after it; `RELEASE <name>` discards the entry and everything
564 /// after; COMMIT/ROLLBACK clears the whole stack.
565 savepoints: Vec<(String, Catalog, Option<crate::users::UserStore>)>,
566 /// v7.37.15 (Phase E) — cached MVCC snapshot for REPEATABLE
567 /// READ / SERIALIZABLE. Captured at `exec_begin` time when the
568 /// session's `current_isolation_level` is RR/SER; read paths
569 /// inside the TX use this snapshot rather than calling
570 /// `Engine::current_snapshot` per statement, so a row that
571 /// becomes visible mid-tx (because another writer committed)
572 /// is NOT exposed to this tx — preserving RR's invariant.
573 ///
574 /// `None` for READ COMMITTED (default): each statement gets a
575 /// fresh snapshot via `current_snapshot()`.
576 cached_snapshot: Option<spg_storage::snapshot::Snapshot>,
577 /// v7.37.17 (Phase E2 — RC rebase) — tables this tx has run DML
578 /// against. The per-statement rebase extracts/replays write-sets
579 /// only for these (see `maybe_rc_rebase`).
580 touched_tables: alloc::collections::BTreeSet<String>,
581 /// v7.39 (round 552) — tables this tx has READ. PG's SIREAD locks,
582 /// at table granularity: the coarse end of the same idea, and what
583 /// PG itself falls back to when its per-tuple lock memory runs out.
584 ///
585 /// A SERIALIZABLE tx aborts at COMMIT if any table it read was
586 /// written by a transaction that committed after its snapshot —
587 /// the read/write antidependency SI cannot see. Coarse granularity
588 /// means SPG aborts some transactions PG would let through; it
589 /// never lets through one PG would abort.
590 read_tables: alloc::collections::BTreeSet<String>,
591 /// v7.39 (round 552) — was THIS transaction opened SERIALIZABLE?
592 ///
593 /// `Engine::current_isolation_level` is one field for the whole
594 /// engine, not part of the per-session bag, so with two connections
595 /// open one transaction's COMMIT resets it under the other's feet —
596 /// the shared-engine leak rounds 279 and 283 chased through session
597 /// state and advisory locks. The level a transaction runs at has to
598 /// live on the transaction.
599 serializable: bool,
600 /// The engine's commit sequence when this tx began.
601 begin_commit_seq: u64,
602 /// v7.39 (round 494) — has anything asked for this shadow catalog
603 /// MUTABLY since BEGIN?
604 ///
605 /// COMMIT installs the shadow over the committed catalog, so a
606 /// transaction that changed nothing must install nothing — otherwise
607 /// it reverts whatever other sessions committed while it was open.
608 /// `touched_tables` cannot answer this: it records DML targets for the
609 /// rebase, and a `SELECT lo_write(…)` classifies read-only while
610 /// mutating (the large-object pins caught exactly that).
611 ///
612 /// Set in `active_catalog_mut`, the single place a `&mut Catalog` is
613 /// handed out. A caller that takes the mutable handle without writing
614 /// merely keeps the old install behaviour, so the flag errs toward
615 /// installing.
616 shadow_dirty: bool,
617 /// v7.39 (round 298) — this transaction is in the aborted state.
618 ///
619 /// Per SLOT. It used to be one flag on the shared `Engine`, guarded
620 /// by `in_transaction()` — the GLOBAL "is any transaction open"
621 /// test. So an autocommit statement that failed while a DIFFERENT
622 /// connection happened to hold a transaction set the flag, and
623 /// every other connection was then refused with 25P02. Round 283
624 /// fixed seven sites of this exact shape in the server; this one is
625 /// in the engine and was missed.
626 aborted: bool,
627 /// v7.39 (round 288) — `SET CONSTRAINTS … {DEFERRED|IMMEDIATE}`
628 /// override for this transaction. `None` = each constraint uses
629 /// its own declared timing; `Some(true)` = every DEFERRABLE one is
630 /// deferred; `Some(false)` = every one is immediate.
631 constraints_deferred: Option<bool>,
632 /// v7.39 (round 308) — per-constraint overrides from the NAMED form
633 /// of `SET CONSTRAINTS`. Consulted before `constraints_deferred`, so
634 /// `ALL DEFERRED` followed by `fk_a IMMEDIATE` leaves fk_a immediate
635 /// and everything else deferred. An `ALL` form clears this map,
636 /// which is what makes a later blanket setting win — PG resets the
637 /// whole set the same way.
638 constraints_deferred_by_name: BTreeMap<String, bool>,
639 /// v7.37.17 — the tx executed a statement whose effect on the
640 /// shadow catalog can't be expressed as a versioned row write-set
641 /// (DDL, COPY, anything unclassified). The rebase would lose it,
642 /// so the tx degrades to its frozen BEGIN-time view (SI) for the
643 /// rest of its life — the pre-E2 behaviour, honestly kept.
644 rebase_poisoned: bool,
645 /// v7.37.17 — statements successfully run inside this tx. The
646 /// first statement sees the BEGIN-time clone unchanged (it IS the
647 /// latest base at that point); rebasing starts from the second.
648 stmts_run: u32,
649 /// v7.39 (round 196) — the engine `commit_epoch` this tx last
650 /// rebased against (BEGIN seeds it). When the epoch hasn't moved,
651 /// no other path committed to the base catalog, so the
652 /// per-statement RC rebase — whose write-set extraction is a full
653 /// scan of every touched table — is skipped entirely. The r196
654 /// wire panel traced tx_batch's 2.8× LOSS to exactly that scan
655 /// running before EVERY in-tx statement (~200 µs/stmt on a
656 /// 20k-row table, 58× the statement itself). Over-incrementing
657 /// the epoch is safe (an extra rebase is only slower, never
658 /// wrong); missing an increment would be a correctness bug, so
659 /// the epoch bumps on every completed non-tx statement.
660 rebased_at_epoch: u64,
661 /// v7.37.17 (Phase E4 fix) — (old RowId → new RowId) pairs recorded
662 /// by every in-place UPDATE this tx ran, keyed by table (RowIds are
663 /// per-relation). An UPDATE's write-set is tombstone(old) +
664 /// insert(new); when a rebase skips a CONFLICTING tombstone (the
665 /// row was updated/deleted by a concurrently-committed tx), the
666 /// paired insert must be dropped too — otherwise the row
667 /// DUPLICATES (caught by the E4 isolation matrix).
668 update_pairs: alloc::collections::BTreeMap<
669 String,
670 Vec<(
671 spg_storage::row_header::RowId,
672 spg_storage::row_header::RowId,
673 )>,
674 >,
675}
676
677/// v7.11.0 — frozen read-only view of the engine's committed state.
678/// Constructed via [`Engine::clone_snapshot`]. Holds clones of the
679/// catalog, statistics, clock function, and row-cap config — the
680/// four fields the `execute_readonly` path actually reads. Cheap to
681/// `Clone` (each clone shares the underlying `PersistentVec` row
682/// storage; only the trie root pointers copy). Send + Sync so a
683/// snapshot can be moved across `tokio::task::spawn_blocking`
684/// boundaries without coordination.
685///
686/// The contract: a snapshot reflects the engine's state at the
687/// moment `clone_snapshot()` returned. Subsequent writes to the
688/// engine are NOT visible. Callers who need fresher data take a
689/// new snapshot.
690#[derive(Debug, Clone)]
691pub struct CatalogSnapshot {
692 catalog: Catalog,
693 statistics: statistics::Statistics,
694 clock: Option<ClockFn>,
695 max_query_rows: Option<usize>,
696}
697
698/// CoW-1 (v7.34) — frozen view of the *persisted* committed engine
699/// state. Carries every field the `snapshot()` envelope serializes;
700/// v7.39 (round 279) — the per-CONNECTION state, parked while another
701/// connection holds the engine.
702///
703/// The server runs ONE shared `Engine` behind a `RwLock`
704/// (`ServerState.engine`, built once at startup), so everything the
705/// engine called "session state" was in fact process-wide and leaked
706/// between clients: two connections saw each other's prepared
707/// statements, and one client's `SET sql_mode` re-dialected another's
708/// string literals. PG scopes all of this per session.
709///
710/// Rather than thread a session handle through every call site, the
711/// engine keeps the ACTIVE session's state in its own fields — so the
712/// ~40 existing `self.session_params` / `self.backslash_escapes` uses
713/// are untouched — and swaps the whole bag when the caller announces a
714/// different session. Embedded hosts never announce one and stay on
715/// session 0 forever, exactly as before.
716#[derive(Debug, Default)]
717pub(crate) struct SessionBag {
718 pub(crate) session_params: BTreeMap<String, String>,
719 pub(crate) backslash_escapes: bool,
720 /// v7.39 (round 470) — is the MySQL session in a strict `sql_mode`?
721 ///
722 /// MariaDB's default includes `STRICT_TRANS_TABLES`, so this starts
723 /// true; `SET sql_mode=''` (or any list without a STRICT_ flag) turns
724 /// it off and a value that would otherwise raise is bent to fit
725 /// instead — the same conversion `INSERT IGNORE` uses.
726 pub(crate) mysql_strict: bool,
727 pub(crate) prepared_statements: BTreeMap<String, PreparedSqlStatement>,
728 /// v7.39 (round 499) — the value `nextval` last returned IN THIS
729 /// SESSION, per sequence, and which sequence that was.
730 ///
731 /// PG defines `currval` and `lastval` as session-local: they answer
732 /// the number THIS session was given, and error with 55000 ("not yet
733 /// defined in this session") when it has not called `nextval`. They
734 /// are deliberately not the sequence's current value — another
735 /// session may have advanced it since, and reading that would hand
736 /// back a number this session never owned, which is what a caller
737 /// then uses as a foreign key.
738 ///
739 /// Measured before this (`iso_session` T1/T2): `currval` answered in
740 /// a connection that had never called `nextval`, and `lastval`
741 /// answered across connections, because the tracking lived on the
742 /// shared engine rather than in the bag.
743 pub(crate) seq_currvals: BTreeMap<String, i64>,
744 pub(crate) last_sequence_used: Option<String>,
745 /// v7.39 (round 553) — the isolation level THIS connection is
746 /// running at.
747 ///
748 /// It lived on the shared engine, so it leaked both ways between
749 /// connections. Measured over pgwire against PG18: connection B
750 /// opened a plain BEGIN and `SHOW transaction_isolation` answered
751 /// `serializable` — A's level; and A, still inside its SERIALIZABLE
752 /// block, then read `read committed`, because B's COMMIT reset the
753 /// field under it. PG answers `read committed` and `serializable`
754 /// throughout. So a transaction that asked for SERIALIZABLE ran at
755 /// READ COMMITTED and one that asked for nothing ran at
756 /// SERIALIZABLE, purely because another connection was busy.
757 ///
758 /// Round 552 saw the same field give way and worked around it by
759 /// putting the level on the TRANSACTION; this puts the session's
760 /// own copy where the rest of its state already lives — the place
761 /// r306's comment says every piece of per-connection state belongs
762 /// so it never gets a process-wide version to regress from.
763 pub(crate) isolation_level: spg_sql::ast::IsolationLevel,
764 /// v7.39 (round 306) — open large-object descriptors. Per session
765 /// from the start, deliberately: r277/r279/r283 each landed a piece
766 /// of per-connection state on the process-wide engine first and had
767 /// to be unpicked afterwards, so this one never gets a process-wide
768 /// version to regress from. PG additionally scopes descriptors to
769 /// the transaction, so the table is emptied at COMMIT / ROLLBACK.
770 pub(crate) lo_descriptors: BTreeMap<i32, LargeObjectDescriptor>,
771 /// Next descriptor number to hand out. PG starts at 0 and counts up
772 /// within a transaction, restarting once the transaction ends.
773 pub(crate) lo_next_fd: i32,
774 /// v7.39 (round 321, V54) — open server-side cursors. They lived on
775 /// the shared engine until now, i.e. in ONE namespace for every
776 /// connection: two clients could not both `DECLARE c`, a `FETCH`
777 /// could read another client's rows, and `CLOSE ALL` closed
778 /// everybody's.
779 pub(crate) cursors: BTreeMap<String, cursor::OpenCursor>,
780 /// v7.39 (round 347, M2) — MySQL's `LAST_INSERT_ID()`. Per SESSION
781 /// from the start (r277/r279/r283 each paid for landing per-connection
782 /// state on the shared engine first): one connection's insert must not
783 /// be readable as another's. MariaDB, measured: a fresh session reads
784 /// 0; an insert that generates an AUTO_INCREMENT value sets it to the
785 /// FIRST one generated; a statement that generates none — an explicit
786 /// id, an UPDATE, a DELETE, a plain table — leaves it alone.
787 pub(crate) last_insert_id: i64,
788 /// v7.39 (round 426) — MySQL's `ROW_COUNT()`. Per SESSION like
789 /// `last_insert_id`. MariaDB, measured: a DML statement leaves the
790 /// number of rows it CHANGED (an UPDATE that matched but changed
791 /// nothing leaves 0); a SELECT leaves -1; DDL leaves 0. A FRESH
792 /// session reads 0 (measured), not -1.
793 pub(crate) row_count: i64,
794 /// v7.39 (round 430) — MySQL USER variables (`SET @x = 5`). Per
795 /// SESSION like `last_insert_id` / `row_count`; its own namespace,
796 /// separate from the `@@` session parameters. Reading an unset one
797 /// answers NULL, as MariaDB does.
798 pub(crate) user_vars: BTreeMap<String, spg_storage::Value<'static>>,
799 /// v7.39 (round 436) — the logical names of this session's TEMPORARY
800 /// tables. Each is stored in the catalog under a per-session prefix; this
801 /// set is what says "resolve `t` to my temp one" and what `end_session`
802 /// walks to drop them.
803 pub(crate) temp_tables: alloc::collections::BTreeSet<String>,
804 /// v7.39 (round 469) — the session's TEMPORARY sequences and views, by
805 /// logical name. Separate sets because dropping one at session end has
806 /// to name the catalog map it lives in.
807 pub(crate) temp_sequences: alloc::collections::BTreeSet<String>,
808 pub(crate) temp_views: alloc::collections::BTreeSet<String>,
809}
810
811/// v7.39 (round 306) — one open large-object descriptor.
812#[derive(Debug, Clone, Copy)]
813pub(crate) struct LargeObjectDescriptor {
814 pub(crate) oid: u32,
815 /// Byte offset the next read / write starts at.
816 pub(crate) pos: u64,
817 /// Whether the descriptor was opened with `INV_WRITE`. Reads need no
818 /// permission at all in PG — a write-only descriptor reads fine —
819 /// so only this half is worth remembering.
820 pub(crate) writable: bool,
821}
822
823/// v7.39 (round 277) — one SQL-level prepared statement.
824#[derive(Debug, Clone)]
825pub(crate) struct PreparedSqlStatement {
826 /// The body with its `$N` placeholders still in place.
827 pub(crate) body: spg_sql::ast::Statement,
828 /// Declared parameter type names, in order; empty when PG would
829 /// have inferred them.
830 pub(crate) param_types: alloc::vec::Vec<String>,
831 /// The whole `PREPARE …` text, which `pg_prepared_statements`
832 /// reports verbatim.
833 pub(crate) source: String,
834}
835
836/// `Clone` is O(1) on the catalog (Arc bump) and cheap typed-clones
837/// on the trailers. Decouples "capture state" from "serialize bytes"
838/// so the background-checkpoint worker can hold the snapshot and
839/// produce bytes off the engine write lock.
840#[derive(Debug, Clone)]
841pub struct EngineSnapshot {
842 catalog: Catalog,
843 users: UserStore,
844 publications: publications::Publications,
845 subscriptions: subscriptions::Subscriptions,
846 statistics: statistics::Statistics,
847}
848
849impl EngineSnapshot {
850 /// Same envelope rules as `Engine::snapshot()`: bare catalog when
851 /// every trailer is empty, full envelope otherwise.
852 pub fn serialize(&self) -> Vec<u8> {
853 if self.users.is_empty()
854 && self.publications.is_empty()
855 && self.subscriptions.is_empty()
856 && self.statistics.is_empty()
857 {
858 self.catalog.serialize()
859 } else {
860 build_envelope(
861 &self.catalog.serialize(),
862 &users::serialize_users(&self.users),
863 &self.publications.serialize(),
864 &self.subscriptions.serialize(),
865 &self.statistics.serialize(),
866 )
867 }
868 }
869}
870
871/// v7.39 (parallel-agg P0) — host-injected parallel executor. The
872/// engine is `no_std` and cannot spawn threads; like `ClockFn` /
873/// `RandomFn`, the std-side host (spg-server / embedded-tokio)
874/// injects an implementation at startup. `None` (the default, and
875/// the only option in pure-`no_std` embeddings) keeps every code
876/// path single-threaded and byte-identical to pre-P0 behaviour.
877///
878/// The callback returns `Box<dyn Any + Send>` so one trait serves
879/// any shard-result type; call sites downcast what they produced.
880pub trait ParallelRunner: Send + Sync {
881 /// Run `f(0) .. f(n-1)`, possibly concurrently; return the
882 /// results in shard order. Every call completes before return.
883 fn run_shards(
884 &self,
885 n: usize,
886 f: &(dyn Fn(usize) -> alloc::boxed::Box<dyn core::any::Any + Send> + Sync),
887 ) -> alloc::vec::Vec<alloc::boxed::Box<dyn core::any::Any + Send>>;
888}
889
890/// Engine slot for the injected runner — a newtype so the `Engine`
891/// derive(Debug) keeps working over the non-Debug trait object.
892#[derive(Clone, Default)]
893pub struct ParallelRunnerSlot(pub(crate) Option<alloc::sync::Arc<dyn ParallelRunner>>);
894
895impl core::fmt::Debug for ParallelRunnerSlot {
896 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
897 f.write_str(if self.0.is_some() {
898 "ParallelRunner(<injected>)"
899 } else {
900 "ParallelRunner(none)"
901 })
902 }
903}
904
905/// v7.39 (parallel-agg P0) — below this many input rows a query
906/// never parallelises: thread spin-up beats the win on small scans.
907pub(crate) const PARALLEL_MIN_ROWS: usize = 100_000;
908
909/// v7.39 — diagnostic counter: how many aggregate scans took the
910/// sharded path (read by benches to ground-truth activation).
911/// v7.39 (round 740) — matview delta ground-truth counters (the r735
912/// lesson: a green content pin cannot distinguish "delta applied" from
913/// "silently fell back to full"; these can).
914pub static MATVIEW_FANOUT_BUFFERED: core::sync::atomic::AtomicU64 =
915 core::sync::atomic::AtomicU64::new(0);
916pub static MATVIEW_DELTA_APPLIED: core::sync::atomic::AtomicU64 =
917 core::sync::atomic::AtomicU64::new(0);
918pub static MATVIEW_DELTA_BAILED: core::sync::atomic::AtomicU64 =
919 core::sync::atomic::AtomicU64::new(0);
920pub static PARALLEL_AGG_FIRED: core::sync::atomic::AtomicU64 =
921 core::sync::atomic::AtomicU64::new(0);
922
923// The engine carries several independent session/capture flags (dialect,
924// FK-checks, meta-view materialisation, redo capture); they're orthogonal
925// switches, not a state enum begging to be modelled.
926#[allow(clippy::struct_excessive_bools)]
927#[derive(Debug, Default)]
928pub struct Engine {
929 /// v7.39 (parallel-agg P0) — see [`ParallelRunner`].
930 pub(crate) parallel_runner: ParallelRunnerSlot,
931 /// Committed catalog — what survives `Engine::snapshot()` and what
932 /// outside-TX `SELECT`s read.
933 catalog: Catalog,
934 /// Active TX slots, keyed by `TxId`. Empty when no TX is in flight.
935 /// v4.41.1 runtime invariant: at most one entry (single-writer
936 /// model unchanged). v4.42 will let dispatch hold multiple entries
937 /// concurrently for group commit + engine MVCC.
938 tx_catalogs: BTreeMap<TxId, TxState>,
939 /// v7.39 (round 552) — the COMMIT SEQUENCE at which each table was
940 /// last written. Commit order, not begin order: a transaction that
941 /// began first can commit last, so the writer version allocated at
942 /// BEGIN cannot answer "did this change after I read it".
943 table_last_commit: BTreeMap<String, u64>,
944 /// Monotonic, bumped once per successful COMMIT.
945 commit_seq: u64,
946 /// Which slot the next exec_* call should mutate. Set by
947 /// `execute_in(sql, tx_id)` at the entry point; legacy `execute(sql)`
948 /// sets it to `IMPLICIT_TX`. None when no TX is in flight (read /
949 /// write goes straight against `catalog`).
950 current_tx: Option<TxId>,
951 /// Monotonic counter for `alloc_tx_id`. Starts at 1 — slot 0 is
952 /// reserved for `IMPLICIT_TX`.
953 next_tx_id: u64,
954 /// v7.37.15 (Phase C) — versions allocated by in-flight
955 /// writers. Snapshot construction folds this into the
956 /// `Snapshot.in_progress` set so concurrent readers (or readers
957 /// inside an older snapshot's REPEATABLE READ) don't see
958 /// uncommitted writes.
959 ///
960 /// SPG's single-writer invariant means at most one writer
961 /// version sits here at any moment (the one currently
962 /// executing inside the engine write lock). The set survives
963 /// engine clones because tx-commit removes versions before the
964 /// `Engine::snapshot_data` returns, so a snapshot taken after
965 /// commit observes an empty set.
966 ///
967 /// `BTreeSet` (not Vec) so iteration is sorted — Snapshot
968 /// constructor expects the input sorted for binary-search
969 /// `contains` correctness.
970 active_writer_versions: BTreeSet<u64>,
971 /// v7.37.15 (Phase C.2) — writer versions that ABORTED (rolled
972 /// back). The engine-side visibility oracle ([`Self::xact_status`])
973 /// consults this to report `Aborted` for a version that left the
974 /// in-flight set via rollback rather than commit — the third state
975 /// the abort-aware [`spg_storage::snapshot::Snapshot::visible_with_status`]
976 /// needs once Phase C.3's in-place writes leave aborted stamps in
977 /// place. Pruned below `oldest_active` by vacuum (Phase D); until
978 /// then it grows only with rolled-back transactions (a never-die
979 /// follow-up, not a commit-path leak).
980 aborted_versions: BTreeSet<u64>,
981 /// v7.37.15 (Phase C.4) — row-level lock table keyed on stable
982 /// `(RelId, RowId)`. The in-place write path (C.3) acquires a
983 /// tuple lock before stamping xmax; `exec_commit` / `exec_rollback`
984 /// release the whole transaction's locks at end. No writer acquires
985 /// yet at this commit — the field + delegating methods are the
986 /// plumbing the write path consumes next. Rides on `Engine` like
987 /// `active_writer_versions`; the sharded lock-free manager is C.5.
988 locks: crate::locks::LockTable,
989 /// v7.37.15 (Phase C.3) — kill switch for the in-place MVCC write
990 /// path. `false` (default) = legacy physical semantics (DELETE
991 /// physically removes the row, UPDATE replaces in place). `true` =
992 /// the C.3 write path (DELETE tombstones via `mark_row_deleted`,
993 /// UPDATE tombstones the old version + appends the new one, both
994 /// keeping dead versions physically present for the now-uniformly-
995 /// gated readers until vacuum reclaims them). `no_std` engine can't
996 /// read env; the host (spg-server / spg-embedded) reads
997 /// `SPG_MVCC_INPLACE` and calls [`Self::set_mvcc_inplace`]. Off
998 /// until the write path + PG18 differential tests are proven.
999 mvcc_inplace: bool,
1000 /// v7.37.16 — threshold-triggered synchronous vacuum at DML statement
1001 /// exit (autovacuum-lite; see .claude/state/autovacuum-design.md).
1002 /// Default ON; hosts may disable via `SPG_AUTOVACUUM=0`.
1003 autovacuum: bool,
1004 /// v7.39 (round 173) — whether the statement-exit trigger runs the
1005 /// vacuum **inline**. Default ON (embedded: single-threaded host,
1006 /// the statement path is the only place work can happen). A host
1007 /// with a background autovacuum worker (spg-server) flips this off
1008 /// and drives [`Self::autovacuum_tick`] from its own thread instead
1009 /// — PG's shape, where autovacuum never runs inside a client
1010 /// statement. Only meaningful while `autovacuum` itself is on.
1011 autovacuum_inline: bool,
1012 /// v7.37.15 (Phase C) — TxId → writer version registry. When
1013 /// `exec_begin` opens an explicit transaction it allocates a
1014 /// fresh writer version (via [`Self::begin_writer_version`])
1015 /// and stashes the mapping here so the matching `exec_commit`
1016 /// / `exec_rollback` can call
1017 /// [`Self::commit_writer_version`] on the right entry. Empty
1018 /// when no explicit transactions are open.
1019 /// v7.39 (round 295, E3 Phase 1b) — rows a `SKIP LOCKED` pass found
1020 /// held by another transaction. Set by the locking pre-pass and
1021 /// consulted by the base scan, which is READ-only here: the locks
1022 /// themselves were taken under `&mut self` in the pre-pass, so the
1023 /// `&self` scan never mutates the lock table. (A `RefCell` there
1024 /// would cost `Engine: Sync`, which the server's `RwLock<Engine>`
1025 /// needs — see the RFC's §5.6.)
1026 pub(crate) lock_skip_rows: Option<(String, alloc::collections::BTreeSet<usize>)>,
1027 tx_writer_versions: BTreeMap<TxId, u64>,
1028 /// v7.37.15 (Epic W slice 2) — the current statement's autocommit
1029 /// writer version, memoized. In autocommit
1030 /// [`Self::writer_version_for_current_stmt`] mints a fresh version
1031 /// via `next_version()` (a `fetch_add`), so calling it a second
1032 /// time — e.g. when the redo drain post-stamps `RowChange`s —
1033 /// would allocate a *different* number than the writes actually
1034 /// used. Memoizing the first allocation for the duration of one
1035 /// statement makes the drain stamp read back the exact version the
1036 /// rows were written with, without advancing the counter twice.
1037 /// `None` outside a statement / before the first fetch; saved and
1038 /// reset per `execute_in_with_cancel` so it never leaks across
1039 /// statements. Explicit transactions bypass this (their version is
1040 /// the deterministic `tx_writer_versions` entry).
1041 stmt_writer_version: Option<u64>,
1042 /// v7.22 (round-13 T3) — session string-literal dialect. `false`
1043 /// (default) = PG semantics (backslash literal, `''` escape);
1044 /// `true` = MySQL semantics (`\'` etc.). Flipped by the
1045 /// deterministic session signals each dump emits: `SET sql_mode`
1046 /// (only MySQL clients/dumps send it) turns it on,
1047 /// `SET standard_conforming_strings = on` (every pg_dump
1048 /// preamble) turns it off. The plan cache is cleared on every
1049 /// flip — the same SQL text lexes differently per dialect.
1050 backslash_escapes: bool,
1051 /// v7.39 (round 470) — see [`SessionBag::mysql_strict`].
1052 mysql_strict: bool,
1053 /// v7.39 (round 306) — the live session's open large-object
1054 /// descriptors, swapped in and out with the rest of its bag.
1055 pub(crate) lo_descriptors: BTreeMap<i32, LargeObjectDescriptor>,
1056 pub(crate) lo_next_fd: i32,
1057 /// v7.37.17 — name of the sequence most recently advanced by
1058 /// nextval() in this Engine (session). Backs PG's lastval().
1059 /// None until the first nextval; PG errors in that state.
1060 last_sequence_used: Option<String>,
1061 /// v7.39 (round 499) — per-session `currval` values; see
1062 /// [`SessionBag::seq_currvals`].
1063 seq_currvals: alloc::collections::BTreeMap<String, i64>,
1064 /// v7.39 (round 277) — SQL-level prepared statements, session
1065 /// scoped exactly as in PG. Keyed by name; each entry keeps the
1066 /// parsed body (placeholders intact), the declared parameter type
1067 /// names and the statement text `pg_prepared_statements` reports.
1068 prepared_statements: alloc::collections::BTreeMap<String, PreparedSqlStatement>,
1069 /// v7.39 (round 279) — which connection's state is currently
1070 /// installed in the fields above. 0 is the embedded / default
1071 /// session.
1072 current_session: u32,
1073 /// Parked state for every OTHER connection.
1074 sessions: BTreeMap<u32, SessionBag>,
1075 /// v7.39 (round 279) — advisory locks, held ACROSS sessions and so
1076 /// deliberately NOT part of the swapped bag: the whole purpose of
1077 /// an advisory lock is to be visible to the other connection.
1078 /// key → (owning session, re-entrant depth). PG allows the same
1079 /// session to take a lock it already holds.
1080 advisory_locks: BTreeMap<i64, (u32, u32)>,
1081 /// Optional wall clock used to satisfy `NOW()` / `CURRENT_TIMESTAMP`
1082 /// / `CURRENT_DATE`. Set by the host environment.
1083 clock: Option<ClockFn>,
1084 /// v4.1 cryptographic RNG for per-user password salt. Set by the
1085 /// host. `None` means SQL-driven `CREATE USER` uses a
1086 /// deterministic fallback — see `SaltFn`.
1087 salt_fn: Option<SaltFn>,
1088 /// v4.2 per-query row cap. `None` = unlimited. When set, a
1089 /// SELECT that materialises more than `n` rows returns
1090 /// `EngineError::RowLimitExceeded`. Enforced before the result
1091 /// is shaped into wire frames so a runaway scan can't blow the
1092 /// server's heap.
1093 max_query_rows: Option<usize>,
1094 /// v7.30.3 (mailrs round-26) per-query byte cap on join/filter
1095 /// materialisation. `None` = unlimited. Approximate net
1096 /// accounting (Value heap payloads + per-cell enum overhead)
1097 /// charged at every point the join pipeline clones rows;
1098 /// crossing the cap raises `EngineError::QueryBytesExceeded`
1099 /// instead of pressuring the host into reclaim livelock. The
1100 /// host wires this to `SPG_MAX_QUERY_BYTES` (embed defaults it
1101 /// ON; the server keeps its allocator-precise budget as the
1102 /// outer layer).
1103 pub(crate) max_query_bytes: Option<usize>,
1104 /// v7.39 (round 786, T35 Phase A) — host factory for spill runs.
1105 /// `None` (the default, and every embedded caller that has not opted
1106 /// in) keeps today's behaviour exactly: a sort that outgrows
1107 /// `max_query_bytes` still refuses rather than spilling.
1108 pub(crate) temp_run_factory: Option<crate::TempRunFactory>,
1109 /// v4.1 RBAC user table. Empty means "no RBAC configured yet" —
1110 /// the server decides what that means at the auth boundary
1111 /// (open mode vs legacy single-password mode). User CRUD goes
1112 /// through `create_user`/`drop_user`/`verify_user`; persistence
1113 /// rides the snapshot envelope alongside the catalog.
1114 pub(crate) users: UserStore,
1115 /// v6.1.2 logical-replication publication catalog. Empty until
1116 /// `CREATE PUBLICATION` runs. Persistence rides the v3 envelope
1117 /// trailer (see `build_envelope`).
1118 publications: publications::Publications,
1119 /// v6.1.4 logical-replication subscription catalog. Empty until
1120 /// `CREATE SUBSCRIPTION` runs. Persistence rides the v4 envelope
1121 /// trailer.
1122 subscriptions: subscriptions::Subscriptions,
1123 /// v6.2.0 — per-column statistics for the cost-based optimizer.
1124 /// Populated by `ANALYZE`; queried via `spg_statistic` virtual
1125 /// table. Persistence rides the v5 envelope trailer.
1126 statistics: statistics::Statistics,
1127 /// v6.3.0 — engine-level plan cache. Caches the post-`prepare()`
1128 /// `Statement` keyed on SQL text. In-memory only — does NOT ride
1129 /// the snapshot envelope (rebuilt on demand after restart).
1130 plan_cache: plan_cache::PlanCache,
1131 /// v6.5.1 — per-distinct-SQL execution stats. In-memory only,
1132 /// surfaced via `spg_stat_query` virtual table. Updated by the
1133 /// `execute_*` paths after a successful execute.
1134 query_stats: query_stats::QueryStats,
1135 /// v6.5.2 — connection-state provider callback. spg-server
1136 /// registers a function at startup that snapshots its
1137 /// per-pgwire-connection registry into `ActivityRow`s; engine
1138 /// reads through it on every `SELECT * FROM spg_stat_activity`.
1139 /// `None` ⇒ no-data (returns empty rows; matches the no_std
1140 /// embedded callers that don't run pgwire).
1141 activity_provider: Option<ActivityProvider>,
1142 /// v6.5.3 — audit-chain provider + verifier. Same pattern as
1143 /// activity_provider: spg-server registers both at startup;
1144 /// engine reads through on `SELECT * FROM spg_audit_chain` and
1145 /// `SELECT * FROM spg_audit_verify`. `None` ⇒ no-data.
1146 audit_chain_provider: Option<AuditChainProvider>,
1147 audit_verifier: Option<AuditVerifier>,
1148 /// v6.5.6 — slow-query log threshold in microseconds. When set,
1149 /// every successful execute whose elapsed exceeds the threshold
1150 /// gets fed to the registered slow-query log callback (so
1151 /// spg-server can emit a structured log line). Default `None`
1152 /// = no slow-query logging.
1153 slow_query_threshold_us: Option<u64>,
1154 slow_query_logger: Option<SlowQueryLogger>,
1155 /// v7.12.1 — session parameters set via `SET <name> = <value>`.
1156 /// Only `default_text_search_config` is consumed by the engine
1157 /// today (the FTS function dispatcher reads it when
1158 /// `to_tsvector(text)` is called without an explicit config).
1159 /// All other names are accepted + recorded so PG-dump output
1160 /// loads, but have no behavioural effect.
1161 pub(crate) session_params: BTreeMap<String, String>,
1162 /// v7.39 (round 218) — open server-side cursors (DECLARE … CURSOR),
1163 /// keyed by name. Materialized at DECLARE (INSENSITIVE semantics —
1164 /// PG's only actual behaviour too); FETCH / MOVE walk the stored rows.
1165 /// Lifecycle: created only inside a transaction; COMMIT closes
1166 /// non-HOLD cursors and marks WITH HOLD ones held; ROLLBACK closes
1167 /// everything not already held by an earlier commit. Never serialized.
1168 /// Session-scoped in PG; SPG stores them engine-wide (the same
1169 /// process-level session-state architecture wall as `session_params`).
1170 pub(crate) cursors: BTreeMap<String, cursor::OpenCursor>,
1171 /// v7.39 (round 347, M2) — the current session's LAST_INSERT_ID().
1172 /// Swapped with [`SessionBag`] like every other per-connection slot.
1173 /// An atomic because `LAST_INSERT_ID(expr)` SETS it while evaluation
1174 /// holds only `&Engine` — and `Engine` must stay `Sync`, which a
1175 /// `Cell` would have taken away (spg-embedded-tokio shares one across
1176 /// tasks; clippy caught it there before the tests did).
1177 pub(crate) last_insert_id: core::sync::atomic::AtomicI64,
1178 /// v7.39 (round 426) — the current session's ROW_COUNT(). Swapped
1179 /// with [`SessionBag`] like every other per-connection slot. A plain
1180 /// i64: unlike LAST_INSERT_ID it is only ever WRITTEN from the
1181 /// statement driver, which holds `&mut Engine`.
1182 pub(crate) row_count: i64,
1183 /// v7.39 (round 430) — this session's MySQL USER variables.
1184 /// Swapped with [`SessionBag`] like every other per-connection slot.
1185 pub(crate) user_vars: BTreeMap<String, spg_storage::Value<'static>>,
1186 /// v7.39 (round 436) — the logical names of this session's TEMPORARY
1187 /// tables. Swapped with [`SessionBag`]; see `session_temp_name`.
1188 pub(crate) temp_tables: BTreeSet<String>,
1189 pub(crate) temp_sequences: BTreeSet<String>,
1190 pub(crate) temp_views: BTreeSet<String>,
1191 /// v7.39 (round 222) — channels this session LISTENs on. Engine-wide
1192 /// (the same process-level session-state architecture wall as
1193 /// `session_params`). Never serialized.
1194 pub(crate) listen_channels: BTreeSet<String>,
1195 /// v7.39 (round 222) — NOTIFYs raised inside the current transaction,
1196 /// held until COMMIT (PG: transactional delivery, deduplicated within
1197 /// the tx); dropped at ROLLBACK.
1198 pub(crate) tx_pending_notifies: Vec<(String, String)>,
1199 /// v7.39 (round 222) — committed notifications on LISTENed channels,
1200 /// awaiting a drain by the wire layer ('A' NotificationResponse) or an
1201 /// embedded caller ([`Engine::take_notifications`]).
1202 pub(crate) delivered_notifies: Vec<(String, String)>,
1203 /// v7.39 (read01 round 46) — NOTICEs raised by the statement now
1204 /// executing. PG emits a NoticeResponse whenever an `IF EXISTS` /
1205 /// `IF NOT EXISTS` clause makes it skip work ("table \"t\" does not
1206 /// exist, skipping"). The engine appends the PG-worded text here;
1207 /// the caller drains it with [`Engine::take_notices`] after each
1208 /// statement (pgwire turns each into an 'N' message, embedded
1209 /// callers can ignore or surface them). Cleared at the start of
1210 /// every statement so a notice never leaks into the next one.
1211 pending_notices: Vec<Notice>,
1212 /// v7.38 (read01 P3.12) — cumulative row-write counters feeding
1213 /// `pg_stat_database` (database-wide `tup_inserted` / `tup_updated` /
1214 /// `tup_deleted`). Bumped by the affected-row count of each successful
1215 /// INSERT / UPDATE / DELETE statement. Per-Engine (so tests stay
1216 /// isolated); on the server's shared engine they read as the
1217 /// since-start database totals PG reports.
1218 /// v7.39 (pg_stat knife A) — committed / rolled-back transaction
1219 /// counters for pg_stat_database. Atomics so the read-only
1220 /// autocommit path (&self) can count its implicit commit, matching
1221 /// PG (every successful statement outside a tx block is one
1222 /// xact_commit — SELECTs included).
1223 /// v7.37 (round 884) — what sorts have spilled in this process, for
1224 /// `pg_stat_database` and for EXPLAIN ANALYZE's `Sort Method`.
1225 pub(crate) spill_stats: crate::tempstore::SpillStats,
1226 pub(crate) xact_commit: core::sync::atomic::AtomicU64,
1227 pub(crate) xact_rollback: core::sync::atomic::AtomicU64,
1228 /// v7.39 (pg_stat knife A) — host-injected live backend count for
1229 /// pg_stat_database.numbackends (ClockFn-style fn slot; the server
1230 /// wires its connection registry, embedded stays None -> 1).
1231 pub(crate) backend_count_fn: Option<BackendCountFn>,
1232 pub(crate) backend_pid_fn: Option<BackendPidFn>,
1233 /// v7.39 (round 476) — see [`WalLsnFn`].
1234 pub(crate) wal_lsn_fn: Option<WalLsnFn>,
1235 /// v7.39 (round 318, V51) — host connection-control hook. See
1236 /// [`BackendSignalFn`].
1237 pub(crate) backend_signal_fn: Option<BackendSignalFn>,
1238 /// v7.39 (tz epic) — injected IANA timezone lookups; None on a
1239 /// host without zoneinfo (named zones then fail to SET, honestly).
1240 pub(crate) tz_offset_fn: Option<TzOffsetFn>,
1241 pub(crate) tz_localize_fn: Option<TzLocalizeFn>,
1242 pub(crate) tz_canon_fn: Option<TzCanonFn>,
1243 pub(crate) tz_abbrev_fn: Option<TzAbbrevFn>,
1244 /// v7.39 (round 502) — see [`TzAllFn`].
1245 pub(crate) tz_all_fn: Option<TzAllFn>,
1246 pub(crate) stat_tup_inserted: u64,
1247 pub(crate) stat_tup_updated: u64,
1248 pub(crate) stat_tup_deleted: u64,
1249 /// v7.39 (round 192) — per-table DML counters for
1250 /// pg_stat_user_tables (n_tup_ins / n_tup_upd / n_tup_del).
1251 /// Engine-side and NON-transactional, like PG's stats collector:
1252 /// a rolled-back INSERT still counts, and a tx's counts don't
1253 /// ride the shadow catalog (the RC rebase rebuilt shadow tables
1254 /// from the committed base, silently dropping any counter bumped
1255 /// on the shadow — the r192 probe's tx-wrapped inserts read 0).
1256 /// Keyed by table name; DROP TABLE clears, RENAME re-keys.
1257 pub(crate) table_write_stats: alloc::collections::BTreeMap<String, (u64, u64, u64)>,
1258 /// v7.39 (round 196) — bumped after every completed statement that
1259 /// ran OUTSIDE a transaction block (any autocommit statement, plus
1260 /// COMMIT itself via the post-statement check). An open tx whose
1261 /// `rebased_at_epoch` equals this value knows the committed base
1262 /// hasn't moved and skips the per-statement RC rebase (whose
1263 /// write-set extraction full-scans every touched table).
1264 /// Over-approximation is deliberate: read-only statements bump it
1265 /// too, which only costs an extra (correct) rebase.
1266 pub(crate) commit_epoch: u64,
1267 /// v7.38 (read01 P3.19) — `SET LOCAL` undo log for the current
1268 /// transaction. Each entry is `(param_name, prior_value)` captured
1269 /// just before a `SET LOCAL` overwrote it (`None` = the param had no
1270 /// session value, so restoring means removing it). Replayed in
1271 /// reverse at COMMIT / ROLLBACK to revert transaction-local settings;
1272 /// `savepoint_guc_marks` records the stack depth at each open
1273 /// savepoint so `ROLLBACK TO` can unwind just the later ones.
1274 pub(crate) local_guc_saves: Vec<(String, Option<String>)>,
1275 /// v7.39 (GUC knife 3) — parsed DateStyle / IntervalStyle /
1276 /// extra_float_digits, kept in lockstep with `session_params` so
1277 /// renderers don't re-parse GUC text per cell.
1278 pub(crate) render_style: crate::eval::RenderStyle,
1279 pub(crate) savepoint_guc_marks: Vec<(String, usize)>,
1280 /// v7.12.7 — depth counter for trigger-emitted embedded SQL.
1281 /// Each time the engine executes a `DeferredEmbeddedStmt` it
1282 /// increments this; the recursive `execute_stmt_with_cancel`
1283 /// inside that path checks against [`MAX_TRIGGER_RECURSION`]
1284 /// to bound runaway cascades (trigger A's UPDATE on table B
1285 /// fires trigger B which UPDATEs table A which fires trigger
1286 /// A again…). Reset to 0 once the original DML returns.
1287 trigger_recursion_depth: u32,
1288 /// v7.39 (round 140) — set while a DELETE / UPDATE is being re-run by the
1289 /// DO ALSO rule wrapper so the wrapper's inner call does not re-enter the
1290 /// rule-rewrite path (which would recurse forever). INSERT captures its
1291 /// post-image rows directly and needs no such guard.
1292 rule_rewrite_active: bool,
1293 /// v7.14.0 — when `SET FOREIGN_KEY_CHECKS=0` is in effect
1294 /// (mysqldump preamble), the FK existence + arity check at
1295 /// CREATE TABLE time is deferred. FKs referencing a
1296 /// not-yet-existing parent land in `pending_foreign_keys`
1297 /// keyed by child table; `SET FOREIGN_KEY_CHECKS=1` drains
1298 /// the queue and resolves each FK against the now-complete
1299 /// catalog. Empty by default; the queue is drained on every
1300 /// `RESET ALL` too.
1301 foreign_key_checks: bool,
1302 /// v7.16.2 — true on the temp Engine an outer
1303 /// `exec_select_with_meta_views` builds, telling that
1304 /// temp engine "stop short-circuiting into the meta-view
1305 /// path — your catalog already has the materialised
1306 /// tables; just run the regular SELECT." Without this we'd
1307 /// infinite-loop since the meta-view name (e.g.
1308 /// `__spg_info_columns`) still triggers
1309 /// `select_references_meta_view`.
1310 meta_views_materialised: bool,
1311 pending_foreign_keys: Vec<(alloc::string::String, spg_sql::ast::ForeignKeyConstraint)>,
1312 /// v7.38 元机制 D — frozen snapshot of `SPG_TEST_*` env vars. Read
1313 /// once at construction (`with_env_cfg`) and queried on hot paths
1314 /// via `engine.env_cfg().<field>`. Production builds keep this at
1315 /// `EnvConfig::default()`, so the optimiser can const-fold every
1316 /// `if env_cfg.<field>` gate. See `testkit::env_config` + the
1317 /// `xtests/sigil/test-mode-gucs.md` index.
1318 env_cfg: testkit::EnvConfig,
1319 /// v7.38 P0 元机制 A — per-engine `injection_points` attach
1320 /// table. Only exists when the crate is built with the
1321 /// `injection-points` feature; release builds carry no field.
1322 /// Pushed onto the thread-local stack by
1323 /// `enter_injection_scope()` so the `injection_point!()` macro
1324 /// can find it from anywhere in the executor without rewiring
1325 /// every signature. See
1326 /// `crates/spg-engine/src/testkit/injection.rs`.
1327 #[cfg(feature = "injection-points")]
1328 injection_store: alloc::sync::Arc<crate::testkit::injection::InjectionStore>,
1329 /// v7.34 (crash-recovery P0 #2) — row-level redo capture. When the
1330 /// embedding layer turns this on (persistence enabled), each mutating
1331 /// `execute` records the physical [`RowChange`]s it applied; the
1332 /// engine drains them into `last_redo` on success, and the embedded
1333 /// layer reads them via [`Engine::take_redo`] to write the WAL in
1334 /// place of the SQL text. Off (default) = zero capture overhead.
1335 redo_capture: bool,
1336 /// Redo captured by the most recent successful mutating `execute`,
1337 /// awaiting drain by the embedding layer. Cleared on each capture.
1338 last_redo: Vec<RowChange>,
1339 /// v7.39 (round 735, S14/B3) — per-table change sequence, bumped on
1340 /// every write entry (INSERT / UPDATE / DELETE / TRUNCATE / COPY /
1341 /// table-shape DDL). In-memory only: after a restart the map is
1342 /// empty, every watermark comparison misses, and the next REFRESH
1343 /// is a full one — stale-view-safe by construction. A rolled-back
1344 /// transaction's bump stays too, which can only cause an EXTRA full
1345 /// refresh, never a wrong no-op.
1346 table_change_seq: alloc::collections::BTreeMap<String, u64>,
1347 /// v7.39 (round 735, S14/B3) — per-materialized-view refresh
1348 /// watermark: the (table, change-seq) pairs its last full refresh
1349 /// saw. When every dependency's seq is unchanged, REFRESH is an
1350 /// O(1) no-op — an incremental-maintenance first step PG does not
1351 /// have (its REFRESH always recomputes).
1352 matview_refresh_watermark: alloc::collections::BTreeMap<String, Vec<(String, u64)>>,
1353 /// v7.39 (round 736, S14/B3 knife 2) — delta-maintainable
1354 /// materialized views: mv name -> its single base table. Registered
1355 /// at CREATE MATERIALIZED VIEW / full REFRESH when the body is a
1356 /// single-stored-table pure projection (no aggregates / joins /
1357 /// CTEs / subqueries / DISTINCT / ORDER / LIMIT / windows / SRFs).
1358 matview_maintainable: alloc::collections::BTreeMap<String, String>,
1359 /// Buffered base-table row changes per maintainable view, fanned
1360 /// out from the statement redo drain. Capped (see
1361 /// `MATVIEW_DELTA_CEILING`); an overflowed view falls back to a
1362 /// full refresh — never-die, never-stale.
1363 matview_delta_buf: alloc::collections::BTreeMap<String, Vec<RowChange>>,
1364 matview_delta_overflow: alloc::collections::BTreeSet<String>,
1365 /// v7.39 (round 738, S14/B3 knife 3) — per-view row map: expected
1366 /// PHYSICAL length of the view's backing table, plus base-row
1367 /// RowId -> view row position. Built only by the maintainable full
1368 /// refresh's internal scan (the SQL path cannot see rowids), and
1369 /// consulted by the delete/tombstone delta arms. In-memory: restart
1370 /// or any length mismatch (a vacuum moved rows) -> full refresh.
1371 matview_row_map:
1372 alloc::collections::BTreeMap<String, (usize, alloc::collections::BTreeMap<u64, usize>)>,
1373 /// v7.38 轴 4 — currently-selected SQL isolation level. Set by
1374 /// `SET TRANSACTION ISOLATION LEVEL …`; read by
1375 /// `SHOW transaction_isolation`. v7.37.8 implements the
1376 /// SQL surface; actual semantic differentiation (REPEATABLE READ
1377 /// snapshot / SERIALIZABLE SSI) lands in a separate train.
1378 pub(crate) current_isolation_level: spg_sql::ast::IsolationLevel,
1379}
1380
1381/// v7.12.7 — hard cap on nested trigger-emitted embedded SQL
1382/// fires. 16 deep is well past anything a normal trigger graph
1383/// uses while still preventing infinite-loop wedging.
1384const MAX_TRIGGER_RECURSION: u32 = 16;
1385
1386/// v6.5.6 — callback signature for slow-query log emission. Called
1387/// with `(sql, elapsed_us)` once per successful execute that crosses
1388/// the threshold.
1389pub type SlowQueryLogger = fn(&str, u64);
1390
1391/// v6.5.2 — one row of `spg_stat_activity`. Engine-public so
1392/// spg-server can construct rows without re-exporting internal
1393/// dispatch types.
1394#[derive(Debug, Clone)]
1395pub struct ActivityRow {
1396 pub pid: u32,
1397 pub user: String,
1398 /// v7.39 (round 319, V52) — the peer's IP, empty when the connection
1399 /// has no TCP peer (PG reports NULL there).
1400 pub client_addr: String,
1401 /// v7.39 (round 319, V52) — the peer's port. PG reports **-1**, not
1402 /// NULL, for a connection with no TCP port; measured on PG 18.4.
1403 pub client_port: i32,
1404 /// v7.39 (round 319, V52) — the database this connection named. Empty
1405 /// when it named none; both `pg_stat_activity.datname` and
1406 /// `SHOW PROCESSLIST.db` report that as NULL.
1407 pub database: String,
1408 pub started_at_us: i64,
1409 pub current_sql: String,
1410 /// v7.37.14 (B6.3) — PG-style wait-event categorisation
1411 /// ("Lock", "LWLock", "IPC", "IO", "Timeout", "Client",
1412 /// "BufferPin", "Extension", ""). Empty string means idle.
1413 /// Pair with `wait_event` to identify "what specifically is
1414 /// the backend waiting on" the same way PG does.
1415 pub wait_event_type: String,
1416 pub wait_event: String,
1417 pub elapsed_us: i64,
1418 pub in_transaction: bool,
1419 /// v7.17 Phase 2.4 — startup-param `application_name` (or the
1420 /// last value the client sent via `SET application_name = '...'`).
1421 /// Empty when the client never declared one.
1422 pub application_name: String,
1423 /// v7.39 (round 474) — PG's `backend_type`: `client backend` for a
1424 /// connection, or the worker's own name for a background process.
1425 ///
1426 /// pg_stat_activity used to hardcode `client backend`, so SPG's own
1427 /// background workers — the ones that hold the engine write lock and
1428 /// are exactly what an operator is looking for when a statement
1429 /// stalls — did not appear at all. PG18 lists eight of them beside
1430 /// the single client backend on an idle server.
1431 pub backend_type: String,
1432}
1433
1434impl ActivityRow {
1435 /// The `backend_type` PG gives a background process: no database, no
1436 /// user, no query, and a state PG reports as NULL.
1437 #[must_use]
1438 pub fn background(pid: u32, backend_type: &str) -> Self {
1439 Self {
1440 pid,
1441 user: String::new(),
1442 client_addr: String::new(),
1443 client_port: -1,
1444 database: String::new(),
1445 started_at_us: 0,
1446 current_sql: String::new(),
1447 wait_event_type: String::new(),
1448 wait_event: String::new(),
1449 elapsed_us: 0,
1450 in_transaction: false,
1451 application_name: String::new(),
1452 backend_type: backend_type.into(),
1453 }
1454 }
1455}
1456
1457/// v6.5.2 — provider callback type. Fresh snapshot returned each
1458/// call; engine doesn't cache the slice.
1459pub type ActivityProvider = fn() -> Vec<ActivityRow>;
1460
1461/// v7.39 (round 318, V41) — how loud a diagnostic the statement raised is.
1462/// PG distinguishes them on the wire (`S`/`V` fields of NoticeResponse) and
1463/// clients act on it: psql prints `WARNING:` in a different colour, and
1464/// several drivers surface warnings to the application while dropping
1465/// notices. Emitting everything as NOTICE loses that.
1466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1467pub enum NoticeSeverity {
1468 Notice,
1469 Warning,
1470 /// v7.39 (round 757, F31-B3) — `RAISE INFO`. PG sends INFO to the
1471 /// client ALWAYS, regardless of `client_min_messages`.
1472 Info,
1473}
1474
1475impl NoticeSeverity {
1476 /// The non-localized severity string PG puts in the `V` field.
1477 #[must_use]
1478 pub const fn as_pg_str(self) -> &'static str {
1479 match self {
1480 Self::Notice => "NOTICE",
1481 Self::Warning => "WARNING",
1482 Self::Info => "INFO",
1483 }
1484 }
1485}
1486
1487/// v7.39 (round 318, V41) — one diagnostic the statement raised, in PG's
1488/// exact wording minus the severity banner (the wire layer adds that).
1489#[derive(Debug, Clone)]
1490pub struct Notice {
1491 pub severity: NoticeSeverity,
1492 pub message: String,
1493}
1494
1495/// v6.5.3 — one row of `spg_audit_chain`. Engine-public so
1496/// spg-server can construct rows directly from `AuditEntry`.
1497#[derive(Debug, Clone)]
1498pub struct AuditRow {
1499 pub seq: i64,
1500 pub ts_ms: i64,
1501 pub prev_hash_hex: String,
1502 pub entry_hash_hex: String,
1503 pub sql: String,
1504}
1505
1506/// v6.5.3 — chain-table provider + verifier. spg-server registers
1507/// fn pointers that snapshot / verify the audit log. `verify`
1508/// returns `(verified_count, broken_at_seq)` — `broken_at_seq` is
1509/// `-1` on a clean chain.
1510pub type AuditChainProvider = fn() -> Vec<AuditRow>;
1511pub type AuditVerifier = fn() -> (i64, i64);
1512
1513impl Engine {
1514 pub fn new() -> Self {
1515 Self {
1516 catalog: Catalog::new(),
1517 parallel_runner: ParallelRunnerSlot::default(),
1518 tx_catalogs: BTreeMap::new(),
1519 table_last_commit: BTreeMap::new(),
1520 commit_seq: 0,
1521 current_tx: None,
1522 backslash_escapes: false,
1523 mysql_strict: true,
1524 lo_descriptors: BTreeMap::new(),
1525 lo_next_fd: 0,
1526 prepared_statements: alloc::collections::BTreeMap::new(),
1527 current_session: 0,
1528 sessions: BTreeMap::new(),
1529 advisory_locks: BTreeMap::new(),
1530 last_sequence_used: None,
1531 seq_currvals: alloc::collections::BTreeMap::new(),
1532 next_tx_id: 1,
1533 active_writer_versions: BTreeSet::new(),
1534 aborted_versions: BTreeSet::new(),
1535 locks: crate::locks::LockTable::new(),
1536 mvcc_inplace: !cfg!(feature = "mvcc-inplace-off"),
1537 autovacuum: true,
1538 autovacuum_inline: true,
1539 lock_skip_rows: None,
1540 tx_writer_versions: BTreeMap::new(),
1541 stmt_writer_version: None,
1542 clock: None,
1543 salt_fn: None,
1544 max_query_rows: None,
1545 max_query_bytes: None,
1546 temp_run_factory: None,
1547 users: UserStore::new(),
1548 publications: publications::Publications::new(),
1549 subscriptions: subscriptions::Subscriptions::new(),
1550 statistics: statistics::Statistics::new(),
1551 plan_cache: plan_cache::PlanCache::new(),
1552 query_stats: query_stats::QueryStats::new(),
1553 activity_provider: None,
1554 audit_chain_provider: None,
1555 audit_verifier: None,
1556 slow_query_threshold_us: None,
1557 slow_query_logger: None,
1558 session_params: BTreeMap::new(),
1559 cursors: BTreeMap::new(),
1560 last_insert_id: core::sync::atomic::AtomicI64::new(0),
1561 row_count: 0,
1562 user_vars: BTreeMap::new(),
1563 temp_tables: BTreeSet::new(),
1564 temp_sequences: BTreeSet::new(),
1565 temp_views: BTreeSet::new(),
1566 listen_channels: BTreeSet::new(),
1567 tx_pending_notifies: Vec::new(),
1568 delivered_notifies: Vec::new(),
1569 pending_notices: Vec::new(),
1570 spill_stats: crate::tempstore::SpillStats::default(),
1571 xact_commit: core::sync::atomic::AtomicU64::new(0),
1572 xact_rollback: core::sync::atomic::AtomicU64::new(0),
1573 backend_count_fn: None,
1574 backend_pid_fn: None,
1575 wal_lsn_fn: None,
1576 backend_signal_fn: None,
1577 tz_offset_fn: None,
1578 tz_localize_fn: None,
1579 tz_canon_fn: None,
1580 tz_abbrev_fn: None,
1581 tz_all_fn: None,
1582 stat_tup_inserted: 0,
1583 table_write_stats: alloc::collections::BTreeMap::new(),
1584 commit_epoch: 0,
1585 stat_tup_updated: 0,
1586 stat_tup_deleted: 0,
1587 local_guc_saves: Vec::new(),
1588 render_style: crate::eval::RenderStyle::default(),
1589 savepoint_guc_marks: Vec::new(),
1590 trigger_recursion_depth: 0,
1591 rule_rewrite_active: false,
1592 foreign_key_checks: true,
1593 meta_views_materialised: false,
1594 pending_foreign_keys: Vec::new(),
1595 env_cfg: testkit::EnvConfig::default(),
1596 #[cfg(feature = "injection-points")]
1597 injection_store: alloc::sync::Arc::new(
1598 crate::testkit::injection::InjectionStore::default(),
1599 ),
1600 redo_capture: false,
1601 current_isolation_level: spg_sql::ast::IsolationLevel::ReadCommitted,
1602 last_redo: Vec::new(),
1603 table_change_seq: alloc::collections::BTreeMap::new(),
1604 matview_refresh_watermark: alloc::collections::BTreeMap::new(),
1605 matview_maintainable: alloc::collections::BTreeMap::new(),
1606 matview_delta_buf: alloc::collections::BTreeMap::new(),
1607 matview_delta_overflow: alloc::collections::BTreeSet::new(),
1608 matview_row_map: alloc::collections::BTreeMap::new(),
1609 }
1610 }
1611
1612 /// v7.11.0 — clone the engine's committed catalog + read-time
1613 /// state into a frozen `CatalogSnapshot`. Cheap (`Catalog` is
1614 /// backed by `PersistentVec`; cloning is O(log n) per table).
1615 /// Subsequent writes to this engine are invisible to the
1616 /// snapshot; the snapshot is self-contained and can be moved
1617 /// to another thread for concurrent `execute_readonly_on_snapshot`
1618 /// calls. The basis for [`AsyncReadHandle`] in spg-embedded-tokio
1619 /// and any other read-fanout pattern.
1620 #[must_use]
1621 pub fn clone_snapshot(&self) -> CatalogSnapshot {
1622 CatalogSnapshot {
1623 catalog: self.active_catalog().clone(),
1624 statistics: self.statistics.clone(),
1625 clock: self.clock,
1626 max_query_rows: self.max_query_rows,
1627 }
1628 }
1629
1630 /// v7.39 (round 513) — does this role exist? `'x'::regrole` needs it,
1631 /// and roles live on the engine rather than the catalog.
1632 #[must_use]
1633 pub fn role_exists(&self, name: &str) -> bool {
1634 // v7.39 (round 696) — the SESSION's own identity, same class as the
1635 // `postgres` case below and missed by it. `current_user` reported
1636 // the connected name while this predicate denied it, so `SET ROLE
1637 // <me>` refused the role the session was already running as.
1638 if name == self.session_user() {
1639 return true;
1640 }
1641 // The engine's default identity exists even before any CREATE USER.
1642 //
1643 // v7.39 (round 652) — and so does `postgres`. `synth_pg_roles`
1644 // has always inserted it as the bootstrap superuser when no user
1645 // by that name was created, so this predicate and the catalogue
1646 // it is supposed to reflect disagreed: `pg_roles` listed
1647 // `postgres` while `'postgres'::regrole` said it did not exist.
1648 // Every pg_dump names it (`OWNER TO postgres`), so the ALTER
1649 // TABLE OWNER check added this round would have refused the one
1650 // role that appears in essentially every dump.
1651 self.effective_users().contains(name)
1652 || name.eq_ignore_ascii_case("admin")
1653 || name.eq_ignore_ascii_case("postgres")
1654 }
1655
1656 /// v7.39 (round 520) — the role an oid names, as `pg_get_userbyid`
1657 /// reports it. The numbering is `synth_pg_roles`': base 10, one per
1658 /// user in catalog order.
1659 #[must_use]
1660 pub fn role_name_for_oid(&self, oid: i64) -> Option<String> {
1661 // Oid 10 is the bootstrap superuser, which `synth_pg_roles` always
1662 // publishes as `postgres`. Following the catalogue rather than the
1663 // session is the point: a join on `relowner = pg_roles.oid` and
1664 // `pg_get_userbyid(relowner)` have to name the same role.
1665 if oid == 10 {
1666 return Some(alloc::string::String::from("postgres"));
1667 }
1668 let idx = usize::try_from(oid - 11).ok()?;
1669 self.users
1670 .iter()
1671 .nth(idx)
1672 .map(|(n, _)| alloc::string::String::from(n))
1673 }
1674
1675 /// v7.37.15 (Phase B / C / E) — current per-row visibility
1676 /// snapshot for in-engine scans. Captures the live writer-
1677 /// version cursor + active-writer set; readers built from this
1678 /// Snapshot see committed state through the moment of capture
1679 /// and DO NOT observe uncommitted writes still inside
1680 /// `active_writer_versions`.
1681 ///
1682 /// Phase E: if there's an explicit transaction in flight under
1683 /// REPEATABLE READ or SERIALIZABLE isolation, returns the
1684 /// snapshot the tx cached at BEGIN time — every statement in
1685 /// the tx sees the same coherent prior-committed view. READ
1686 /// COMMITTED (the default) returns a fresh snapshot per call,
1687 /// matching PG's per-statement visibility semantics.
1688 ///
1689 /// `oldest_active = version` when no writer is in flight (== no
1690 /// dead row could still be observed); else == min of active
1691 /// versions (vacuum-floor).
1692 #[must_use]
1693 pub fn current_snapshot(&self) -> spg_storage::snapshot::Snapshot {
1694 // v7.39 (round 297, E3 Phase 1b) — carry the SKIP LOCKED
1695 // exclusions on the snapshot. Every row source threads a
1696 // snapshot through `is_row_visible`, so this is the one place
1697 // that cannot be routed around; adding the filter per scan site
1698 // missed the live path three times.
1699 let locked_out = self.lock_skip_rows.as_ref().and_then(|(t, set)| {
1700 self.active_catalog()
1701 .get(t)
1702 .map(|tbl| (tbl.rel_id(), set.clone()))
1703 });
1704 let mut snap = self.current_snapshot_inner();
1705 snap.locked_out = locked_out;
1706 snap
1707 }
1708
1709 fn current_snapshot_inner(&self) -> spg_storage::snapshot::Snapshot {
1710 // Phase E — if we're inside a RR/SER tx, return its
1711 // cached snapshot so the whole tx sees one frozen view.
1712 if let Some(tx_id) = self.current_tx
1713 && let Some(state) = self.tx_catalogs.get(&tx_id)
1714 && let Some(s) = state.cached_snapshot.as_ref()
1715 {
1716 return s.clone();
1717 }
1718 // v7.37.15 (Phase C.3, step 1) — carry the current tx's writer
1719 // version as the snapshot's `tx_id` so the visibility gate's
1720 // self-write branch (`visible` step 1) recognises rows this
1721 // transaction stamped (`xmin == v`, with `v` in
1722 // `active_writer_versions`). Without this the tx's own
1723 // uncommitted rows fall to the in-progress step and become
1724 // invisible to itself on the gated read paths. Autocommit reads
1725 // (no `tx_writer_versions` entry) keep `tx_id = 0`.
1726 let reader_tx_id = self
1727 .current_tx
1728 .and_then(|t| self.tx_writer_versions.get(&t).copied())
1729 .unwrap_or(0);
1730 let version = spg_storage::row_header::current_version();
1731 if self.active_writer_versions.is_empty() {
1732 // Hot path: no writer in flight. Snapshot::unbounded()
1733 // would also work, but pinning to the live cursor
1734 // means the snapshot's oldest_active is accurate
1735 // (= version) so subsequent vacuum can advance.
1736 return spg_storage::snapshot::Snapshot::new(
1737 version,
1738 spg_storage::snapshot::InProgressSet::empty(),
1739 version,
1740 reader_tx_id,
1741 );
1742 }
1743 let sorted: alloc::vec::Vec<u64> = self.active_writer_versions.iter().copied().collect();
1744 let oldest = *sorted.first().unwrap_or(&version);
1745 spg_storage::snapshot::Snapshot::new(
1746 version,
1747 spg_storage::snapshot::InProgressSet::from_sorted(sorted),
1748 oldest,
1749 reader_tx_id,
1750 )
1751 }
1752
1753 /// v7.37.15 (Phase C) — allocate the next writer version AND
1754 /// add it to the in-flight set so concurrent snapshots hide
1755 /// the resulting writes until [`Self::commit_writer_version`]
1756 /// removes the entry. Returns the allocated version so the
1757 /// writer can stamp it on `xmin` / `xmax`.
1758 pub fn begin_writer_version(&mut self) -> u64 {
1759 let v = spg_storage::row_header::next_version();
1760 self.active_writer_versions.insert(v);
1761 v
1762 }
1763
1764 /// v7.37.15 (Phase C) — mark a previously-allocated writer
1765 /// version as committed. Subsequent snapshots stop including
1766 /// it in `in_progress`, so the writes the version stamped
1767 /// become visible to new readers.
1768 ///
1769 /// No-op if the version was never allocated; matches PG's
1770 /// idempotent `TransactionIdCommitTree` semantics.
1771 pub fn commit_writer_version(&mut self, v: u64) {
1772 self.active_writer_versions.remove(&v);
1773 }
1774
1775 /// v7.37.15 (Phase C.2) — mark a previously-allocated writer
1776 /// version as ABORTED (rolled back). Removes it from the in-flight
1777 /// set and records it in `aborted_versions` so the visibility
1778 /// oracle ([`Self::xact_status`]) reports `Aborted` rather than
1779 /// silently treating it as committed once it leaves the in-flight
1780 /// set. Phase C.3's in-place write path relies on this: a
1781 /// rolled-back version's xmin/xmax stamps stay physically present
1782 /// until vacuum reclaims them, and readers must NOT see them.
1783 ///
1784 /// Idempotent; a no-op if the version was never allocated.
1785 pub fn abort_writer_version(&mut self, v: u64) {
1786 self.active_writer_versions.remove(&v);
1787 self.aborted_versions.insert(v);
1788 }
1789
1790 /// v7.37.15 (Phase C.2) — the visibility oracle's terminal-status
1791 /// lookup for one version. In-flight if still allocated, Aborted
1792 /// if it rolled back, otherwise Committed (the default for a
1793 /// version that left the in-flight set the normal way, and for
1794 /// every frozen / pruned old version the engine no longer tracks).
1795 ///
1796 /// `aborted_versions` is bounded by pruning below `oldest_active`
1797 /// during vacuum (Phase D): once no live snapshot can still see an
1798 /// aborted version's stamps, its entry is dropped. Until Phase D
1799 /// lands the set only grows with rolled-back transactions — noted
1800 /// as a never-die follow-up, not a steady-state leak on the
1801 /// commit path.
1802 #[must_use]
1803 pub fn xact_status(&self, v: u64) -> spg_storage::snapshot::XactStatus {
1804 use spg_storage::snapshot::XactStatus;
1805 if self.active_writer_versions.contains(&v) {
1806 XactStatus::InProgress
1807 } else if self.aborted_versions.contains(&v) {
1808 XactStatus::Aborted
1809 } else {
1810 XactStatus::Committed
1811 }
1812 }
1813
1814 /// v7.37.15 (Phase C.4) — acquire a tuple lock on a stable
1815 /// `(RelId, RowId)` for writer `version`. The in-place write path
1816 /// (C.3) calls this before stamping xmax; `SELECT ... FOR UPDATE`
1817 /// wires here via the parser's lock-strength clause (C.4). Returns
1818 /// the [`LockOutcome`](crate::locks::LockOutcome) the caller acts on
1819 /// (grant / park / skip / fail / deadlock-abort).
1820 pub fn acquire_row_lock(
1821 &mut self,
1822 rel: spg_storage::row_header::RelId,
1823 row: spg_storage::row_header::RowId,
1824 mode: crate::locks::LockMode,
1825 version: u64,
1826 policy: crate::locks::WaitPolicy,
1827 ) -> crate::locks::LockOutcome {
1828 self.locks.acquire(rel, row, mode, version, policy)
1829 }
1830
1831 /// v7.37.15 (Phase C.4) — release every lock + wait held by
1832 /// `version` at transaction end. Called from `exec_commit` /
1833 /// `exec_rollback` alongside the writer-version bookkeeping.
1834 pub fn release_tx_locks(&mut self, version: u64) {
1835 self.locks.release_all(version);
1836 }
1837
1838 /// v7.37.15 (Phase C.4) — number of rows currently locked, for the
1839 /// `pg_locks` enumeration and tests.
1840 #[must_use]
1841 pub fn locked_row_count(&self) -> usize {
1842 self.locks.locked_row_count()
1843 }
1844
1845 /// v7.37.15 (Phase C.3) — is the in-place MVCC write path enabled?
1846 /// `false` (default) keeps legacy physical DELETE/UPDATE. The C.3
1847 /// writers consult this to choose tombstone-vs-physical.
1848 #[must_use]
1849 pub fn mvcc_inplace(&self) -> bool {
1850 self.mvcc_inplace
1851 }
1852
1853 /// v7.37.15 (Phase C.3) — enable/disable the in-place MVCC write
1854 /// path. Called by the host after reading `SPG_MVCC_INPLACE` (the
1855 /// `no_std` engine can't read the environment itself). Off until
1856 /// the write path is proven against PG18 differential tests.
1857 pub fn set_mvcc_inplace(&mut self, on: bool) {
1858 self.mvcc_inplace = on;
1859 }
1860
1861 /// v7.39 (parallel-agg P0) — inject the host's parallel executor
1862 /// (see [`ParallelRunner`]). Called once at host startup; the
1863 /// engine stays single-threaded without it.
1864 /// v7.39 (pg_stat knife A) — inject the host's live backend count.
1865 pub fn set_backend_count_fn(&mut self, f: BackendCountFn) {
1866 self.backend_count_fn = Some(f);
1867 }
1868
1869 /// v7.39 (read01 pgstatfuncs.c) — inject the host's calling-connection
1870 /// identity for pg_backend_pid().
1871 /// v7.39 (round 476) — register the WAL byte-position provider.
1872 pub fn set_wal_lsn_fn(&mut self, f: WalLsnFn) {
1873 self.wal_lsn_fn = Some(f);
1874 }
1875
1876 pub fn set_backend_pid_fn(&mut self, f: BackendPidFn) {
1877 self.backend_pid_fn = Some(f);
1878 }
1879
1880 /// v7.39 (round 318, V51) — inject the host's connection-control hook,
1881 /// so `pg_cancel_backend` / `pg_terminate_backend` / `KILL` act instead
1882 /// of answering a constant.
1883 pub fn set_backend_signal_fn(&mut self, f: BackendSignalFn) {
1884 self.backend_signal_fn = Some(f);
1885 }
1886
1887 /// v7.39 (round 786, T35 Phase A) — install the host's spill-run
1888 /// factory. Without one the engine cannot spill and a sort that
1889 /// outgrows `max_query_bytes` keeps refusing, which is exactly the
1890 /// behaviour every caller has today.
1891 pub fn set_temp_run_factory(&mut self, f: crate::TempRunFactory) {
1892 self.temp_run_factory = Some(f);
1893 }
1894
1895 /// Whether spilling is available in this process.
1896 #[must_use]
1897 pub fn can_spill(&self) -> bool {
1898 self.temp_run_factory.is_some()
1899 }
1900
1901 /// v7.39 (round 786) — open a fresh spill run, or `None` when no
1902 /// host factory is installed. Phase B's run generation calls this;
1903 /// it lives here so the `None` path stays a single decision point.
1904 pub(crate) fn open_temp_run(
1905 &self,
1906 ) -> Option<Result<alloc::boxed::Box<dyn crate::TempRun>, crate::TempStoreError>> {
1907 self.temp_run_factory.map(|f| f())
1908 }
1909
1910 /// v7.39 (tz epic) — inject the host's IANA timezone lookups
1911 /// (spg-tzif's fn family on std hosts).
1912 pub fn set_tz_fns(
1913 &mut self,
1914 offset: TzOffsetFn,
1915 localize: TzLocalizeFn,
1916 canon: TzCanonFn,
1917 abbrev: TzAbbrevFn,
1918 ) {
1919 self.tz_offset_fn = Some(offset);
1920 self.tz_localize_fn = Some(localize);
1921 self.tz_canon_fn = Some(canon);
1922 self.tz_abbrev_fn = Some(abbrev);
1923 }
1924
1925 /// v7.39 (round 502) — the zone enumerator behind `pg_timezone_names`.
1926 /// Separate from `set_tz_fns` so an embedder that already calls that
1927 /// one keeps compiling.
1928 pub fn set_tz_all_fn(&mut self, all: TzAllFn) {
1929 self.tz_all_fn = Some(all);
1930 }
1931
1932 /// Every zone the host knows at `utc_micros`; empty without a hook.
1933 pub(crate) fn tz_all_at(
1934 &self,
1935 utc_micros: i64,
1936 ) -> alloc::vec::Vec<(alloc::string::String, alloc::string::String, i64, bool)> {
1937 self.tz_all_fn
1938 .map_or_else(alloc::vec::Vec::new, |f| f(utc_micros))
1939 }
1940
1941 pub fn set_parallel_runner(&mut self, runner: alloc::sync::Arc<dyn ParallelRunner>) {
1942 self.parallel_runner = ParallelRunnerSlot(Some(runner));
1943 }
1944
1945 /// v7.37.15 (Phase C) — allocate a fresh version number for
1946 /// the next write. Always strictly monotonic + process-wide
1947 /// shared so concurrent engines on the same process agree on
1948 /// "tx 17 commits before tx 18". Phase C writer paths call
1949 /// this once per INSERT / UPDATE / DELETE statement to obtain
1950 /// the version they'll stamp on the new row's `xmin` (or the
1951 /// existing row's `xmax`).
1952 ///
1953 /// Returns [`XMIN_FROZEN`] when MVCC stamping is intentionally
1954 /// off (legacy `in_memory` flow / WAL replay): the writer
1955 /// then takes the legacy frozen-insert short-circuit path
1956 /// inside `Table::insert_with_xmin`.
1957 #[must_use]
1958 pub fn next_writer_version(&self) -> u64 {
1959 spg_storage::row_header::next_version()
1960 }
1961
1962 /// v7.37.15 (Phase C) — version a writer should stamp on
1963 /// rows produced by the current statement. Inside an explicit
1964 /// transaction the version is the tx's pre-allocated one (so
1965 /// every statement in the tx commits atomically at COMMIT);
1966 /// in autocommit it allocates a fresh version per statement.
1967 ///
1968 /// This is the canonical helper engine writers should call
1969 /// — using it instead of `next_writer_version` ensures
1970 /// explicit-tx semantics where every row produced by the tx
1971 /// shares one xmin and concurrent readers don't see partial
1972 /// state until COMMIT.
1973 ///
1974 /// v7.37.15 (Epic W slice 2) — takes `&mut self` so the autocommit
1975 /// branch can **memoize** its freshly-minted version in
1976 /// `stmt_writer_version`. `next_writer_version()` is a `fetch_add`,
1977 /// so without memoization a second call within one statement (the
1978 /// redo drain post-stamps the captured `RowChange`s) would allocate
1979 /// a *different* version than the writes used. Memoizing makes the
1980 /// value stable for the statement's lifetime; it is reset per
1981 /// `execute_in_with_cancel`, so the counter still advances exactly
1982 /// once per autocommit statement — identical to before.
1983 pub fn writer_version_for_current_stmt(&mut self) -> u64 {
1984 if let Some(tx_id) = self.current_tx
1985 && let Some(&v) = self.tx_writer_versions.get(&tx_id)
1986 {
1987 return v;
1988 }
1989 // Autocommit shape: fresh version, immediately "committed"
1990 // (no entry in active_writer_versions, so subsequent
1991 // readers see the row). Memoized for the statement so the
1992 // redo drain reads back the same version the writes used.
1993 if let Some(v) = self.stmt_writer_version {
1994 return v;
1995 }
1996 let v = self.next_writer_version();
1997 self.stmt_writer_version = Some(v);
1998 v
1999 }
2000
2001 /// Construct an engine restored from a previously-snapshotted catalog
2002 /// (see `snapshot()`).
2003 pub fn restore(catalog: Catalog) -> Self {
2004 Self {
2005 lock_skip_rows: None,
2006 catalog,
2007 parallel_runner: ParallelRunnerSlot::default(),
2008 tx_catalogs: BTreeMap::new(),
2009 table_last_commit: BTreeMap::new(),
2010 commit_seq: 0,
2011 current_tx: None,
2012 backslash_escapes: false,
2013 mysql_strict: true,
2014 lo_descriptors: BTreeMap::new(),
2015 lo_next_fd: 0,
2016 prepared_statements: alloc::collections::BTreeMap::new(),
2017 current_session: 0,
2018 sessions: BTreeMap::new(),
2019 advisory_locks: BTreeMap::new(),
2020 last_sequence_used: None,
2021 seq_currvals: alloc::collections::BTreeMap::new(),
2022 next_tx_id: 1,
2023 active_writer_versions: BTreeSet::new(),
2024 aborted_versions: BTreeSet::new(),
2025 locks: crate::locks::LockTable::new(),
2026 mvcc_inplace: !cfg!(feature = "mvcc-inplace-off"),
2027 autovacuum: true,
2028 autovacuum_inline: true,
2029 tx_writer_versions: BTreeMap::new(),
2030 stmt_writer_version: None,
2031 clock: None,
2032 salt_fn: None,
2033 max_query_rows: None,
2034 max_query_bytes: None,
2035 temp_run_factory: None,
2036 users: UserStore::new(),
2037 publications: publications::Publications::new(),
2038 subscriptions: subscriptions::Subscriptions::new(),
2039 statistics: statistics::Statistics::new(),
2040 plan_cache: plan_cache::PlanCache::new(),
2041 query_stats: query_stats::QueryStats::new(),
2042 activity_provider: None,
2043 audit_chain_provider: None,
2044 audit_verifier: None,
2045 slow_query_threshold_us: None,
2046 slow_query_logger: None,
2047 session_params: BTreeMap::new(),
2048 cursors: BTreeMap::new(),
2049 last_insert_id: core::sync::atomic::AtomicI64::new(0),
2050 row_count: 0,
2051 user_vars: BTreeMap::new(),
2052 temp_tables: BTreeSet::new(),
2053 temp_sequences: BTreeSet::new(),
2054 temp_views: BTreeSet::new(),
2055 listen_channels: BTreeSet::new(),
2056 tx_pending_notifies: Vec::new(),
2057 delivered_notifies: Vec::new(),
2058 pending_notices: Vec::new(),
2059 spill_stats: crate::tempstore::SpillStats::default(),
2060 xact_commit: core::sync::atomic::AtomicU64::new(0),
2061 xact_rollback: core::sync::atomic::AtomicU64::new(0),
2062 backend_count_fn: None,
2063 backend_pid_fn: None,
2064 wal_lsn_fn: None,
2065 backend_signal_fn: None,
2066 tz_offset_fn: None,
2067 tz_localize_fn: None,
2068 tz_canon_fn: None,
2069 tz_abbrev_fn: None,
2070 tz_all_fn: None,
2071 stat_tup_inserted: 0,
2072 table_write_stats: alloc::collections::BTreeMap::new(),
2073 commit_epoch: 0,
2074 stat_tup_updated: 0,
2075 stat_tup_deleted: 0,
2076 local_guc_saves: Vec::new(),
2077 render_style: crate::eval::RenderStyle::default(),
2078 savepoint_guc_marks: Vec::new(),
2079 trigger_recursion_depth: 0,
2080 rule_rewrite_active: false,
2081 foreign_key_checks: true,
2082 meta_views_materialised: false,
2083 pending_foreign_keys: Vec::new(),
2084 env_cfg: testkit::EnvConfig::default(),
2085 #[cfg(feature = "injection-points")]
2086 injection_store: alloc::sync::Arc::new(
2087 crate::testkit::injection::InjectionStore::default(),
2088 ),
2089 redo_capture: false,
2090 current_isolation_level: spg_sql::ast::IsolationLevel::ReadCommitted,
2091 last_redo: Vec::new(),
2092 table_change_seq: alloc::collections::BTreeMap::new(),
2093 matview_refresh_watermark: alloc::collections::BTreeMap::new(),
2094 matview_maintainable: alloc::collections::BTreeMap::new(),
2095 matview_delta_buf: alloc::collections::BTreeMap::new(),
2096 matview_delta_overflow: alloc::collections::BTreeSet::new(),
2097 matview_row_map: alloc::collections::BTreeMap::new(),
2098 }
2099 }
2100
2101 /// Restore an engine + user table from a v4.1 envelope produced
2102 /// by `snapshot_with_users()`. Falls back to plain catalog-only
2103 /// restore if the envelope magic isn't present (so v3.x snapshot
2104 /// files still load). v6.1.2 adds the optional publications
2105 /// trailer (envelope v3); a v1/v2 envelope deserialises to an
2106 /// empty publication table.
2107 pub fn restore_envelope(buf: &[u8]) -> Result<Self, EngineError> {
2108 match split_envelope(buf) {
2109 EnvelopeParse::Pair {
2110 catalog: catalog_bytes,
2111 users: user_bytes,
2112 publications: pub_bytes,
2113 subscriptions: sub_bytes,
2114 statistics: stats_bytes,
2115 } => {
2116 let mut catalog =
2117 Catalog::deserialize(catalog_bytes).map_err(EngineError::Storage)?;
2118 crate::ddl::rebuild_all_excl_indexes(&mut catalog);
2119 let users = users::deserialize_users(user_bytes)
2120 .map_err(|e| EngineError::Unsupported(alloc::format!("users restore: {e}")))?;
2121 let publications = match pub_bytes {
2122 Some(b) => publications::Publications::deserialize(b).map_err(|e| {
2123 EngineError::Unsupported(alloc::format!("publications restore: {e:?}"))
2124 })?,
2125 None => publications::Publications::new(),
2126 };
2127 let subscriptions = match sub_bytes {
2128 Some(b) => subscriptions::Subscriptions::deserialize(b).map_err(|e| {
2129 EngineError::Unsupported(alloc::format!("subscriptions restore: {e:?}"))
2130 })?,
2131 None => subscriptions::Subscriptions::new(),
2132 };
2133 let statistics = match stats_bytes {
2134 Some(b) => statistics::Statistics::deserialize(b).map_err(|e| {
2135 EngineError::Unsupported(alloc::format!("statistics restore: {e:?}"))
2136 })?,
2137 None => statistics::Statistics::new(),
2138 };
2139 Ok(Self {
2140 lock_skip_rows: None,
2141 catalog,
2142 parallel_runner: ParallelRunnerSlot::default(),
2143 tx_catalogs: BTreeMap::new(),
2144 table_last_commit: BTreeMap::new(),
2145 commit_seq: 0,
2146 current_tx: None,
2147 backslash_escapes: false,
2148 mysql_strict: true,
2149 lo_descriptors: BTreeMap::new(),
2150 lo_next_fd: 0,
2151 prepared_statements: alloc::collections::BTreeMap::new(),
2152 current_session: 0,
2153 sessions: BTreeMap::new(),
2154 advisory_locks: BTreeMap::new(),
2155 last_sequence_used: None,
2156 seq_currvals: alloc::collections::BTreeMap::new(),
2157 next_tx_id: 1,
2158 active_writer_versions: BTreeSet::new(),
2159 aborted_versions: BTreeSet::new(),
2160 locks: crate::locks::LockTable::new(),
2161 mvcc_inplace: !cfg!(feature = "mvcc-inplace-off"),
2162 autovacuum: true,
2163 autovacuum_inline: true,
2164 tx_writer_versions: BTreeMap::new(),
2165 stmt_writer_version: None,
2166 clock: None,
2167 salt_fn: None,
2168 max_query_rows: None,
2169 max_query_bytes: None,
2170 temp_run_factory: None,
2171 users,
2172 publications,
2173 subscriptions,
2174 statistics,
2175 plan_cache: plan_cache::PlanCache::new(),
2176 query_stats: query_stats::QueryStats::new(),
2177 activity_provider: None,
2178 audit_chain_provider: None,
2179 audit_verifier: None,
2180 slow_query_threshold_us: None,
2181 slow_query_logger: None,
2182 session_params: BTreeMap::new(),
2183 cursors: BTreeMap::new(),
2184 last_insert_id: core::sync::atomic::AtomicI64::new(0),
2185 row_count: 0,
2186 user_vars: BTreeMap::new(),
2187 temp_tables: BTreeSet::new(),
2188 temp_sequences: BTreeSet::new(),
2189 temp_views: BTreeSet::new(),
2190 listen_channels: BTreeSet::new(),
2191 tx_pending_notifies: Vec::new(),
2192 delivered_notifies: Vec::new(),
2193 pending_notices: Vec::new(),
2194 spill_stats: crate::tempstore::SpillStats::default(),
2195 xact_commit: core::sync::atomic::AtomicU64::new(0),
2196 xact_rollback: core::sync::atomic::AtomicU64::new(0),
2197 backend_count_fn: None,
2198 backend_pid_fn: None,
2199 wal_lsn_fn: None,
2200 backend_signal_fn: None,
2201 tz_offset_fn: None,
2202 tz_localize_fn: None,
2203 tz_canon_fn: None,
2204 tz_abbrev_fn: None,
2205 tz_all_fn: None,
2206 stat_tup_inserted: 0,
2207 table_write_stats: alloc::collections::BTreeMap::new(),
2208 commit_epoch: 0,
2209 stat_tup_updated: 0,
2210 stat_tup_deleted: 0,
2211 local_guc_saves: Vec::new(),
2212 render_style: crate::eval::RenderStyle::default(),
2213 savepoint_guc_marks: Vec::new(),
2214 trigger_recursion_depth: 0,
2215 rule_rewrite_active: false,
2216 foreign_key_checks: true,
2217 meta_views_materialised: false,
2218 pending_foreign_keys: Vec::new(),
2219 env_cfg: testkit::EnvConfig::default(),
2220 #[cfg(feature = "injection-points")]
2221 injection_store: alloc::sync::Arc::new(
2222 crate::testkit::injection::InjectionStore::default(),
2223 ),
2224 redo_capture: false,
2225 current_isolation_level: spg_sql::ast::IsolationLevel::ReadCommitted,
2226 last_redo: Vec::new(),
2227 table_change_seq: alloc::collections::BTreeMap::new(),
2228 matview_refresh_watermark: alloc::collections::BTreeMap::new(),
2229 matview_maintainable: alloc::collections::BTreeMap::new(),
2230 matview_delta_buf: alloc::collections::BTreeMap::new(),
2231 matview_delta_overflow: alloc::collections::BTreeSet::new(),
2232 matview_row_map: alloc::collections::BTreeMap::new(),
2233 })
2234 }
2235 EnvelopeParse::CrcMismatch { expected, computed } => {
2236 Err(EngineError::Storage(StorageError::Corrupt(alloc::format!(
2237 "snapshot envelope CRC32 mismatch (expected={expected:#010x}, computed={computed:#010x})"
2238 ))))
2239 }
2240 EnvelopeParse::Bare => {
2241 let mut catalog = Catalog::deserialize(buf).map_err(EngineError::Storage)?;
2242 crate::ddl::rebuild_all_excl_indexes(&mut catalog);
2243 Ok(Self::restore(catalog))
2244 }
2245 }
2246 }
2247
2248 pub const fn users(&self) -> &UserStore {
2249 &self.users
2250 }
2251
2252 /// Builder: attach a wall clock so `NOW()` / `CURRENT_TIMESTAMP` /
2253 /// `CURRENT_DATE` evaluate to a real value instead of erroring out.
2254 /// v7.39 (round 279) — announce which connection is about to run.
2255 /// The server calls this before every statement; embedded hosts
2256 /// never do and stay on session 0.
2257 ///
2258 /// Swapping parks the outgoing connection's state and installs the
2259 /// incoming one's, creating it on first sight. The plan cache is
2260 /// cleared because the string-literal dialect is part of what
2261 /// swaps and the same SQL text lexes differently under it.
2262 pub fn set_current_session(&mut self, id: u32) {
2263 if id == self.current_session {
2264 return;
2265 }
2266 let outgoing = SessionBag {
2267 session_params: core::mem::take(&mut self.session_params),
2268 backslash_escapes: self.backslash_escapes,
2269 mysql_strict: self.mysql_strict,
2270 prepared_statements: core::mem::take(&mut self.prepared_statements),
2271 lo_descriptors: core::mem::take(&mut self.lo_descriptors),
2272 lo_next_fd: self.lo_next_fd,
2273 cursors: core::mem::take(&mut self.cursors),
2274 last_insert_id: self
2275 .last_insert_id
2276 .load(core::sync::atomic::Ordering::Relaxed),
2277 row_count: self.row_count,
2278 user_vars: core::mem::take(&mut self.user_vars),
2279 temp_tables: core::mem::take(&mut self.temp_tables),
2280 temp_sequences: core::mem::take(&mut self.temp_sequences),
2281 temp_views: core::mem::take(&mut self.temp_views),
2282 seq_currvals: core::mem::take(&mut self.seq_currvals),
2283 last_sequence_used: self.last_sequence_used.take(),
2284 isolation_level: self.current_isolation_level,
2285 };
2286 self.sessions.insert(self.current_session, outgoing);
2287 let incoming = self.sessions.remove(&id).unwrap_or_default();
2288 self.session_params = incoming.session_params;
2289 self.backslash_escapes = incoming.backslash_escapes;
2290 self.mysql_strict = incoming.mysql_strict;
2291 self.prepared_statements = incoming.prepared_statements;
2292 self.lo_descriptors = incoming.lo_descriptors;
2293 self.lo_next_fd = incoming.lo_next_fd;
2294 self.cursors = incoming.cursors;
2295 self.last_insert_id.store(
2296 incoming.last_insert_id,
2297 core::sync::atomic::Ordering::Relaxed,
2298 );
2299 self.row_count = incoming.row_count;
2300 self.user_vars = incoming.user_vars;
2301 self.temp_tables = incoming.temp_tables;
2302 self.temp_sequences = incoming.temp_sequences;
2303 self.temp_views = incoming.temp_views;
2304 self.seq_currvals = incoming.seq_currvals;
2305 self.last_sequence_used = incoming.last_sequence_used;
2306 self.current_isolation_level = incoming.isolation_level;
2307 self.current_session = id;
2308 // The incoming session's temp namespace must be live before its very
2309 // first statement resolves a name.
2310 self.refresh_temp_prefix();
2311 self.plan_cache.clear();
2312 }
2313
2314 /// v7.39 (round 436) — the catalog-name prefix session `id` stores its
2315 /// TEMPORARY tables under. Mirrors PG's per-session `pg_temp_N` schema;
2316 /// the leading underscores keep it out of any name a client can write.
2317 fn temp_prefix_for(id: u32) -> String {
2318 alloc::format!("__spg_temp_{id}__")
2319 }
2320
2321 /// The catalog name this session's TEMPORARY table `logical` takes.
2322 pub(crate) fn session_temp_name(&self, logical: &str) -> String {
2323 alloc::format!("{}{logical}", Self::temp_prefix_for(self.current_session))
2324 }
2325
2326 /// v7.39 (round 436) — point every catalog this session can reach at its
2327 /// temp namespace, or at none when it owns no temporary tables (so a
2328 /// session that never made one pays a single `Option` check per lookup).
2329 /// Both the committed catalog and any open transaction's shadow are set:
2330 /// a temp table created inside a transaction must resolve there too.
2331 pub(crate) fn refresh_temp_prefix(&mut self) {
2332 let prefix = if self.temp_tables.is_empty()
2333 && self.temp_sequences.is_empty()
2334 && self.temp_views.is_empty()
2335 {
2336 None
2337 } else {
2338 Some(Self::temp_prefix_for(self.current_session))
2339 };
2340 self.catalog.set_temp_prefix(prefix.clone());
2341 for shadow in self.tx_catalogs.values_mut() {
2342 shadow.catalog.set_temp_prefix(prefix.clone());
2343 }
2344 }
2345
2346 /// v7.39 (round 279) — a connection has gone away: drop its parked
2347 /// state and release every advisory lock it still held, which is
2348 /// what PG does at backend exit.
2349 pub fn end_session(&mut self, id: u32) {
2350 // v7.39 (round 436) — a TEMPORARY table dies with its session, in
2351 // both PG and MySQL. Done before the bag is dropped, since the bag
2352 // is what knows which tables the session owns.
2353 let owned: Vec<String> = if id == self.current_session {
2354 self.temp_tables.iter().cloned().collect()
2355 } else {
2356 self.sessions
2357 .get(&id)
2358 .map(|b| b.temp_tables.iter().cloned().collect())
2359 .unwrap_or_default()
2360 };
2361 // v7.39 (round 469) — the same for TEMPORARY sequences and views,
2362 // which PG also drops at backend exit.
2363 let owned_seqs: Vec<String> = if id == self.current_session {
2364 self.temp_sequences.iter().cloned().collect()
2365 } else {
2366 self.sessions
2367 .get(&id)
2368 .map(|b| b.temp_sequences.iter().cloned().collect())
2369 .unwrap_or_default()
2370 };
2371 let owned_views: Vec<String> = if id == self.current_session {
2372 self.temp_views.iter().cloned().collect()
2373 } else {
2374 self.sessions
2375 .get(&id)
2376 .map(|b| b.temp_views.iter().cloned().collect())
2377 .unwrap_or_default()
2378 };
2379 if !owned.is_empty() || !owned_seqs.is_empty() || !owned_views.is_empty() {
2380 let prefix = Self::temp_prefix_for(id);
2381 for logical in owned {
2382 let mangled = alloc::format!("{prefix}{logical}");
2383 self.catalog.drop_table(&mangled);
2384 }
2385 for logical in owned_seqs {
2386 let mangled = alloc::format!("{prefix}{logical}");
2387 self.catalog.drop_sequence(&mangled);
2388 }
2389 for logical in owned_views {
2390 let mangled = alloc::format!("{prefix}{logical}");
2391 self.catalog.drop_view(&mangled);
2392 }
2393 if id == self.current_session {
2394 self.temp_tables.clear();
2395 self.temp_sequences.clear();
2396 self.temp_views.clear();
2397 self.refresh_temp_prefix();
2398 }
2399 }
2400 self.sessions.remove(&id);
2401 self.advisory_locks.retain(|_, (owner, _)| *owner != id);
2402 if id == self.current_session {
2403 self.session_params.clear();
2404 self.prepared_statements.clear();
2405 self.backslash_escapes = false;
2406 self.mysql_strict = true;
2407 self.lo_descriptors.clear();
2408 self.lo_next_fd = 0;
2409 self.cursors.clear();
2410 self.current_session = 0;
2411 }
2412 }
2413
2414 /// v7.39 (round 302, V15) — force the current session's string-literal
2415 /// dialect. A MySQL-protocol connection defaults to MySQL semantics
2416 /// (backslash is an escape: `'\n'` is a newline), which PG's own
2417 /// default (`standard_conforming_strings = on`) does not do. The
2418 /// mysql-wire shim calls this once, right after installing its
2419 /// session, so a client that never sends `SET sql_mode` still gets
2420 /// MySQL string handling; a later `SET sql_mode='NO_BACKSLASH_ESCAPES'`
2421 /// flips it back through the normal SET path. Clearing the plan cache
2422 /// mirrors [`set_current_session`] — the same SQL text lexes
2423 /// differently once the flag moves.
2424 pub fn set_backslash_escapes(&mut self, flag: bool) {
2425 if flag != self.backslash_escapes {
2426 self.backslash_escapes = flag;
2427 self.plan_cache.clear();
2428 }
2429 }
2430
2431 /// v7.39 (round 279) — take an advisory lock. Returns false only
2432 /// when ANOTHER session holds it; re-taking one this session
2433 /// already holds bumps a depth counter, as in PG.
2434 pub(crate) fn advisory_try_lock(&mut self, key: i64) -> bool {
2435 let me = self.current_session;
2436 match self.advisory_locks.get_mut(&key) {
2437 Some((owner, depth)) if *owner == me => {
2438 *depth += 1;
2439 true
2440 }
2441 Some(_) => false,
2442 None => {
2443 self.advisory_locks.insert(key, (me, 1));
2444 true
2445 }
2446 }
2447 }
2448
2449 /// Release one level. False when this session does not hold it —
2450 /// PG answers false and emits a warning; SPG answers false.
2451 pub(crate) fn advisory_unlock(&mut self, key: i64) -> bool {
2452 let me = self.current_session;
2453 match self.advisory_locks.get_mut(&key) {
2454 Some((owner, depth)) if *owner == me => {
2455 *depth -= 1;
2456 if *depth == 0 {
2457 self.advisory_locks.remove(&key);
2458 }
2459 true
2460 }
2461 _ => false,
2462 }
2463 }
2464
2465 /// Release every advisory lock this session holds.
2466 pub(crate) fn advisory_unlock_all(&mut self) {
2467 let me = self.current_session;
2468 self.advisory_locks.retain(|_, (owner, _)| *owner != me);
2469 }
2470
2471 /// v7.39 (round 417) — the current session's id (for MySQL
2472 /// `IS_USED_LOCK`, which reports the connection that holds a lock).
2473 /// v7.39 (round 430) — read one of this session's MySQL USER
2474 /// variables. `None` when it was never set, which the caller turns
2475 /// into NULL (MariaDB reads an unset user variable as NULL).
2476 pub(crate) fn user_var(&self, name: &str) -> Option<&spg_storage::Value<'static>> {
2477 self.user_vars.get(name)
2478 }
2479
2480 pub(crate) const fn current_session_id(&self) -> u32 {
2481 self.current_session
2482 }
2483
2484 /// v7.39 (round 417) — who holds an advisory-lock key (any session id),
2485 /// or `None` when nobody holds it. Used by MySQL `IS_USED_LOCK` and to
2486 /// separate `RELEASE_LOCK`'s "not held by anyone" (returns NULL) from
2487 /// "held by someone else" (returns 0).
2488 pub(crate) fn advisory_holder(&self, key: i64) -> Option<u32> {
2489 self.advisory_locks.get(&key).map(|(owner, _)| *owner)
2490 }
2491
2492 /// v7.39 (round 417) — MySQL `RELEASE_ALL_LOCKS()` returns the number of
2493 /// locks it released; PG's `pg_advisory_unlock_all()` returns void.
2494 pub(crate) fn advisory_unlock_all_count(&mut self) -> i32 {
2495 let me = self.current_session;
2496 // Total depth held by this session, so re-locked keys count as many.
2497 let mut n: i32 = 0;
2498 for (_, (owner, depth)) in &self.advisory_locks {
2499 if *owner == me {
2500 n = n.saturating_add(*depth as i32);
2501 }
2502 }
2503 self.advisory_locks.retain(|_, (owner, _)| *owner != me);
2504 n
2505 }
2506
2507 #[must_use]
2508 pub const fn with_clock(mut self, clock: ClockFn) -> Self {
2509 self.clock = Some(clock);
2510 self
2511 }
2512
2513 /// Builder: attach an OS-backed RNG for per-user password salts.
2514 /// The host (`spg-server`) typically wires this to `/dev/urandom`.
2515 #[must_use]
2516 pub const fn with_salt_fn(mut self, f: SaltFn) -> Self {
2517 self.salt_fn = Some(f);
2518 self
2519 }
2520
2521 /// v7.38 元机制 D — install a frozen [`testkit::EnvConfig`] snapshot.
2522 ///
2523 /// Hosts (spg-server, spg-embedded, tests) call this once at engine
2524 /// init with either `EnvConfig::from_env()` (production-with-test-vars)
2525 /// or `EnvConfig::builder()....build()` (programmatic). After
2526 /// construction the engine never reads env vars; all test-mode
2527 /// behaviour flows through `self.env_cfg()`.
2528 #[must_use]
2529 pub fn with_env_cfg(mut self, env_cfg: testkit::EnvConfig) -> Self {
2530 self.env_cfg = env_cfg;
2531 self
2532 }
2533
2534 /// v7.38 元机制 D — frozen test-mode GUC snapshot. Hot paths gate
2535 /// nondeterministic surfaces on fields of this struct; production
2536 /// default keeps every field at `false / None / Auto` so the
2537 /// optimiser can const-fold the gate.
2538 pub fn env_cfg(&self) -> &testkit::EnvConfig {
2539 &self.env_cfg
2540 }
2541
2542 /// v7.38 元机制 D acceptor — single seed source for every
2543 /// nondeterministic engine subsystem (hash builders, randomised
2544 /// tie-breakers, …). Honour `SPG_TEST_RANDOM_SEED=N` when set;
2545 /// otherwise derive from the engine's wall clock (production) or
2546 /// fall back to a fixed sentinel when the host hasn't installed
2547 /// a clock. Two engines built with the same builder seed return
2548 /// byte-equal output for the same query.
2549 /// See `xtests/sigil/test-mode-gucs.md`.
2550 pub fn rng_seed(&self) -> u64 {
2551 if let Some(seed) = self.env_cfg.random_seed {
2552 return seed;
2553 }
2554 match self.clock {
2555 Some(f) => f() as u64,
2556 // Production engines without a clock installed get a fixed
2557 // non-zero sentinel; same shape as PG's `random()` start
2558 // state under a `setseed(0)`.
2559 None => 0xBAD_5EED_DEAD_BEEF,
2560 }
2561 }
2562
2563 /// v7.38 P0 元机制 A — push this engine's `InjectionStore` onto
2564 /// the thread-local stack so any `injection_point!()` reached
2565 /// during the returned guard's lifetime resolves against this
2566 /// engine. Mirrors PG's per-backend injection table.
2567 ///
2568 /// Returns a no-op guard when the `injection-points` feature is
2569 /// off so call sites don't need `#[cfg]`.
2570 pub fn enter_injection_scope(&self) -> crate::testkit::injection::InjectionGuard {
2571 #[cfg(feature = "injection-points")]
2572 {
2573 crate::testkit::injection::enter_scope(&self.injection_store)
2574 }
2575 #[cfg(not(feature = "injection-points"))]
2576 {
2577 crate::testkit::injection::new_guard()
2578 }
2579 }
2580
2581 /// v7.38 P0 元机制 A — expose the per-engine store so tests can
2582 /// query notice counts / detach actions without parsing SQL
2583 /// output. Only present when the feature is on.
2584 #[cfg(feature = "injection-points")]
2585 pub fn injection_store(&self) -> alloc::sync::Arc<crate::testkit::injection::InjectionStore> {
2586 self.injection_store.clone()
2587 }
2588
2589 /// Builder: cap the number of rows a single SELECT may return.
2590 /// Exceeding the cap raises `EngineError::RowLimitExceeded` —
2591 /// the bound is checked inside the executor so a runaway
2592 /// catalog scan can't allocate millions of rows before the
2593 /// server gets a chance to reject the result.
2594 #[must_use]
2595 pub const fn with_max_query_rows(mut self, n: usize) -> Self {
2596 self.max_query_rows = Some(n);
2597 self
2598 }
2599
2600 /// Builder: cap the approximate heap bytes a single SELECT's
2601 /// join/filter materialisation may hold. Exceeding the cap
2602 /// raises `EngineError::QueryBytesExceeded`. Rows are the wrong
2603 /// unit when one row carries a multi-MB body (mailrs round-26:
2604 /// 1000-row batches of full mail text walked a 15 GiB host into
2605 /// reclaim livelock without ever tripping a row ceiling).
2606 #[must_use]
2607 pub const fn with_max_query_bytes(mut self, n: usize) -> Self {
2608 self.max_query_bytes = Some(n);
2609 self
2610 }
2611
2612 /// The *committed* catalog. Note: during a transaction this returns the
2613 /// pre-TX state — `SELECT` inside a TX goes through `execute()` and reads
2614 /// the shadow. Tests that inspect outside-TX state should use this.
2615 pub const fn catalog(&self) -> &Catalog {
2616 &self.catalog
2617 }
2618
2619 /// Capture a frozen view of the committed engine state. Catalog
2620 /// is O(1) Arc bump; trailers are cheap clones. Decouples "capture"
2621 /// (needs &Engine) from "serialize" (CPU, no engine access) — the
2622 /// seam the background-checkpoint worker rides in CoW-2.
2623 pub fn snapshot_data(&self) -> EngineSnapshot {
2624 EngineSnapshot {
2625 catalog: self.catalog.clone(),
2626 users: self.users.clone(),
2627 publications: self.publications.clone(),
2628 subscriptions: self.subscriptions.clone(),
2629 statistics: self.statistics.clone(),
2630 }
2631 }
2632
2633 /// Serialize the *committed* catalog to bytes. v0.6 was full-snapshot; v0.9
2634 /// adds the rule that an open TX's shadow is never snapshotted — only the
2635 /// post-COMMIT state is persisted. v4.1 wraps the catalog in an envelope
2636 /// when there are users to persist; an empty user table snapshots as the
2637 /// bare catalog format (backwards-compat with v3.x readers). v6.1.2
2638 /// adds publications to the envelope condition: either non-empty
2639 /// users OR non-empty publications now triggers the envelope path.
2640 pub fn snapshot(&self) -> Vec<u8> {
2641 self.snapshot_data().serialize()
2642 }
2643
2644 /// True when at least one TX slot is in flight. v4.41.1 runtime
2645 /// invariant: at most one slot active at a time (dispatch holds
2646 /// `engine.write()` across the entire wrap). v4.42 will let this
2647 /// return true with multiple slots concurrently.
2648 pub fn in_transaction(&self) -> bool {
2649 !self.tx_catalogs.is_empty()
2650 }
2651
2652 /// v7.37 C.5 (A.2) — per-connection in-transaction test. A given
2653 /// connection is "in a transaction" iff its own `tx_id` has an open
2654 /// shadow slot. Unlike [`in_transaction`] (which is true if *any* tx is
2655 /// open), this lets concurrent connections each carry their own explicit
2656 /// transaction without colliding on the global slot. `IMPLICIT_TX` never
2657 /// has a persistent slot (autocommit reads/writes the main catalog), so
2658 /// this is false for the autocommit id.
2659 pub fn is_tx_open(&self, tx_id: TxId) -> bool {
2660 self.tx_catalogs.contains_key(&tx_id)
2661 }
2662
2663 /// v7.37 (round 828) — the user store THIS session should read:
2664 /// its transaction's role shadow when one exists, the committed
2665 /// store otherwise. The auth path and other sessions read
2666 /// `self.users` directly on purpose — an uncommitted role must not
2667 /// be visible to them, let alone able to log in.
2668 pub(crate) fn effective_users(&self) -> &crate::users::UserStore {
2669 if let Some(tx) = self.current_tx
2670 && let Some(state) = self.tx_catalogs.get(&tx)
2671 && let Some(shadow) = &state.users
2672 {
2673 return shadow;
2674 }
2675 &self.users
2676 }
2677
2678 /// v7.37 (round 828) — the store role DDL writes to: the TX's role
2679 /// shadow (created from the committed store on first use) inside a
2680 /// transaction, the committed store in autocommit. Every mutation
2681 /// of roles or memberships goes through here, so `BEGIN; CREATE
2682 /// ROLE r; ROLLBACK` leaves nothing behind — the shadow drops with
2683 /// the TxState — and COMMIT installs the shadow wholesale.
2684 pub(crate) fn role_ddl_users_mut(&mut self) -> &mut crate::users::UserStore {
2685 let tx_slot = self
2686 .current_tx
2687 .filter(|tx| self.tx_catalogs.contains_key(tx));
2688 match tx_slot {
2689 Some(tx) => {
2690 if self
2691 .tx_catalogs
2692 .get(&tx)
2693 .is_some_and(|state| state.users.is_none())
2694 {
2695 let committed = self.users.clone();
2696 if let Some(state) = self.tx_catalogs.get_mut(&tx) {
2697 state.users = Some(committed);
2698 }
2699 }
2700 self.tx_catalogs
2701 .get_mut(&tx)
2702 .and_then(|state| state.users.as_mut())
2703 .expect("role shadow ensured just above for an open tx slot")
2704 }
2705 None => &mut self.users,
2706 }
2707 }
2708
2709 /// v4.41.1 allocate a fresh TX handle. Used by spg-server dispatch
2710 /// to scope each implicit-wrap BEGIN..stmt..COMMIT to its own slot
2711 /// in `tx_catalogs`. v4.42 — the commit-barrier leader allocates
2712 /// one of these per task in its group, runs `BEGIN`+sql+`COMMIT`
2713 /// sequentially under a single `engine.write()` so each task's
2714 /// mutations accumulate into shared state, then either keeps the
2715 /// accumulated state (fsync OK) or restores the pre-image via
2716 /// `replace_catalog` (fsync err).
2717 pub fn alloc_tx_id(&mut self) -> TxId {
2718 let id = TxId(self.next_tx_id);
2719 self.next_tx_id = self.next_tx_id.saturating_add(1);
2720 id
2721 }
2722
2723 /// v4.42 — atomically replace the live catalog. Used by the
2724 /// commit-barrier leader to roll back a group whose batched
2725 /// fsync failed: the leader snapshots `engine.catalog().clone()`
2726 /// (O(1) Arc bump after the v4.39/v4.40 persistent migration)
2727 /// at group start, sequentially applies each task's BEGIN+sql+
2728 /// COMMIT under the same write lock to accumulate mutations
2729 /// into shared state, batches the WAL bytes, fsyncs once, and
2730 /// on failure calls this with the pre-image to undo every
2731 /// task in the group at once.
2732 ///
2733 /// **Does NOT touch `tx_catalogs` / `current_tx`.** Any
2734 /// explicit-TX slot from a concurrent client (created via the
2735 /// legacy `IMPLICIT_TX`-less dispatch path or via the future
2736 /// MVCC-readers v5+ work) has its own snapshot baked into the
2737 /// slot — restoring `self.catalog` to the pre-image leaves
2738 /// those slots untouched, exactly as they were when the leader
2739 /// took the lock. The leader's own implicit-TX slots are all
2740 /// already discarded (`exec_commit` removed them as each
2741 /// task's COMMIT ran) by the time this is reached.
2742 pub fn replace_catalog(&mut self, catalog: Catalog) {
2743 self.catalog = catalog;
2744 }
2745
2746 /// v6.7.0 — public shim around `Catalog::freeze_oldest_to_cold`
2747 /// so tests + the spg-server freezer can drive a freeze without
2748 /// reaching into the private `active_catalog_mut`. v6.7.4
2749 /// parallel freezer will build on this surface.
2750 ///
2751 /// Marks the table's cached `cold_row_count` stale because the
2752 /// freeze added cold locators that ANALYZE hasn't yet refreshed.
2753 pub fn freeze_oldest_to_cold(
2754 &mut self,
2755 table_name: &str,
2756 index_name: &str,
2757 max_rows: usize,
2758 ) -> Result<spg_storage::FreezeReport, EngineError> {
2759 let report = self
2760 .active_catalog_mut()
2761 .freeze_oldest_to_cold(table_name, index_name, max_rows)
2762 .map_err(EngineError::Storage)?;
2763 if let Some(t) = self.active_catalog_mut().get_mut(table_name) {
2764 t.mark_cold_row_count_stale();
2765 }
2766 Ok(report)
2767 }
2768
2769 /// v6.7.5 — public shim used by the spg-server follower's
2770 /// segment-forwarding receiver. Registers a cold-tier segment
2771 /// at a specific id (the master's id, as transmitted on the
2772 /// wire) so the follower's BTree-Cold locators stay byte-
2773 /// identical with the master's. Wraps
2774 /// `Catalog::load_segment_bytes_at` under the standard
2775 /// clone-mutate-replace pattern.
2776 ///
2777 /// Returns `Ok(())` on success **and** on the "slot already
2778 /// occupied" case — a follower mid-reconnect may receive a
2779 /// segment chunk for a segment_id it already has on disk
2780 /// (forwarded last session); the caller should treat that
2781 /// path as a no-op rather than a fatal error.
2782 pub fn receive_cold_segment(
2783 &mut self,
2784 segment_id: u32,
2785 bytes: Vec<u8>,
2786 ) -> Result<(), EngineError> {
2787 let mut new_cat = self.catalog.clone();
2788 match new_cat.load_segment_bytes_at(segment_id, bytes) {
2789 Ok(()) => {
2790 self.replace_catalog(new_cat);
2791 Ok(())
2792 }
2793 Err(StorageError::Corrupt(msg)) if msg.contains("already occupied") => Ok(()),
2794 Err(e) => Err(EngineError::Storage(e)),
2795 }
2796 }
2797
2798 /// v7.39 (round 598) — mutable access to the base catalog, for the
2799 /// recursive-CTE loop.
2800 ///
2801 /// It built a whole `Engine` per iteration to hold the working set:
2802 /// `Engine::restore` initialises 82 fields, and a counting allocator put
2803 /// the loop at 63 allocations and 104 kB PER ITERATION — 1 GB for a
2804 /// 10,000-row recursive CTE, none of it dependent on how much else was
2805 /// in the catalog. One engine, whose CTE table is refilled each round,
2806 /// needs this.
2807 pub(crate) fn base_catalog_mut(&mut self) -> &mut Catalog {
2808 &mut self.catalog
2809 }
2810
2811 pub(crate) fn active_catalog(&self) -> &Catalog {
2812 match self.current_tx {
2813 Some(t) => self
2814 .tx_catalogs
2815 .get(&t)
2816 .map_or(&self.catalog, |s| &s.catalog),
2817 None => &self.catalog,
2818 }
2819 }
2820
2821 fn active_catalog_mut(&mut self) -> &mut Catalog {
2822 let tx = self.current_tx;
2823 match tx {
2824 Some(t) => match self.tx_catalogs.get_mut(&t) {
2825 Some(s) => {
2826 // v7.39 (round 494) — see `TxState::shadow_dirty`.
2827 s.shadow_dirty = true;
2828 &mut s.catalog
2829 }
2830 None => &mut self.catalog,
2831 },
2832 None => &mut self.catalog,
2833 }
2834 }
2835
2836 /// v7.34 (crash-recovery P0 #2) — turn row-level redo capture on/off.
2837 /// The embedding layer enables it when persistence is on so each
2838 /// mutating `execute` records the physical [`RowChange`]s it applied
2839 /// (drained via [`Engine::take_redo`]). Off = zero capture overhead.
2840 pub fn set_redo_capture(&mut self, on: bool) {
2841 self.redo_capture = on;
2842 }
2843
2844 /// v7.39 (round 735, S14/B3) — record that `table`'s rows (or shape)
2845 /// changed. Cheap (one BTreeMap bump), called from every write entry;
2846 /// the materialized-view refresh watermark reads it.
2847 /// v7.39 (round 736) — per-view buffered-delta ceiling. Past this,
2848 /// the view's next REFRESH is a full one (the buffer is the
2849 /// optimisation, not the truth).
2850 pub(crate) const MATVIEW_DELTA_CEILING: usize = 65_536;
2851
2852 /// v7.39 (round 736) — fan the drained redo out to every
2853 /// maintainable view whose base table it touches.
2854 pub(crate) fn fan_out_matview_deltas(&mut self, drained: &[RowChange]) {
2855 if self.matview_maintainable.is_empty() {
2856 return;
2857 }
2858 for ch in drained {
2859 let t = ch.table_name().to_ascii_lowercase();
2860 let hit: Vec<String> = self
2861 .matview_maintainable
2862 .iter()
2863 .filter(|(_, base)| **base == t)
2864 .map(|(mv, _)| mv.clone())
2865 .collect();
2866 for mv in hit {
2867 if self.matview_delta_overflow.contains(&mv) {
2868 continue;
2869 }
2870 let buf = self.matview_delta_buf.entry(mv.clone()).or_default();
2871 if buf.len() >= Self::MATVIEW_DELTA_CEILING {
2872 self.matview_delta_overflow.insert(mv.clone());
2873 self.matview_delta_buf.remove(&mv);
2874 } else {
2875 buf.push(ch.clone());
2876 MATVIEW_FANOUT_BUFFERED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2877 }
2878 }
2879 }
2880 }
2881
2882 pub(crate) fn bump_table_change(&mut self, table: &str) {
2883 let k = table.to_ascii_lowercase();
2884 *self.table_change_seq.entry(k).or_insert(0) += 1;
2885 }
2886
2887 /// v7.37.8 — read accessor for tests / observability. The
2888 /// embedding layer flips this on once per `open_path` (after
2889 /// replay completes) when `SPG_WAL_ROW_REDO` is enabled (now
2890 /// default in v7.37.8). A consumer that wants to verify the
2891 /// post-upgrade contract ("writes go to V5 ROW_REDO by default")
2892 /// reads this through `Database::engine_redo_capture()` instead
2893 /// of inspecting WAL bytes (which the auto-checkpoint truncates
2894 /// on `Drop`).
2895 pub fn redo_capture_enabled(&self) -> bool {
2896 self.redo_capture
2897 }
2898
2899 /// v7.38 轴 4 — currently-selected SQL isolation level. Default
2900 /// `ReadCommitted` after construction; updated by
2901 /// `SET TRANSACTION ISOLATION LEVEL …`. Read by
2902 /// `SHOW transaction_isolation` and any future MVCC/SSI gate.
2903 pub fn current_isolation_level(&self) -> spg_sql::ast::IsolationLevel {
2904 self.current_isolation_level
2905 }
2906
2907 /// v7.34 — take the redo captured by the most recent successful
2908 /// mutating `execute` (empty when capture is off, the statement was a
2909 /// read, or it changed nothing). The embedding layer writes these to
2910 /// the WAL in place of the SQL text.
2911 pub fn take_redo(&mut self) -> Vec<RowChange> {
2912 core::mem::take(&mut self.last_redo)
2913 }
2914
2915 /// v7.34 (crash-recovery P0 #2) — replay a row-level redo log onto the
2916 /// committed catalog (the row-level WAL recovery primitive: apply the
2917 /// captured physical changes from a checkpoint baseline, in place of
2918 /// re-executing the SQL). Trusts the log — no uniqueness/FK/parse.
2919 pub fn apply_redo(&mut self, changes: &[RowChange]) -> Result<(), EngineError> {
2920 self.catalog
2921 .apply_redo(changes)
2922 .map_err(EngineError::Storage)
2923 }
2924
2925 /// Read-only execute path. Succeeds for `SELECT` / `SHOW TABLES`
2926 /// / `SHOW COLUMNS`; returns `EngineError::WriteRequired` for
2927 /// every other statement, so the caller can fall through to the
2928 /// `&mut self` `execute` path under a write lock. Engine state is
2929 /// not mutated even on the success path (`rewrite_clock_calls`
2930 /// and `resolve_order_by_position` both mutate the locally-owned
2931 /// AST, not `self`).
2932 ///
2933 /// v4.2: cap result-set size. Applied after the executor
2934 /// materialises rows but before they leave the engine — wrapping
2935 /// every Rows-returning exec_* function would scatter the check.
2936 ///
2937 /// v7.31 (memory campaign, bucket A) — the same choke point now
2938 /// also enforces the BYTE budget on the final result set, so
2939 /// single-table and aggregate paths (which don't route through
2940 /// the join materialiser's incremental accounting) still cannot
2941 /// hand the host an unbounded result. Intermediate single-table
2942 /// clones are the 7.31.x follow-up (design doc, bucket A).
2943 fn enforce_row_limit(
2944 &self,
2945 result: Result<QueryResult, EngineError>,
2946 ) -> Result<QueryResult, EngineError> {
2947 if let Ok(QueryResult::Rows { rows, .. }) = &result {
2948 if let Some(cap) = self.max_query_rows
2949 && rows.len() > cap
2950 {
2951 return Err(EngineError::RowLimitExceeded(cap));
2952 }
2953 if let Some(byte_cap) = self.max_query_bytes
2954 && approx_rows_bytes(rows) > byte_cap
2955 {
2956 return Err(EngineError::QueryBytesExceeded(byte_cap));
2957 }
2958 }
2959 result
2960 }
2961}
2962
2963/// v7.31 (memory campaign — ceiling-first / never-die, design v1) —
2964/// per-table slice of the engine's resident-memory accounting.
2965/// `hot_encoded_bytes` is the storage layer's maintained meter (what
2966/// the rows encode to); `approx_resident_bytes` is what they COST in
2967/// RAM (per-cell enum slots + heap payloads via `approx_row_bytes`)
2968/// — the gap between the two is the representation multiplier the
2969/// round-26 report measured at ~11× end-to-end.
2970#[derive(Debug, Clone)]
2971pub struct TableMemoryStats {
2972 pub name: String,
2973 pub hot_rows: u64,
2974 /// Cached cold-row count (refreshed by ANALYZE — see
2975 /// `Table::cold_row_count`'s staleness contract).
2976 pub cold_rows: u64,
2977 pub hot_encoded_bytes: u64,
2978 pub approx_resident_bytes: u64,
2979 pub index_count: u64,
2980 /// v7.31 C2 — sum of `IndexKind::approx_resident_bytes()` over the
2981 /// table's indices: every variant (BTree / NSW / BRIN / GIN family)
2982 /// walks its own structure, so the GIN posting lists and NSW layer
2983 /// adjacency that dominate text/vector tables are counted honestly
2984 /// instead of the old flat-token estimate.
2985 pub approx_index_bytes: u64,
2986}
2987
2988/// v7.31 — whole-engine memory snapshot: the polling form of the
2989/// round-26 ask-4 watermark signal. Hosts compare
2990/// `total_approx_resident_bytes` (+ their own WAL/file accounting)
2991/// against their deployment ceiling and shed/shrink before the
2992/// kernel does it for them.
2993#[derive(Debug, Clone)]
2994pub struct MemoryStats {
2995 pub tables: Vec<TableMemoryStats>,
2996 pub total_hot_encoded_bytes: u64,
2997 pub total_approx_resident_bytes: u64,
2998 pub total_approx_index_bytes: u64,
2999 /// The active per-query materialisation budget (bucket A), so a
3000 /// monitoring host sees ceiling and usage through one call.
3001 pub max_query_bytes: Option<usize>,
3002 /// v7.31 C2 — bucket D: live WAL bytes (active chunk + buffered,
3003 /// uncheckpointed). `None` from the engine itself — it has no WAL;
3004 /// the durable hosts (embed `Database`, server) fill it in from
3005 /// their own WAL accounting. `Some(0)` means "host has a WAL and
3006 /// it is empty"; `None` means "no WAL on this path" (in-memory).
3007 pub wal_bytes: Option<u64>,
3008}
3009
3010/// v6.2.0 — true for engine-managed catalog tables that the bare
3011/// `ANALYZE` (no target) should skip. v6.2.0 has no internal
3012/// tables yet (publications / subscriptions / users / statistics
3013/// all live as engine fields, not catalog tables), so this is a
3014/// reserved future-proofing hook — every existing user table is
3015/// analysed.
3016const fn is_internal_table_name(_name: &str) -> bool {
3017 false
3018}
3019
3020#[cfg(test)]
3021mod tests;