1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
//! v7.37.15 (Phase A) — per-row MVCC visibility header.
//!
//! ## Why this exists
//!
//! Pre-v7.37.15 SPG's MVCC story was at the **catalog level**:
//! `CatalogSnapshot` Arc-clones the catalog trie roots, readers see a
//! coherent prior-committed view, writers serialise. That works for
//! mailrs / sentori's SELECT-heavy IMAP workload, but caps multi-
//! writer throughput at "one writer at a time" forever and forces an
//! Arc clone of the entire catalog on each readonly statement —
//! mailrs measured a ~50% perf gap vs PG18 traceable to this.
//!
//! v7.37.15 adds **per-row** visibility on top of that catalog-level
//! Arc snapshot model. Both coexist:
//!
//! - The Arc snapshot path remains for SELECT-only fast reads — the
//! catalog trie clones are still O(1) Arc bumps.
//! - Each row in the table now carries a `RowHeader { xmin, xmax,
//! flags }`; scans filter rows against a `Snapshot { version,
//! in_progress }`.
//! - Writers update `xmin` on insert / `xmax` on delete or update,
//! so concurrent writers to different rows no longer block one
//! another (granularity = per-row, not per-catalog).
//!
//! ## Why u64 instead of PG's 32-bit Xid
//!
//! PG carries the historical scars of a 32-bit transaction id — epoch
//! advancement, FrozenTransactionId, VACUUM FREEZE, the entire
//! anti-wraparound machinery. SPG starts fresh: `xmin` and `xmax`
//! are `u64`. At 1ns per transaction (a contemporary CPU's L1 cycle
//! budget) u64 wraps in 584 years; we explicitly do NOT implement
//! wraparound handling because we will never hit it.
//!
//! ## Layout: parallel `PersistentVec`, not embedded in `Row`
//!
//! The header lives in `Table::headers: PersistentVec<RowHeader>`
//! **parallel** to `Table::rows: PersistentVec<Row<'static>>`. Why
//! not embedded in the Row struct?
//!
//! 1. **Cache locality on visibility-only scans.** A scan that only
//! needs to count visible rows (think `SELECT COUNT(*)`) walks
//! headers without touching row bodies. With the header inline
//! every cache line carries one row's worth of payload; with the
//! header in a separate Vec the scan only loads 24-byte headers,
//! yielding ~10x throughput on wide-row tables.
//! 2. **Public API stability.** `pub struct Row { pub values }` is
//! the shape every caller — eval / sort / agg / projection —
//! already pattern-matches. Adding a header field would break
//! every match arm in the codebase for a field most call sites
//! don't care about.
//! 3. **Per-row freeze**. The visibility map (per-segment
//! `all_visible` bitmap) is an `&[bool]` slice over headers —
//! parallel storage makes the bitmap construction zero-copy.
//!
//! ## Backward compatibility
//!
//! Rows that come from a pre-v7.37.15 envelope (V1-V5) have no
//! header on disk. On load, every such row gets a default
//! `RowHeader::frozen()` (`xmin = 1`, `xmax = 0`,
//! `flags = HEAP_XMIN_FROZEN`). Visibility checks against any
//! valid snapshot return `true` — so old data is fully visible to
//! everyone, matching the pre-v7.37.15 contract.
use ;
/// Bit 0 of `RowHeader.flags`: `xmin` is conceptually-`FrozenXid` — the row
/// existed at process start (loaded from a V1-V5 envelope) and is
/// unconditionally visible to every snapshot.
pub const HEAP_XMIN_FROZEN: u8 = 1 << 0;
/// Bit 1: the row is the head of a HOT chain (v7.37.15 Phase D).
/// Hot-tier in-place UPDATE optimisation; cold tier never sets this.
pub const HEAP_HOT_UPDATED: u8 = 1 << 1;
/// Bit 2: the row is a HOT chain non-head element (v7.37.15 Phase D).
pub const HEAP_ONLY_TUPLE: u8 = 1 << 2;
/// Bit 3: the row's `xmax` is conceptually-frozen — the delete is
/// older than any live snapshot, so vacuum may reclaim it on the
/// next pass.
pub const HEAP_XMAX_FROZEN: u8 = 1 << 3;
/// Sentinel value used for `xmax` when the row has NOT been deleted.
/// PG uses `InvalidTransactionId = 0`; SPG matches.
pub const XMAX_ALIVE: u64 = 0;
/// Sentinel used for `xmin` on rows loaded from a pre-v7.37.15
/// envelope. Any non-zero value < every real transaction id works
/// — we pick 1 (PG uses `FrozenTransactionId = 2`).
pub const XMIN_FROZEN: u64 = 1;
/// Per-row MVCC visibility header.
///
/// 24 bytes after alignment (8 + 8 + 1 + 7 padding). The padding
/// is intentional: a power-of-two stride keeps array indexing
/// cheap and matches the cache-line layout PG uses for
/// `HeapTupleHeaderData`.
/// Process-wide monotonic version counter shared by every Database
/// instance in this process. `RowHeader.xmin / xmax` values + the
/// reader's `Snapshot.version` all draw from here.
///
/// `u64` so we never wrap. Starts at `XMIN_FROZEN + 1 = 2` so a
/// fresh transaction can never collide with frozen rows.
///
/// Process-wide (not per-Database) so concurrent databases in the
/// same process share a coherent view of "is tx 17 still alive" —
/// the BitSet `Snapshot.in_progress` keys off the same numbering.
static GLOBAL_VERSION: AtomicU64 = new;
/// Allocate the next transaction id / row version. Caller stores
/// it in the row's `xmin` (for insert) or `xmax` (for delete /
/// update). Threadsafe; no lock involved.
/// Read the current version cursor without advancing. Snapshots
/// use this as their `Snapshot.version`.
/// v7.38 — recover the version cursor past a version read off a durable
/// image. `GLOBAL_VERSION` lives in process memory and restarts at
/// `XMIN_FROZEN + 1`, but rows persisted by an earlier process carry the
/// versions *that* process allocated. A fresh process must not hand out a
/// version any restored row already uses, and — because `Snapshot::visible`
/// rejects `xmin > version` as "written by a future transaction" — must take
/// snapshots at a version above every restored `xmin`, or committed rows
/// silently vanish from reads. The same applies to `xmax`: a delete that
/// looks like the future would resurrect the deleted row.
///
/// This is the version-cursor twin of the `next_rowid` recovery in
/// `codec::read_mvcc_header_appendix`, and mirrors PG recovering `nextXid`
/// from `pg_control` rather than restarting the counter at zero.
///
/// `XMAX_ALIVE` (0) carries no version and is ignored.
/// v7.37.15 (Phase C.1) — stable per-relation row identity.
///
/// ## Why a stable id, separate from the physical index
///
/// Pre-Phase-C a row was addressed by its **physical index** into
/// `Table::rows`. That index is invalidated the moment a delete /
/// vacuum compacts the survivor vec — every surviving row after the
/// hole shifts down. Physical indices therefore cannot serve as:
///
/// 1. a **row-lock key** (Phase C.4: `(RelId, RowId)` must survive
/// concurrent compaction while a lock is held),
/// 2. a **HOT-chain pointer** (Phase D: chain head → new version
/// must not dangle after vacuum),
/// 3. a **WAL redo identity** (Epic W: `RowChange` UPDATE/DELETE
/// must name the row by a key that survives replay, not a slot
/// that shifted — closing the position-fragility caveat on the
/// `RowChange` doc).
///
/// `RowId` is per-relation, monotonic, and **never reused**. It
/// lives in `Table::rowids: PersistentVec<RowId>` parallel to
/// `rows` / `headers`, so `rowids[i]` is the stable id of the row
/// physically at slot `i`. Compaction rebuilds all three vecs
/// together, so the id travels with the row while the slot shifts.
///
/// Phase C.1 introduces the id additively (allocated + kept
/// lock-step, but indices still address by physical slot); later
/// phases migrate index locators, the lock table, and the WAL to
/// address by `RowId`.
///
/// `u64`, never wraps (same rationale as `xmin`/`xmax`). Starts at
/// 1 per relation; 0 is reserved as an "unassigned" sentinel.
;
/// v7.37.15 (Phase C.1) — stable per-catalog relation identity.
///
/// ## Why a stable id, separate from the table's `Vec` position
///
/// A `Catalog` stores tables in a `Vec<Table>`; a `DROP TABLE`
/// removes one, shifting every later table's position down. So the
/// physical `tables[i]` index cannot key:
///
/// 1. the **row-lock table** — Phase C.4 keys locks by
/// `(RelId, RowId)`; a lock held across a concurrent `DROP TABLE`
/// of an *unrelated* table must keep naming the same relation,
/// 2. the **`RelationStore` shard map** — Phase C.5 splits the
/// single catalog latch into a `DashMap<RelId, _>` of per-relation
/// locks; the key must survive catalog mutation,
/// 3. a **replication relation mapping** — Epic R maps a change to
/// its relation by a stable id, not a shifting slot.
///
/// `RelId` is per-catalog, monotonic, and **never reused** even after
/// the table is dropped, so a stale lock / redo reference is
/// detectable rather than silently aliasing a table that reused the
/// slot. It pairs with [`RowId`] to form the `(RelId, RowId)` tuple
/// identity Phase C.4's lock table needs.
///
/// Introduced additively in Phase C.1: assigned at `CREATE TABLE`
/// and stored on the table, but nothing consumes it yet. `u64`,
/// never wraps; 0 is the `RelId::UNASSIGNED` sentinel, real ids start
/// at 1.
;