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
// transaction.rs — Transaction lifecycle, savepoints, commit protocol, and data operations.
// This is the orchestration layer (layer 6 in the module graph per ARCHITECTURE.md) that ties
// together the handle table, data pages, overflow pages, freemap, superblock, and page
// cache into a coherent transactional API.
//
// Durability model (shadow paging, no WAL):
// - Writes never overwrite live pages. Mutations go to freshly allocated pages via
// PageCache::new_page() and the new roots are threaded through a rebuilt handle
// table spine (COW). The previously-committed pages remain intact on disk until
// the new superblock supersedes them.
// - A commit becomes visible atomically when a new superblock with a higher
// txn_counter and a valid checksum is fsync'd to its (alternating) slot.
// - Crash recovery = open_existing() runs Superblock::select() and picks the
// highest-txn_counter superblock with a valid checksum. A torn/partially-written
// new superblock fails its checksum, so the previous committed state wins —
// no log replay, no undo.
//
// Concurrency model:
// - A TransactionManager is single-writer. active_txn guards against nested begin().
// Multi-process exclusion is enforced at the file layer by flock() in PageIo;
// only one TransactionManager may hold the database open at a time.
// - TransactionManager is NOT internally thread-safe — callers must serialize
// access. Readers and writers share the same PageCache; there is no MVCC.
//
// In-memory vs on-disk state during an open transaction:
// - All mutations live in the PageCache as dirty entries. Nothing mutated by the
// transaction is durable (or even written to the file in general) until commit().
// - The superblock on disk still points at committed_roots; current_roots lives
// only in memory. A crash mid-transaction discards all dirty pages from cache
// and the on-disk superblock still references the prior committed snapshot.
// - NOTE: `new_page()` (file extension) extends the underlying file immediately;
// data-page allocation (via `cow_alloc`) prefers reuse from the committed
// freemap tree but also calls through to `new_page()` when the freemap is
// empty. Either way, any pages
// extended-but-uncommitted before a crash are harmless because nothing in the
// committed superblock references them, and the rollback path
// (`cache.truncate(committed_roots.total_pages)` — I3) actively shrinks the
// file on a clean rollback so they don't accumulate at all.
//
// In-memory mode: `TransactionManager::create_new` and `open_existing` are
// backend-agnostic — whether the underlying PageIo is backed by a file (with
// flock) or by a Vec<u8> (no flock, no durability) is invisible here. The
// in-memory entry points live in `lib.rs` and just hand this module a
// memory-backed PageIo. Every transactional invariant in this file (commit
// ordering, poison on fatal error, watermark rollback) applies equally to the
// in-memory backend.
use ;
// I127 (ISSUES.md, 2026-06-21): FxHashMap (not std SipHash) for the per-op
// slot-accounting maps (the SlotPacker live-slot maps in packing.rs,
// Savepoint.live_slots below, and the open-time scan map in recovery.rs).
// Keys are trusted local u64 page ids — no DoS surface — so SipHash is pure cost,
// exactly the I77 rationale; that pass converted the page cache/LRU but missed
// these. FxHashMap is a drop-in std HashMap with a faster non-DoS-resistant hasher.
use ;
use crateDataPage;
use crate;
use crateFreeMapTree;
use crate;
use crate;
use crateOverflow;
use crate;
use cratePageCache;
use crateChiselCounters;
use crate;
// Largest value stored inline in a data-page slot. Larger values are written to an
// overflow chain and referenced by a single HandleEntry with HandleFlags::Overflow.
//
// I117 (ISSUES.md, 2026-06-21): COMPUTED from the page constants (was a
// hand-maintained `8162` literal with only a prose "keep in sync" note). A data
// page's usable body is `CHECKSUM_OFFSET - DATA_PAGE_HEADER_SIZE`, minus one
// `SLOT_ENTRY_SIZE` slot-directory entry. Deriving it makes drift impossible —
// which is what makes the `.expect("value fits in empty page")` in
// `insert_into_data_page` safe by construction: a value `<= MAX_INLINE_VALUE`
// always fits an empty page, so that expect is structurally unreachable.
const MAX_INLINE_VALUE: usize =
CHECKSUM_OFFSET - DATA_PAGE_HEADER_SIZE - crateSLOT_ENTRY_SIZE;
/// Snapshot of the mutable "pointers" that define a consistent database state.
/// A commit succeeds by writing a superblock that references exactly these roots;
/// a rollback succeeds by reverting current_roots back to committed_roots.
///
/// The `named_roots` array is part of this snapshot (ISSUES.md F2) so that
/// set_root_name / clear_root_name participate in the transactional commit
/// point for free — a rollback or `rollback_to` restores named roots at
/// the same time it restores the handle-table root, with no extra plumbing.
/// A nested rollback point within an active transaction.
///
/// Captures the roots and the `next_page_id` watermark at savepoint
/// creation time. `rollback_to(name)` restores the roots and calls
/// `cache.truncate(watermark)`, which drops every cache entry and
/// truncates the file back to the watermark — cleanly discarding every
/// page the transaction allocated after the savepoint (ISSUES.md I3).
///
/// `freed_pages` is still tracked per-savepoint so a future freemap
/// reclamation pass (R2) can restore freed-but-not-yet-reclaimed pages
/// if a savepoint is rolled back to. It is a distinct concern from the
/// cache-level rollback that the watermark handles.
///
/// `live_slots` and `insert_cursor` snapshot the R1 packing state
/// (live slot counts per data page + the current in-progress insert
/// cursor). `rollback_to` restores these so a savepoint rewind leaves
/// the packer in a consistent state. Cloning the HashMap is O(map
/// size) per savepoint but savepoints are rare in the target workloads
/// (drop_table / delete_many don't use them).
/// The single writer for a Chisel database. Not thread-safe; file-level mutual
/// exclusion across processes is provided by flock() in PageIo. Holds both the
/// last durably-committed roots (for reads outside a txn and for rollback) and
/// the in-progress current_roots (only valid while active_txn is true).